
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.
Disclaimer
This content is provided for educational and informational purposes only. The techniques and tools discussed, are intended to raise awareness about security risks and help developers and system administrators protect their systems.
We do not encourage, support, or condone any form of unauthorized access, exploitation, or malicious activity. All demonstrations were conducted in controlled environments with proper authorization.
Use this knowledge responsibly and always adhere to your local laws and ethical guidelines. Hacking should only be performed in environments where you have explicit permission.
The author assumes no responsibility for any damages or legal consequences arising from the misuse of this content. Always ensure you have proper authorization before testing or auditing any system!
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:
- People are dumb, especially today’s pseudo-system administrators.
- 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.
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.
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.
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:
Now if the connection is passed to the shell, PipePunisher will detect it as follows:
Pay attention to JUMP
The result in the terminal will look like this:
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...