Overview
Snoopy is a Hard-rated Linux box that chains together a long list of
misconfigurations and freshly-disclosed CVEs. The path starts with a ../ filter
bypass in a file-download endpoint, which is enough to read the BIND9 configuration
and leak the TSIG key protecting the DNS zone. With that key we take control of
mail.snoopy.htb, redirect Mattermost password-reset mail to a controlled SMTP sink,
and take over an account. Inside Mattermost, a custom /server_provision slash
command can be pointed at an attacker-controlled host, so we catch cbrown’s SSH
credentials with a honeypot. From cbrown we abuse a regex-restricted sudo git apply rule (CVE-2023-23946) with a symlink patch to write into sbrown’s home.
Finally, sbrown can run clamscan --debug as root, and CVE-2023-20052 - an XXE in
ClamAV’s DMG parser - leaks root’s SSH private key straight into the debug output.
The interesting part of this box is that the last two stages rely on CVEs that had essentially no public PoCs at release, so both require reading source/patches and building the exploit by hand.
| |
Reconnaissance
Nmap
| |
Three services are exposed:
| |
The OpenSSH and BIND banners point to Ubuntu 22.04. DNS on TCP 53 is unusual to see externally and immediately worth testing for zone transfers. HTTP is the obvious first enumeration surface.
Web (TCP 80)
The site is a marketing page for a security firm, “SnoopySec”. The team/about pages
list employees with @snoopy.htb emails (cbrown, sbrown, cschultz, vgray,
lpelt, …), which become a username list. A banner on the contact page is a strong
hint about the intended path:
| |
The landing page links to /download and /download?file=announcement.pdf, both
returning a press_release.zip archive. A parameter that names a file on disk
(?file=) is the first thing to test for traversal.
Subdomain enumeration
Virtual-host fuzzing against the Host header finds one extra name:
| |
mm.snoopy.htb responds differently and turns out to host a Mattermost instance.
Registration is disabled, but the login page exposes a “Forgot your password?” flow -
worth keeping in mind given the mailserver banner.
DNS (TCP/UDP 53)
BIND allows a full zone transfer:
| |
| |
The 172.18.0.0/16 addresses look like Docker containers (Mattermost, Postgres, a
“provisions” host). Crucially, mail.snoopy.htb is absent, which matches the
“mailserver offline” banner - if that record can be created, the box’s own DNS
infrastructure will hand over the mail server role.
Initial Access
File read via ../ filter bypass
A direct traversal attempt returns nothing, so there’s some filtering. Fuzzing an LFI wordlist shows that a doubled-up sequence works:
| |
The bug is a classic non-recursive replace: the backend strips ../ a single time,
so ....// collapses to ../ after the strip. The downloaded content begins with
PK, i.e. the requested file is wrapped inside the returned ZIP. Confirming with
/etc/passwd gives the box’s shell users:
| |
Reading /download.php via /proc/self/cwd/download.php reveals the server logic
and confirms it’s file_get_contents-style read (not an include), so there’s no
code execution here - just arbitrary file read:
| |
Leaking the BIND TSIG key
Since the box is mid “DNS migration”, the BIND config is the natural target. Reading the main config file:
| |
exposes the shared key:
| |
And named.conf.local shows what that key is allowed to do:
| |
allow-update with key "rndc-key" means anyone holding this TSIG secret can add,
modify or delete records in the snoopy.htb zone. That’s the entire point of the
file-read primitive.
Hijacking mail.snoopy.htb
With the key saved to rndc.key, nsupdate performs an authenticated dynamic update
to point the missing mail record at an attacker-controlled host:
| |
TSIG (RFC 2845) authenticates the UPDATE message with the shared secret, so BIND
accepts it as if it came from a trusted secondary. Re-querying confirms
mail.snoopy.htb now resolves to the attacker’s IP. The record tends to get reset
periodically, so the next step has to be done reasonably quickly.
Intercepting the password reset
With mail pointed at the attacker host, an SMTP sink is stood up and a Mattermost
password reset is triggered for [email protected]:
| |
The reset mail lands on the listener:
| |
The token is in quoted-printable encoding (=3D is =, and a trailing = marks
a soft line break). After decoding those sequences, the link works, a new password is
set, and the login succeeds into Mattermost as sbrown.
Abusing the provisioning slash command
The Town Square channel has two useful conversations: staff discussing a new server
provisioning tool, and staff mentioning that ClamAV is running on their servers
(a hint for root). Searching channels surfaces the Server Provisioning channel,
where typing / shows a non-default command: /server_provision.
The command opens a form asking for an OS and a target IP. Selecting Linux (which
uses TCP 2222) and pointing it at an attacker-controlled host produces an inbound
connection:
| |
So the provisioning bot SSHes into whatever host it’s given. A DM from cbrown
afterwards asks whether the box is set up yet - the bot is authenticating with real
credentials. Swapping the raw listener for an SSH honeypot captures them:
| |
Submitting the provisioning form again logs the credentials:
| |
Those creds work over SSH on the real box:
| |
Privilege Escalation
cbrown to sbrown (CVE-2023-23946)
cbrown has a tightly-scoped sudo rule:
| |
The regex allows exactly one argument with no spaces or special characters, so the
usual git apply --unsafe-paths --directory ... trick is impossible - the only
argument permitted is a single patch filename. git is 2.34.1, which is vulnerable
to CVE-2023-23946: git apply is supposed to refuse writing through a symbolic
link, but the check can be defeated when the symlink is created by the same patch
before it is used, giving arbitrary file write.
The plan is to write an attacker-controlled public key into sbrown’s
authorized_keys. First, a repo containing a symlink to sbrown’s .ssh directory
(world-writable dir so the sbrown sudo process can unlink the symlink):
| |
Then a patch that renames the tracked symlink and, in the same apply, writes a file through the renamed link:
| |
Applying it as sbrown writes into /home/sbrown/.ssh/authorized_keys:
| |
Because the file path (patch) matches the regex and contains no forbidden
characters, the sudo rule is satisfied while the symlink inside the patch does the
real work. With the attacker key in place:
| |
The user flag is at /home/sbrown/user.txt:
| |
sbrown to root (CVE-2023-20052)
sbrown can run ClamAV’s scanner as root, again behind a regex:
| |
So clamscan --debug can be run on any file placed in ~/scanfiles. ClamAV is
1.0.0, vulnerable to CVE-2023-20052: an XXE in the DMG file parser. When
ClamAV parses a DMG, it reads the embedded Apple plist XML; if that XML defines an
external entity, the parser resolves it, and the resolved content is printed in
--debug output. That turns a scan into an arbitrary file read as root.
Building a DMG with an XXE payload
A plain ISO from genisoimage isn’t enough - ClamAV recognizes it as ISO9660 and
never enters the DMG code path:
| |
Scanning progname.dmg confirms this - the debug log shows Matched signature for file type ISO9660 and no plist parsing. To get a proper UDIF DMG (with the koly
trailer and XML plist ClamAV expects), libdmg-hfsplus wraps the ISO, with
resources.c patched to inject the XXE.
Two edits in dmg/resources.c:
- Add a DOCTYPE with an external entity pointing at root’s key in
plistHeader:
| |
- In
writeResources(), emit the entity reference&xxe;in place of the array key so it gets resolved, and drop the footer so the leaked value shows cleanly in debug output.
Worth noting as a small speed bump: the first build attempt pasted an explanatory note directly into the source instead of as a comment, and the compiler predictably rejected it (
error: unknown type name 'We'). Re-editing to keep the change inside valid C and recompiling fixed it - a reminder to keep injected payloads syntactically valid for whatever is compiling them.
Build and wrap the ISO into a DMG:
| |
| |
Triggering the leak
Host c.dmg, pull it onto the target into the sudo-permitted directory, and scan it:
| |
ClamAV now enters the DMG parser (Found koly block, cli_scandmg: XML offset ...),
resolves the external entity, and dumps root’s private key where it expected the
blkx value:
| |
The trailing comment decodes to [email protected], confirming it’s root’s key.
Root access
Save the key, fix permissions, and log in:
| |
| |
Detection and Mitigation
| Stage | Root cause | Mitigation |
|---|---|---|
| Path traversal | Non-recursive ../ strip in /download | Canonicalize the resolved path and compare against an allow-listed base directory, don’t strip patterns |
| TSIG key exposure | BIND config readable via the traversal | Never store DNS TSIG secrets in a location reachable by a web application’s file-read primitive; restrict file permissions |
| DNS hijack | allow-update with a leaked key and no additional scoping | Scope allow-update to specific record types/names where possible; rotate keys regularly |
| Mattermost account takeover | Password reset delivered to an attacker-controlled mail record | Validate mail server identity independent of DNS (e.g. pinned MX/TLS); rate-limit and alert on password-reset volume |
| Credential exposure via slash command | /server_provision authenticated to attacker-supplied hosts with real credentials | Never let a bot authenticate outbound to user-supplied, unvalidated hosts; scope provisioning targets to an allow-list |
| CVE-2023-23946 (git apply symlink) | Regex-restricted sudo git apply still allowed a symlink-based write | Patch Git; don’t rely on argument regexes to make an inherently dangerous operation safe |
| CVE-2023-20052 (ClamAV DMG XXE) | Regex-restricted sudo clamscan --debug on attacker-supplied files | Patch ClamAV; disable external entity resolution in any XML parser; avoid running scanners with elevated privileges on attacker-influenced input |
Detection signals:
- Repeated
/download?file=requests with doubled traversal sequences (....//). nsupdate/dynamic DNS UPDATE messages originating from non-secondary IP addresses.- Password-reset emails routed to a mail server whose DNS record changed recently.
- Outbound SSH connections initiated by an internal automation account (the provisioning bot) to external IP addresses.
sudoinvocations ofgit applyorclamscanfollowed immediately by unexpected file writes/reads outside the target file.
Lessons Learned
- Non-recursive sanitization is not sanitization. Stripping
../a single time is trivially bypassed with....//. Filtering should be recursive or, better, replaced with canonicalization plus an allow-list. - Leaked TSIG keys are full zone control. A DNS
allow-updatekey is as sensitive as a password; combined with a file-read primitive it hijackedmail.snoopy.htband the entire password-reset flow. - Regex-restricted
sudorules are hard to get right. Both privilege-escalation steps were “locked down” with regexes, yet both were still exploitable - thegit applyrule via an in-patch symlink (CVE-2023-23946), and theclamscan --debugrule via a DMG XXE (CVE-2023-20052). Constraining arguments doesn’t help when the allowed operation is itself dangerous. - Parsers that resolve external entities are file-read primitives. ClamAV resolving XXE in a DMG plist, and surfacing it in debug output, turned an antivirus scan running as root into arbitrary file disclosure.
Command Reference
| |