Mark Mossberg's Blog

Hacker Nonsense

Netcat "-e" Analysis

As I mentioned in a previous post, netcat has this cool -e parameter that lets you specify an executable to essentially turn into a network service, that is, a process that can send and receive data over the network. This option is option is particularly useful when called with a shell (/bin/sh, /bin/bash, etc) as a parameter because this creates a poor man’s remote shell connection, and can also be used as a backdoor into the system. As part of the post-exploitation tool I’m working on, I wanted to try to add this type of remote shell feature, but it wasn’t immediately obvious to me how something like this would be done, so I decided to dive into netcat’s source and see if I could understand how it was implemented.

Not knowing where to start, I first tried searching the file for “-e” which brought me to:

case 'e':           /* prog to exec */
  if (opt_exec)
ncprint(NCPRINT_ERROR | NCPRINT_EXIT,
    _("Cannot specify `-e' option double"));
  opt_exec = strdup(optarg);
  break;

This snippet is using the GNU argument parsing library, getopt, to check if “-e” is set, and if not, setting the global char* variable opt_exec to the parameter. Then I tried searching for opt_exec, bringing me to:

if (netcat_mode == NETCAT_LISTEN) {
  if (opt_exec) {
ncprint(NCPRINT_VERB2, _("Passing control to the specified program"));
ncexec(&listen_sock);       /* this won't return */
  }
  core_readwrite(&listen_sock, &stdio_sock);
  debug_dv(("Listen: EXIT"));
}

This code checks if opt_exec is set, and if so calling ncexec().

 1 /* Execute an external file making its stdin/stdout/stderr the actual socket */
 2 
 3 static void ncexec(nc_sock_t *ncsock)
 4 {
 5   int saved_stderr;
 6   char *p;
 7   assert(ncsock && (ncsock->fd >= 0));
 8 
 9   /* save the stderr fd because we may need it later */
10   saved_stderr = dup(STDERR_FILENO);
11 
12   /* duplicate the socket for the child program */
13   dup2(ncsock->fd, STDIN_FILENO);   /* the precise order of fiddlage */
14   close(ncsock->fd);            /* is apparently crucial; this is */
15   dup2(STDIN_FILENO, STDOUT_FILENO);    /* swiped directly out of "inetd". */
16   dup2(STDIN_FILENO, STDERR_FILENO);    /* also duplicate the stderr channel */
17 
18   /* change the label for the executed program */
19   if ((p = strrchr(opt_exec, '/')))
20     p++;            /* shorter argv[0] */
21   else
22     p = opt_exec;
23 
24   /* replace this process with the new one */
25 #ifndef USE_OLD_COMPAT
26   execl("/bin/sh", p, "-c", opt_exec, NULL);
27 #else
28   execl(opt_exec, p, NULL);
29 #endif
30   dup2(saved_stderr, STDERR_FILENO);
31   ncprint(NCPRINT_ERROR | NCPRINT_EXIT, _("Couldn't execute %s: %s"),
32       opt_exec, strerror(errno));
33 }               /* end of ncexec() */

Here, on lines 13-16 is how the “-e” parameter really works. dup2() accepts two file descriptors and after deallocating the second one (as if close() was called on it), the second one’s value is set to the first. So in this case on line 13, the child process’s stdin is being set to the file descriptor for the network socket netcat opened. This means that the child process will view any data received over the network will as input data and will act accordingly. Then on lines 15 and 16, the stdout and stderr descriptors are also set to the socket, which will cause any output the program has to be directed over the network. As far as line 14 goes, I’m not sure why the original socket file descriptor has to be closed at that exact point (and based on the comments, it seems like the netcat author wasn’t sure either).

The main point is this file descriptor swapping has essentially converted our specified program into a network service; all the input and output will be piped over the network, and at this point the child process can be executed. The child will replace the netcat process and will also inherit the newly set socket file descriptors. Note that on lines 30 and 31 there’s some error handling code that resets the original stderr for the netcat process and prints out an error message. This is because the code should actually never get to this point in execution due to the execl() call and if it does, there was an error executing the child.

I wrote this little python program to see if I understood things correctly:

#!/usr/bin/env python

import sys

inp = sys.stdin.read(5)
if inp == 'hello':
    sys.stdout.write('hi\n')
else:
    sys.stdout.write('bye\n')

It simply reads 5 bytes from stdin and prints ‘hi’ if those 5 bytes were ‘hello’ otherwise printing ‘bye’.

Using this program as the -e parameter results in this:

1
2
3
4
5
6
7
8
9
10
$ netcat -e /tmp/test.py -lp 8080 &
[1] 19021
$ echo asdfg | netcat 127.0.0.1 8080
bye
[1]+  Done                    netcat -e /tmp/blah.py -lp 8080
$ netcat -e /tmp/test.py -lp 8080 &
[1] 19024
$ echo hello | netcat 127.0.0.1 8080
hi
[1]+  Done                    netcat -e /tmp/blah.py -lp 8080

We can see the “server” launched in the background. The echo command sends data into netcat’s stdin, which is being sent over the network, handled by the python script, which sends back its response, which gets printed. Then we can see that the server exits since the netcat process has been replaced by the script, and the script has exited.

Comments