Daily AlpacaHack Writeup (2026-05-18 ~ 2026-05-24)
Daily AlpacaHack の Writeup.
(2026/5/18 - 2026/5/24)
Flag for Switch (Web, 2026/5/18)
- User-Agent ヘッダーの問題. Switch からのアクセスを装えばフラグが獲得できる
- ヘッダーは自由に付与できるため, 例として curl で以下のようにヘッダを指定してあげることでフラグ獲得
curl http://34.170.146.252:13697/ -H "user-agent:Switch"
- switch でやったり switch2 で試してみたいけどまだ持ってない......買いたい............
Small d (Crypto, 2026/5/19)
- RSA 暗号において秘密鍵が小さいと Wiener's Attack で復号できることが知られている (連分数を用いた解読方法, 後でもっと詳しく理解したい)
- 過去に picoCTF で Wiener's Attack (以前参考にした解説記事) を行う問題が出ていたため, その時のコードをそのまま使って解いた
from Crypto.Util.number import long_to_bytes n = 111353847434782342577552021595590550258011828841846535858152583507530588771722260002279996578408863620383269850809207716294963798987223718377857807468589048019837035104532011966123698255324737304049629882745865145426217333401985769872460052131723411921295220461420858919693342128249451360271877470431299412631 e = 24535416463309049799324247328967492260372258790953192167956315042327246069286185977587725024528971333528391611263562578561986228715126642586011994924792498076763835710601221847870887310752328685048945207115800606067006433946397499522425453643335216887549768999628574755599652803334025268917547913285044084805 c = 50703818050337116536617131553410132728558556353681003166889460230372336158097688107289097585329177184508245739367286475582331506026328888441167187947148823875004985350563368702349649387915676041889533193939056249122702228179939214260718957322242668719048863890867936693696170949742006007767253492755830780599 def continued_fraction(numerator, denominator): a = [] while denominator != 0: a.append(numerator // denominator) numerator, denominator = denominator, numerator % denominator return a def convergent(a): if len(a) == 1: return [a[0], 1] else: numerator = [a[0], a[0] * a[1] + 1] denominator = [1, a[1]] for ai in a[2:]: numerator[0], numerator[1] = numerator[1], ai * numerator[1] + numerator[0] denominator[0], denominator[1] = denominator[1], ai * denominator[1] + denominator[0] return [numerator[1], denominator[1]] def wieners_attack(e, n, ciphertext): a = continued_fraction(e, n) for i in range(1, len(a)): d = convergent(a[:i])[1] plaintext = long_to_bytes(pow(ciphertext, d, n)) if plaintext[:7] == b"Alpaca{": print(f"d: {d}\n{plaintext.decode()}") break wieners_attack(e, n, c)
- 今度まじめに勉強します
Kakuzuke (Web, 2026/5/20)
- A, B の選択肢を送ると
sendFileでファイルが送られる Web アプリケーション - 選択肢を選ぶとファイル名がパラメータとして送られるため, パストラバーサルが考えられる
- フラグは相対パスで
../../flag.txtの位置に存在 - 入力パラメータは
choice.length > 5という形で長さチェックが入っているため, 文字列を送る場合は flag にたどり着けない - body パラメータで
choiceに配列を送れば 5 文字の制限を無視してできそうだが, 配列は文字列として解釈される際にカンマ区切りで結合されるため, カンマをどうにかしなければいけない
["a", "b", "c"] -> "a,b,c"
choice[]="a"のように要素が一つの配列として送ろうとすると,app.use(express.urlencoded({ extended: false }));の設定のためchoice[]という別のパラメータとして処理されてしまう- ファイルパス系によくある話で,
sendFile関数も引数として受け取るパスに対して正規化が行われるため,/a/../b -> /bのように..を用いて間の文字をないものにすることができる - 以下のようにパラメータを設定することで無事
../../flag.txtを取得できた
choice=..%2f..%2f&choice=%2f..%2fflag.txt
Python:Impossible (Misc, 2026/5/21)
- Python で
assert文を回避することができればフラグが獲得できる問題 assertの条件はn > 0 & n < 0であり必ず引っ掛かるようにされているevalなどがないため引数をどうにかしてフラグを表示させるのは無理そうだが, 環境変数が指定できるためassertに関連する環境変数を調べる- 調べてみると, PYTHONOPTIMIZE という環境変数があると, 最適化オプションとして
assert文が回避できるらしいので, それを設定することでフラグが表示できた (参考)
env> PYTHONOPTIMIZE=1 arg> 1
Time Travelers' OTP (Web, 2026/5/22)
- 時間を基準にした OTP について, 4000 年後の OTP の値を入力できればクリアの問題
- OTP の生成関数について調べていると,
hotp関数で現在時刻を 30 秒で割った数countを 8 バイトのbufに書き込む処理があった - MDNのドキュメントによると js のビット演算は 32 ビット整数として演算を行うようなので, $\mod 2^{32}$ が適用される
const buf = Buffer.alloc(8); buf.writeUInt32BE((counter & 0xffff_ffff_0000_0000) >>> 32, 0); buf.writeUInt32BE(counter & 0x0000_0000_ffff_ffff, 4);
- 現在時刻と 4000 年後のサイトの時刻のずれが
countにすると $2^{32}-3$ となるため, 現在時刻のパスワードが $30 \times 3$ 秒後には, 4000 年後のサイトのパスワードとなる - Google Authenticator で確認して 1 分半待ってパスワードを入力することでフラグを獲得できた
Even Worse RSA (Crypto, 2026/5/23)
- 一つの素数 $p$ で構成された RSA 暗号
- $gcd(p-1, e) > 1$ のため単純に復号鍵を作成することができなくなっていた
- 今回の問題について, $p-1, e$ の最大公約数を $gcd(p-1, e) = s$ とする時, $d_s \equiv (e/s)^{-1} \mod p$, $ms \equiv c^{d_s} \mod p$ とすると以下が成立
$$c^{d_s} \equiv m^{e \cdot d_s} \equiv m^{s} \mod p$$
- なお, $e \cdot d_s = s(k(p-1)/s+1) = k(p-1) + s$ を用いた
- 以上より $m^s$ が分かる. この問題では $s=6$ だったため, $m^6$ の 6 乗根となる数の候補を有限体上の多項式の根から列挙した (sage math を使用)
p = 8751921425256563367579143227840921849402469143061750238936013324282215699146538047799233649294185141005855739102550788165605861428703197268970229186963997 e = 65538 c = 5947948986109551330433379864390441851954259789762156065124570979131577895849125770689468451948141963015046816934387597958310386264293643862965407787651953 s = gcd(p-1, e) ds = inverse_mod(e//s, (p-1)//s) ms = pow(c, ds, p) F = GF(p) R.<x> = PolynomialRing(F) roots = (x^6 - F(ms)).roots() print(roots)
- 出てきた候補のそれぞれを文字列に直して, "Alpaca" で始めるものを見つけてフラグ獲得
from Crypto.Util.number import long_to_bytes out = [(8751921425256563367579143227840921849402469143061750238936013324282215699146508455868971569227438191699182987583855507423051406495692586500325392336604064, 1), (7126573897339959323788465857603278061713294396243758437540473331141761726344851828441509900989818118248377183806579046290267698460427051570076614172330847, 1), (7126573897339959323788465857603278061713294396243758437540473331141761726344822236511247820923071168941704432287883765547713243527416440801431777321970914, 1), (1625347527916604043790677370237643787689174746817991801395539993140453972801715811287985828371113972064151306814667022617892617901286756467538451864993083, 1), (1625347527916604043790677370237643787689174746817991801395539993140453972801686219357723748304367022757478555295971741875338162968276145698893615014633150, 1), (29591930262080066746949306672751518695280742554454933010610768644836850359933, 1)] for tp in out: print(long_to_bytes(tp[0]))
Counquer Ultimate Device Abyss (Rev, 2026/5/24)
- CUDA 問題
- GPU で動かすのは設定にてこずりそうと思ったが,
chal.cuを読むとコード自体はただの行列演算を用いたフラグチェッカーであることが分かった - 演算としては
((flag - 29*C) @ np.linalg.inv(B)) / 12に相当するため, その通りに numpy で書き下して文字に戻すとフラグが獲得できた
import numpy as np flag = [ [0x00000ee698dea1b1, 0x00000e9dd071a07b, 0x00000c1924ac2e63, 0x00000db7c6567969], [0x00000cf7dca22a8d, 0x00000d361500d93b, 0x00000a4b9a1df377, 0x00000c43dffefdad], [0x000010b0871e1f25, 0x0000108db4c64da3, 0x00000c9baca0e87f, 0x0000100afc00a4b1], [0x00000f83590cad41, 0x00000ead68311d37, 0x00000bf80ff254c3, 0x00000e85f5468875], ] B = [ [0xde, 0xad, 0xbe, 0xaf], [0xab, 0xad, 0x1d, 0xea], [0xca, 0xfe, 0xba, 0xbe], [0xfe, 0xed, 0xfa, 0xce]] C = [ [0x61706c41, 0x61486163, 0x44206b63, 0x796c6961], [0x61706c41, 0x61486163, 0x44206b63, 0x796c6961], [0x61706c41, 0x61486163, 0x44206b63, 0x796c6961], [0x61706c41, 0x61486163, 0x44206b63, 0x796c6961]] flag = np.array(flag) B = np.array(B) C = np.array(C) rec_A = ((flag - 29*C) @ np.linalg.inv(B)) / 12 print(rec_A) rounded_A = np.rint(rec_A).astype(np.int32) recovered = b"".join(int(x).to_bytes(4, "little", signed=True) for x in rounded_A.flatten()) print(recovered)
- CUDA のコードの書き方を調べる機会はなかなかないので楽しかった