Daily AlpacaHack Writeup (2026-06-1 ~ 2026-06-7)
Daily AlpacaHack の Writeup.
(2026/6/1 - 2026/6/7)
Half-Year Recap (Misc, 2026/6/1)
- Welcome 問題
- フラグをコピペする
- 関係ないが, 最近解ける問題が増えてきたのでそろそろ B-side にチャレンジしたいと思っている
Cache Me If You Can (Web, 2026/6/2)
/flagに二回アクセスするとフラグが表示されるアプリケーション- しかし, 一回アクセスするとレスポンスがキャッシュされるため, 二度目のアクセス時にキャッシュがそのまま返されてしまう
Varyヘッダーが付与されるようなセッションの使用などはないが, nginx はシンプルな設定のため URL を変えることでキャッシュにヒットせずフラグが獲得できた
curl http://34.170.146.252:21724/flag -v curl http://34.170.146.252:21724/flag?a=1 -v
vm1 (Misc, 2026/6/3)
- node の vm モジュールについての問題
- vm モジュールを用いた環境で任意のコードが実行できるため, 仮想環境外の環境変数からフラグを読み込むことを目的とする問題
- 通常の仮想マシンであれば仮想環境の外側にアクセスすることは難しいが, ドキュメントには vm モジュールがそのような目的の使用を想定したものではないことが書かれている
- 調べてみると Alpaca Hack の作問などをしている t-chen さんの記事 がヒットした
- どうやら
runInNewContextの第二引数にthisでアクセスすることができるらしく, そこからプロトタイプで親要素にさかのぼっていくことで vm モジュールの呼び出し元の環境へのアクセスが可能になる - 以下のペイロードで環境変数一覧を表示しフラグを獲得
this.constructor.constructor('return process.env')()
Small e (Crypto, 2026/6/4)
eが小さい RSA 暗号e=5は十分小さいので 5 乗根を探せば解が出てくる可能性が高い ($m^5 < n$ の場合)gmpy2.irootを用いて 5 乗根を探してフラグ獲得
from Crypto.Util.number import long_to_bytes from gmpy2 import iroot n = 16128804155875357893998760311719646398956415880698030561267568338090219290157891603532638172238485032759477580902658887080008705298094907918287851011968741355832250883201674570984807747020548844000957785264007870180470757091580750057164459793752714479791809579867574578330198492457059782036488831975650496564440123488599724632450667071325022385750061329492222092884811845354233534224359344208133180005606806945605297101512063251571527406665365800913912218217052716171501186370562004264601496219549705742961866516829669705353956908439174513725075192615787533907765532381255155275740682938535103389401380698116201073921 e = 5 c = 64426504087303951813246838078441662275222839708900359133341615671136342522173619331456628167122546441601604084831803481393964091908425281568746434522937994926680805728974560373917459735665811835839800559253331009380339923486729675042508940809673293031690986041297853338860088143997503137266972063924345287607333207047324410623795727456034926116681588542164755751442528534922088003628572923240330092448273804434443462174179583212626649843445735888430144970416125998013969727919556751231951554897102023125257208495501 m = iroot(c, e) print(long_to_bytes(m[0]))
RPS GAME (Misc, Crypto, 2026/6/5)
- Python の
random.getrandbits(1)で無作為に選ばれる手と 1000 回じゃんけんをし, 6 割以上の勝率を上げるとフラグ獲得できる問題 random.getrandbits()はメルセンヌツイスターを使用するため, 624 回分のデータで以降の出力を予測可能だが, 今回の場合 3 回分の出力すべてを確認することができないためこの手法は難しそうgetrandbits(1)は出力が 0, 1 の二値のため, 以下のように偏りのある分布が生まれる (左が乱数生成結果, 右が選ばれる index)
0, 0, 0: 0 1, 0, 0: 1 0, 1, 0: 0 0, 0, 1: 0 1, 1, 0: 2 1, 0, 1: 1 0, 1, 1: 0 1, 1, 1: 0
- 上記より $\frac{5}{8}$ で 0 番目の手が選ばれるため, それにかつ手を出し続ければ勝てる (期待値 $5^4=625$)
handsがラウンドごとに引き継がれるがhands[0]は相手の出した手のため, 前のラウンドで相手が出した手に勝つ手を出し続けてフラグ獲得
from pwn import * _, host, port = "nc 34.170.146.252 45243".split() sh = remote(host, port) HANDS = ["r", "p", "s"] next_hand = "p" for i in range(1000): prompt = sh.recvuntil(">".encode()) print(prompt.decode()) sh.sendline(next_hand.encode()) prompt = sh.recvuntil("Opponent:".encode()) print(prompt.decode()) opp, you = sh.recvline().decode().strip().split(",") next_hand = HANDS[(HANDS.index(opp) + 1) % 3] sh.interactive()
Flag for localhost (Web, 2026/6/6)
- ローカルホストからのアクセスの場合にフラグを獲得できるプログラム
- nginx を用いたプロキシを経由してアクセスするため, アクセス元の情報は
X-Forwarded-Forヘッダーが参照されるはず - プロキシでは基本的にアクセス元 IP アドレスから順番に経由したアドレスを
X-Forwarded-Forの値の末尾に追加するため, アクセス時にX-Forwarded-Forヘッダーにアドレスをセットしておくとアクセス元が底だと認識されることが多い - 以下のようにローカルホストからのアクセスを偽装する SSRF でフラグ獲得
curl http://34.170.146.252:42474/ -H "X-Forwarded-For:127.0.0.1"
C++ flag checker (Rev, 2026/6/7)
- C++ で作成されたフラグチェッカー
- Ghidra で逆コンパイルするとフラグのエンコード過程で
std::transform<>(uVar6,uVar5,uVar4,uVar3);という処理が見つかった - この関数について調べると配列の各要素に処理を加える関数であり, 四つの引数はベクトルの開始と終了などの位置であり, それ以外に適用する処理を表す lambda 式が入るはずであることが分かった
- lambda 式はそこには見つからなかったので
transformで呼び出される中身についていろいろ調べると,_ZSt9transform~という処理 (波線は続きの名前を端折っている) の中にoperatorやlambdaという言葉が出てくる (このように関数に対応して作られる処理のラベル? は C++ における マングリング というものらしい) lambdaで検索するとundefined __thiscall operator()で_ZZ6encode~というものが呼ばれているのを見つけた_ZZ6encode~の中身ではb**3 + a^0x55の処理がされていた- 以上に基づき以下のように入力エンコードの逆処理を書いて保存されていたエンコード済みフラグを処理に書けるとフラグ獲得
stored = b'\xfc\x6c\xe5\xc9\xee\x63\x2e\x49\xfc\x06\x72\x66\xc8\xb8\x0a\x44\xdc\x1b\xf0\x6b\x82\x93\x27\x91\x92\x9c\x7a\x17\x62\xf0\x3a\x74\x9a\x9d\xf7\x15\x59\x99\x3d\xc5\x6b\x5b\x4a\xad\x3e\x17\x33\x89\x61\x4d\xfc\xe0\x2b\xf9\x27\xf9\x3c\xfc\x7c\x77\x13\x3f' flag = "" for i, char_byte in enumerate(stored): flag += chr(((stored[i] - (i+10)**3)&0xff)^0x55) print(flag)