General··6 min read

HTB Busqueda

1. Recon: a vhost and a confession

Hosts entry, full-port scan first. Two ports, and this time the detail scan shows why the app needs a name: port 80 answers with a redirect to http://searcher.htb/. The application lives on a virtual host, and browsing the bare IP never reaches it.

HTB Busqueda
hosts file with busqueda.htb entry
HTB Busqueda
full-port scan: 22 and 80 only
HTB Busqueda
detail scan: redirect to searcher.htb

The landing page is Searcher: pick a search engine, type a query, get a link. And the footer does what Codify's about page did, one smaller line at a time: "Powered by Flask and Searchor 2.4.0". The version banner is the whole recon summary.

HTB Busqueda
Searcher landing page
HTB Busqueda
form test, footer names Searchor 2.4.0

Searchor's GitHub security tab confirms the target: advisory GHSA-66m2-493m-crh2, arbitrary code through eval() in Searchor's search handling, versions up to 2.4.2 affected.

HTB Busqueda
Searchor advisory GHSA-66m2-493m-crh2

2. Foothold: one line from the advisory to a shell

The bug is an f-string handed to eval(): the query parameter lands inside Python source. A public exploit by nikn0laty wraps the payload with a fifo netcat callback, and after sorting out which hostname the vhost actually answers on, penelope catches the shell: PTY as svc@busqueda, standing in /var/www/app. The Flask app's own account, the same shape as Codify's svc.

HTB Busqueda
exploit run, penelope catches svc@busqueda

3. User: the config that kept giving

The flag sits in svc's home and reads directly. sudo -l demands svc's password, which we do not have yet; remember that for later.

HTB Busqueda
user flag as svc

linpeas points at two things. First, svc's home carries a .gitconfig: the app deploys as a person.

HTB Busqueda
linpeas downloaded
HTB Busqueda
home listing: .gitconfig
HTB Busqueda
.gitconfig: cody
[user]
	email = cody@searcher.htb
	name = cody

Second, Apache's own vhost file shows the box holds a second application no external scan can see: gitea.searcher.htb, proxied to 127.0.0.1:3000. A loopback Gitea, the same only-from-localhost pattern as Chemistry's port 8080, this time routed by hostname. Hosts aliases go in, and the user list answers: administrator and cody.

HTB Busqueda
Apache vhosts: gitea.searcher.htb to 127.0.0.1:3000
HTB Busqueda
hosts aliases added
HTB Busqueda
Gitea users: administrator and cody

Now the payload. /var/www/app is a live git checkout, and .git/config stores the push credentials in the remote URL, plain text:

HTB Busqueda
git log after the safe.directory fix
HTB Busqueda
.git/config: credentials in the remote URL
[remote "origin"]
	url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git

One password, three doors. It logs into Gitea as cody: profile page, private Searcher_site repository, done.

HTB Busqueda
Gitea as cody

It fails SSH: cody@searcher.htb answers Permission denied. And it opens svc's sudo: the earlier password prompt failure was a wrong password, not a missing grant. With it, sudo -l reads:

HTB Busqueda
git log --stat; ssh as cody refused in the background
HTB Busqueda
sudo -l: system-checkup.py
User svc may run the following commands on busqueda:
    (root) /usr/bin/python3 /opt/scripts/system-checkup.py *

4. Root: inspect, format, and a relative path

/opt/scripts is root-owned and execute-only: the scripts run through sudo but never show their source, and the directory's own .git is root-only. The wildcard argument means the script's subcommands decide everything, so probe by execution.

HTB Busqueda
execute-only script directory

docker-ps confirms the architecture: Gitea and a MySQL 8 database, both bound to loopback, both fed by the same compose stack.

HTB Busqueda
docker-ps: gitea and mysql_db

docker-inspect takes a Go template plus a container name, and {{json .}} dumps everything. MySQL containers keep their secrets in environment variables, and this one is no exception:

MYSQL_ROOT_PASSWORD=jI86kGUuj87guWr3RyF
MYSQL_USER=gitea
MYSQL_PASSWORD=yuiu1hoiu4i5ho1uh
MYSQL_DATABASE=gitea

The mounts say as much as the env: the database directory binds from /root/scripts/docker/mysql. The stack we cannot read lives in /root/scripts, and source code that lives somewhere usually has a copy in the git forge it was pushed from.

Reuse check, mapped properly. The root password does not open Gitea's administrator. The MySQL service password does. That is the chain's biggest single step: the database account's secret is also the Gitea admin's secret, and the private scripts repository opens.

HTB Busqueda
administrator: scripts repo

The repo holds system-checkup.py's source, the thing execute-only permissions were protecting. The full-checkup action is one line of interest:

elif action == 'full-checkup':
    try:
        arg_list = ['./full-checkup.sh']
        print(run_command(arg_list))

./full-checkup.sh is a relative path, and subprocess.run resolves it against the process's current working directory. sudo resets the environment but not the directory: stand in /tmp, and root executes whatever full-checkup.sh you planted there.

Prove it before weaponizing it. A first version writes id to a file:

HTB Busqueda
oracle run: uid=0(root)
svc@busqueda:/tmp$ cat proof.txt
uid=0(root) gid=0(root) groups=0(root)

One operational note from that step: the first runs failed with "Something went wrong" because the planted file had no execute bit. subprocess.run raised PermissionError into a bare except that prints nothing useful. Check the boring things first.

Then the real one, and it does not even need a root shell: let the root process copy the flag somewhere world-readable.

HTB Busqueda
flag exfiltrated through the root-run script
#!/bin/bash
cat /root/root.txt > root.txt

svc@busqueda:/tmp$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
[+] Done!
svc@busqueda:/tmp$ cat root.txt
4717e9a7421b50d7f83b266cb7894fda

Root flag, no root shell, same read-oracle pattern the foothold used. The box closes itself.

References

Related Posts