Bash Redirection Tricks for Turning a Scan Into a Target List: Piping the Pentest

Every “getting started in pentesting” post shows the same one-liner: scan a subnet, grep the live hosts, feed them to a brute-force tool. The one-liner is usually treated as a fact to memorize rather than something built out of parts. This post takes it apart. The subject is bash’s redirection and process-control primitives, not how to brute-force SSH: Hydra, Medusa, Ncrack, Patator, and Metasploit’s ssh_login module all get a passing mention at the end and nothing more. If you want the brute-force tools themselves compared properly, that is a separate post.

Everything below only ever targets loopback stand-ins running in a sandbox (127.0.0.1 through 127.0.0.3, standing in for hosts on a lab subnet). Command output is real, not invented. In the examples the loopback addresses are relabeled to 192.168.1.10 / .11 / .12 for readability, matching the placeholder range used elsewhere on this site. Run any of this only against hosts you own or are explicitly authorized to test.

The building blocks

Four redirection operators cover most of what you need:

> file      redirect stdout, overwrite
>> file     redirect stdout, append
2> file     redirect stderr only
2>&1        duplicate stderr onto whatever stdout currently points at

The one that trips people up is 2>&1, because it does not mean “send stderr to stdout” in the abstract. It means “point file descriptor 2 at whatever file descriptor 1 currently points at, right now.” Order matters, and it’s easy to get backwards. Here’s what actually happens with each order, using a real nmap SYN scan attempted without root (SYN scans need a raw socket, so this reliably throws a stderr-only error):

$ nmap -sS 192.168.1.10 -p 22 > out.txt 2>&1
$ cat out.txt
You requested a scan type which requires root privileges.
QUITTING!

That’s the correct order: >file first points stdout at the file, then 2>&1 points stderr at the same place stdout now points, which is the file. Both streams land there. Reverse the two and it breaks:

$ nmap -sS 192.168.1.10 -p 22 2>&1 > out.txt
You requested a scan type which requires root privileges.
QUITTING!
$ cat out.txt

Here 2>&1 runs first and points stderr at wherever stdout was pointing at that moment, which is still the terminal. Then >file moves stdout to the file, but stderr already has its own separate target and stays pointed at the terminal. The error prints to your screen and the file is empty. This is not a corner case; it’s the single most common redirection bug in shell scripts, and the fix is just to read the operators left to right in the order bash actually applies them.

/dev/null is the usual sink when you want a stream gone entirely: 2>/dev/null to drop error noise, >/dev/null 2>&1 to drop everything and only check the exit code.

The one-liner, and where it breaks

The starting point is something like:

nmap 192.168.1.0/24 -p 22 --open | awk '/report for/ {print $5}'

This works, most of the time. It breaks the moment reverse DNS resolves a name for one of the targets. Here’s a real scan against three hosts where one of them resolves to localhost:

