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 managementOnboardingPlugin– welcome announcementsHelpPlugin– 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:
|
|
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:
|
|
Identifying the target – who is admin_murkymare59 and where are they?
|
|
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:
|
|
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:
|
|
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:
|
|
The !nego command is designed for creating negotiation channels for malware deals. The rules are:
- You need exactly 4 users (yourself + 3 others)
- One must be a moderator (
mod_*prefix) - All 4 must be in the channel where you run the command
- 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:
|
|
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:
|
|
[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
!negocommand 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
!negobug 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.