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.



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.


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.

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.

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.

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



[user]
email = cody@searcher.htb
name = codySecond, 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.



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


[remote "origin"]
url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.gitOne password, three doors. It logs into Gitea as cody: profile page, private Searcher_site repository, done.

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:


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.

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

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=giteaThe 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.

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:

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.

#!/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
4717e9a7421b50d7f83b266cb7894fdaRoot flag, no root shell, same read-oracle pattern the foothold used. The box closes itself.