$ nmap 192.168.1.10-12 -p 22 --open
Starting Nmap 7.80 ( https://nmap.org ) at 2026-08-02 16:27 BST
Nmap scan report for localhost (192.168.1.10)
Host is up (0.0020s latency).

PORT   STATE SERVICE
22/tcp open  ssh

Nmap scan report for 192.168.1.11
Host is up (0.0055s latency).

PORT   STATE SERVICE
22/tcp open  ssh

Nmap done: 3 IP addresses (2 hosts up) scanned in 13.05 seconds

Two things to notice. First, 192.168.1.12 never appears at all: --open silently drops any host with no matching open port from the report, so “no line for that host” means closed, not scanned-and-empty. Second, look at what field 5 actually is on each line:

$ nmap 192.168.1.10-12 -p 22 --open | awk '/Nmap scan report for/ {print $5}'
(192.168.1.10)
192.168.1.11

On the resolved host, field 5 is (192.168.1.10) with the parentheses still attached, because the line reads Nmap scan report for localhost (192.168.1.10) and the hostname shifted every field after it by one. A fixed field number only survives as long as nothing before it changes shape. The fix is to stop counting fields and match the pattern instead:

$ nmap 192.168.1.10-12 -p 22 --open \
    | awk '/Nmap scan report for/ {match($0, /([0-9]{1,3}\.){3}[0-9]{1,3}/); print substr($0, RSTART, RLENGTH)}'
192.168.1.10
192.168.1.11

match() finds the IPv4 pattern wherever it sits on the line and substr() pulls out exactly those characters, regardless of whether a hostname, extra whitespace, or anything else shifted the field positions around it. This is the extraction worth actually using; keep the field-number version as the thing to recognize and avoid.

tee and process substitution: fan one stream into several

tee writes its input to one or more files and also passes it through to stdout unchanged. Combined with process substitution, >(command), it can fan a single stream into several independent commands at once, each one treated as if it were a file:

$ cat hostlist.txt \
    | tee >(sed 's/^/[hydra target] /' > branch1.out) \
          >(sed 's/^/[medusa target] /' > branch2.out) \
    > /dev/null
$ cat branch1.out
[hydra target] 192.168.1.10
[hydra target] 192.168.1.11
$ cat branch2.out
[medusa target] 192.168.1.10
[medusa target] 192.168.1.11

Each >(...) gets its own copy of every line tee receives, so the same host list can be staged for two different tools without scanning twice or writing the list out twice by hand.

One real gotcha caught while writing this: running that exact pipeline as a bare one-liner from some automation contexts can silently drop one branch entirely, the process substitution’s subshell doesn’t finish writing before the parent shell moves on and the file descriptor gets torn down early. It was consistently reproducible from one invocation method and consistently fine from another (a saved script, or a bash -c block with the pipeline followed by an explicit wait) in the same sandbox. The practical lesson: don’t trust a multi-branch tee fan-out you’ve only ever tested interactively. Put it in a script, add wait after it if it’s driven from something that might tear the shell down immediately afterward, and check that every branch file actually has content before wiring it into anything unattended like cron or CI.

xargs: turning a line-oriented list into per-target commands

awk and tee get you a clean list. xargs turns each line into an argument for a command you run once per host:

$ cat hostlist.txt | xargs -I{} echo "would launch: hydra -l admin -P wordlist.txt ssh://{}"
would launch: hydra -l admin -P wordlist.txt ssh://192.168.1.10
would launch: hydra -l admin -P wordlist.txt ssh://192.168.1.11

-I{} defines a placeholder and substitutes each input line into it. On its own this just runs one host after another, which is fine for two hosts and painfully slow for two hundred. -P runs multiple in parallel. A real timing comparison against three one-second targets:

ModeCommandWall time
Sequentialfor h in ...; do sleep 1; done3.0s
Parallel, -P3xargs -P3 -I{} ...1.0s

For a target list in the tens or low hundreds, -P is the difference between a scan that finishes during a coffee break and one that finishes overnight. It’s also the first knob to turn back down if a target’s rate limiting starts throttling or blocking you: -P controls your own concurrency, not the remote service’s tolerance for it.

Gating the next stage: pipefail and &&

A pipeline’s exit code, by bash’s default, is the exit code of the last command in it, not whichever command actually failed. That default silently hides a broken nmap invocation behind a perfectly happy awk:

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

With pipefail set, the pipeline’s exit code reflects any failure in the chain, not just the last stage. Combined with &&, that lets you gate the brute-force stage on the recon stage actually having produced something:

set -o pipefail
nmap 192.168.1.0/24 -p 22 --open \
  | awk '/Nmap scan report for/ {match($0, /([0-9]{1,3}\.){3}[0-9]{1,3}/); print substr($0, RSTART, RLENGTH)}' \
  > hostlist.txt \
&& [ -s hostlist.txt ] \
&& cat hostlist.txt | xargs -P4 -I{} echo "would launch: hydra -l admin -P wordlist.txt ssh://{}"

[ -s hostlist.txt ] checks the file is non-empty, which matters given --open can legitimately produce a zero-line result. Without that check, an xargs on an empty file just runs zero times and looks like success either way; the explicit check is what actually tells you nothing was found versus something ran.

Here-strings for quick, file-free testing

A here-string (<<<) feeds a single value to a command’s stdin without a temp file, which is useful for testing an extraction pattern against one line before running it against a real scan:

$ awk '{print "read via here-string:", $1}' <<< "192.168.1.10"
read via here-string: 192.168.1.10

Cheap to type, nothing to clean up afterward.

Putting it together

The full chain: scan, extract with a pattern match rather than a field number, gate on non-empty output, then fan out in parallel.

set -o pipefail
nmap 192.168.1.0/24 -p 22 --open 2>/tmp/nmap.err \
  | awk '/Nmap scan report for/ {match($0, /([0-9]{1,3}\.){3}[0-9]{1,3}/); print substr($0, RSTART, RLENGTH)}' \
  | tee hostlist.txt \
  | wc -l

Every piece of that line is one of the operators covered above: a redirected stderr so nmap’s own noise doesn’t get mixed into the awk input, a pipe into a regex-based extraction instead of a fragile field number, and a tee that saves the list to a file while still letting the count flow through to wc -l for a sanity check.

None of this is specific to SSH or to Hydra. The same pattern, scan, extract robustly, gate on results, fan out in parallel, applies just as well to feeding a web-title grab into a screenshot tool, feeding a live-host list into a vulnerability scanner, or feeding a subdomain list into a certificate check. The brute-force tool at the end is replaceable. The plumbing in front of it is the part worth actually learning.

For the brute-force stage itself, the Password Cracking and Wordlist Engineering series covers offline cracking once you have a hash, and the Sn1per Deep Dive series covers what an orchestrator does with a host list once it has one. A dedicated comparison of the online brute-force tools themselves, Hydra, Medusa, Ncrack, Patator, and Metasploit’s auxiliary modules, is coming as a follow-up.