PipePunisher Banner

Breaking the PIPE: Abusing PIPE to Shell Installations

A CyberSecurity deep dive into shell installer abuse using PipePunisher. Learn how common install patterns like PIPE to Shell using curl and wget can open the door to attackers.

PipePunisher Logo

PIPE|Punisher>$

Think piping curl or wget to a shell is harmless? PipePunisher shows how that one-liner can turn into a security nightmare.

I bet that at least at some point in your life you’ve come across a project that uses a “one-liner” command with PIPE to shell in order to install it.

For example: rust language, nim language… there are hundreds of applications and/or systems that use PIPE to shell as a means of installation… I caught myself thinking: is that safe?

Let’s take the rust installer as an example:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

When you paste this command into the terminal, what it does is download the file to “sh.rustup.rs” but it doesn’t save it, it displays it. Since the command has a “|(PIPE) that redirects the output directly to the “sh” shell, the contents of the file are executed “on-the-fly” in the shell…

But what’s wrong with that?

The script has full access to the shell, meaning that anything can be executed since you are exposing the command interpreter.

So before sending the script to the shell, all you have to do is look at its contents by accessing the link and analyzing the commands, right? We have 2 problems here:

  1. People are dumb, especially today’s pseudo-system administrators.
  2. There is a way to display a legitimate script to the user but send a malicious script to the shell!
Display one script but execute other?

A user with basic knowledge will always try to read the contents of the script before passing it to the shell, but there is a technique used by advanced groups and government organizations such as NSA and MOSSAD.

When curl or wget is used in conjunction with a shell, be it “sh”, “bash”, “zsh“… the behavior of the connection may suffer a “variation”.

And this variation can be used to determine whether the connection is being forwarded to a shell, resulting in the sending of a malicious script, or whether it is a normal connection and thus sending the legitimate script.

How the attack works

In reality, it’s not exactly the connection that changes when using PIPE to shell but the timing. The key to everything is timing, the shell is slow and needs to ingest and process the content line by line.

Time Delay approach

If we send a wait command like “sleep” at the beginning, the shell will execute it, pausing the stream until sleep is executed. This pause is where the magic is because it will allow us to detect when the connection is being sent to a shell.

Obviously there’s no point in putting the wait on the server as the connection will “die” locally and not be paused… it’s much more complex than it looks!

The sending and receiving of TCP transmissions is buffered for each socket. Our job is to fill the buffers before sending the “verification” data and thus maintain control of the transmission.

We will know when the client’s receive buffer is full once we receive a TCP WINDOW Size Flag equal to 0.

PIPE to shell flow

The values of the send and receive buffers are calculated dynamically, check /proc/sys/net/ipv4/tcp_wmem for the send buffer and /proc/sys/net/ipv4/tcp_rmem for the receive buffer.

/proc/sys/net/ipv4/tcp_wmem

4096	16384	4194304

MIN = 4096, DEFAULT = 16384, MAX = 4194304

/proc/sys/net/ipv4/tcp_rmem

4096	131072	6291456

MIN = 4096, DEFAULT = 131072, MAX = 6291456

(Values obtained from a laptop running Debian)

We can control the server’s send buffer but not the client’s, so we must specify a value to try to fill the client’s receive buffer.

Well, obviously to be successful we’ll have to fill the client’s receive buffer somehow… the problem is that everything we send will appear in the terminal or be processed by the shell…

Hiding the terminal output

The only character I know of that can be used to fill the buffer and not appear in the terminal is the “NULL byte” character.

Since we don’t know what the content-length of the transmission will be, we’ll use the Transfer-type: chunked.

Each chunk will be a string of NULL bytes of the same size as the send buffer (we’ll talk more about this later).

At the moment our connection will be something like:

HTTP/1.1 200 OK
Server: Apache
Date: Fri, 27 Jun 2025 21:32:02 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
sleep 3                                 <-- chunk #1
0x0000000000000000000000000000000000... <-- chunk #2
0x0000000000000000000000000000000000... <-- chunk #3
0x0000000000000000000000000000000000... <-- chunk #4

Detecting shell execution

For this difficult task we will need to use mathematics, good old math!

Given the nature of the attack, which involves processing network streams where 1ms is enough to destabilize the connection, I decided to code everything in the fastest programming language ever created, the C language. If you don’t know how to program in C yet, check out the lessons in our free course.

Look at the following function, here we get the “standard deviation” which we will use to calculate the “variance”.

double standard_deviation(double *data, int n) {

    if (n <= 1) return 0.0;
    double mean = 0.0, sum_deviation = 0.0;
    for (int i = 0; i < n; i++) mean += data[i];
    mean /= n;
    for (int i = 0; i < n; i++) sum_deviation += (data[i] - mean) * (data[i] - mean);
    return sqrt(sum_deviation / (n - 1));

}

By combining the calculation of the “jump” that the chunks experience in the connection, we can tell whether the connection is being sent to a shell or not, as long as the connection has the minimum stability.

To automate all of this, the calculations, the jumps, the chunks and everything else, I present to you: PipePunisher, a tool developed specifically for this purpose.

You can download PipePunisher from our github repository or install it on ANDRAX-NG via AFOS-NG.

PipePunisher help

The following arguments are required: --endpoint, --runner, --good, --bad, --address and --port.

  • --endpoint (This is where you should specify which endpoint the user will access, for example “/install.sh”)
  • --runner (The script that contains the delay command)
  • --good (The script with the actual content)
  • --bad (The script with the malicious code to be executed)
  • --address (The ip address where the server will wait for the connection)
  • --port (The port on which the server should listen)
  • --size (The size of the TCP send buffer) [optional]

PipePunisher need a certificate for the https connection, in the case of real-world use you should use a service to create SSL certificates, something like Let’s Encrypt…

However, as we are talking about a test and research environment, we will use a self-signed certificate.

Use OpenSSL to create the self-signed certificate:

openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem

The key.pem and cert.pem files must be in the same directory as when PipePunisher is called.

The command I’m going to use is:

pipepunisher --endpoint /install.sh --runner runner.sh --good good.sh --bad bad.sh --address 0.0.0.0 --port 2233 -s 60000

runner.sh:

sleep 3

good.sh:

echo "You are SAFE... for now!"

bad.sh:

echo "You have a problem because you has been HACKED!"

Let’s test two scenarios. In the first, we will use cURL to download the script, but without passing it to the shell. PipePunisher will simply output the contents of the script to the terminal, simulating a user trying to analyze it before executing it.

The second scenario is a real-world example in which the user enters the command into the terminal and waits for it to “install” Then, PipePunisher detects the shell and sends the contents of bad.sh.

PipePunisher Sending Good Payload

As you can see here, the PipePunisher is handling a connection that isn’t being sent to the shell. As a result, it sends the contents of the good.sh file.

The result using curl in the terminal looks like this:

Curl Good payload from PipePunisher

Now if the connection is passed to the shell, PipePunisher will detect it as follows:

PipePunisher Sending Bad Payload

Pay attention to JUMP

The result in the terminal will look like this:

Curl Bad payload from PipePunisher

How to prevent this attack

The best scenario would be to never run a remote installation script using pipe to shell…

I open the script in the browser, download it, and then run it directly from local storage.

But what if the developer is trustworthy?

Even if the script is hosted on a legitimate and trustworthy developer’s server, it’s not good practice to enter it directly into the shell!

A chain attack targeting your ISP or the developer’s server Gateway could create a Man-In-The-Middle (MITM) connection that would be nearly impossible to distinguish from the real connection.

That said, stay safe, my friend...