上周参与了 hxp CTF ,其中有两个 PHP Web 题目令人印象深刻,也产生了一种让我拍手称快的、对我来说算是新的 LFI 方法,这里就将本次比赛题目分析写一下。当然我不确定这个是不是新方法,若有错误,希望各位师傅们斧正,多多海涵

文章首发于跳跳堂:hxp CTF 2021 - A New Novel LFI

TL;DR

  • Nginx 在后端 Fastcgi 响应过大 或 请求正文 body 过大时会产生临时文件
  • 通过多重链接绕过 PHP LFI stat 限制完成 LFI

本文主要介绍 hxp CTF 2021 中的两种新的 LFI 方法,第一部分主要介绍第一种方法,主要分析 Nginx 部分源码;第二部分简略介绍第二种方法。

Includer’s revenge - Nginx Fastcgi Temp LFI

附件地址:https://2021.ctf.link/assets/files/includer’s%20revenge-25377e1ebb23d014.tar.xz

题目代码比较简单:

<?php 
($_GET['action'] ?? 'read' ) === 'read' ? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');

可以说是 onelinephp 了,当然如果光看这些代码,我们可以直接用 36c3 hxp CTF includer 的解法解掉,用 compress.zip://http:// 产生临时文件,包含即可,具体可以看看我之前写的 writeup :36c3 学习记录#inlcuder

当然这里既然标了 revenge 肯定说明有一些不同的地方,结合题目给我们的附件,我们可以发现相对上一次 includer 题目有了比较大区别,主要在 Dockerfile 里面:

RUN chown -R root:root /var/www && \
    find /var/www -type d -exec chmod 555 {} \; && \
    find /var/www -type f -exec chmod 444 {} \; && \
    chown -R root:root /tmp /var/tmp /var/lib/php/sessions && \
    chmod -R 000 /tmp /var/tmp /var/lib/php/sessions

出题人这里竟然狠心把 php tmp 目录以及一些临时目录都弄得不可写了,所以导致之前题目的产生临时文件的方法就失效了。

所以很明显,我们需要找到另一个产生临时文件,将其包含的方法。

How To Produce Tmp Files

由于之前我觉得 36c3 includer 那个题出的真是令我很赞叹,在某些比赛出题的时候,我也考虑过 php 是不是可以有其他产生临时文件的方法,所以自己也去看了一段时间 php 源码,其产生临时文件主要是通过 php_stream_fopen_tmpfile 这个函数,然而这个函数调用都没几处,所以之前我太菜了就没有挖到了,所以根据我之前的经验,在这里可能并不是 php 的原因。

所以我并没有过多纠结 php 的问题,在 Dockerfile 中我注意到出题人有一行可能类似于 Tip 的操作

RUN ! find / -writable -or -user $(id -un) -or -group $(id -Gn|sed -e 's/ / -or -group /g') 2> /dev/null | grep -Ev -m 1 '^(/dev/|/run/|/proc/|/sys/|/var/lock|/var/log/nginx/error.log|/var/log/nginx/access.log)

既然我们要找一个 www-data 用户可写的地方,我们可以参考这个命令把系统中所有的都找出来,看看有没有什么猫腻:

/dev/core
/dev/stderr
/dev/stdout
/dev/stdin
/dev/fd
/dev/ptmx
/dev/urandom
/dev/zero
/dev/tty
/dev/full
/dev/random
/dev/null
/dev/shm
/dev/mqueue
/dev/pts/1
/dev/pts/ptmx
/run/lock
/run/php
/run/php/php7.4-fpm.sock
/run/php/php7.4-fpm.pid
/proc/keys
/proc/kcore
/proc/timer_list
/proc/sched_debug
/var/lock
/var/lib/nginx/scgi
/var/lib/nginx/body
/var/lib/nginx/uwsgi
/var/lib/nginx/proxy
/var/lib/nginx/fastcgi
/var/log/nginx/access.log
/var/log/nginx/error.log

以上我略去了很多 /proc/xxxx ,所以挨个看下来,很明显,似乎后面 nginx 的可能就是我们要的答案,我们可以在网络上搜索一下相关目录用来干嘛的,最后发现 /var/lib/nginx/fastcgi 目录是 Nginx 的 http-fastcgi-temp-path ,看到 temp 这里就感觉很有意思了,意味着我们可能通过 Nginx 来产生一些文件,并且通过一些搜索我们知道这些临时文件格式是: /var/lib/nginx/fastcgi/x/y/0000000yx

那这临时文件用来干嘛呢?通过阅读 Nginx 文档 fastcgi_buffering 部分

Syntax:fastcgi_buffering on | off;
Default:fastcgi_buffering on;
Context:http, server, location

This directive appeared in version 1.5.6.

Enables or disables buffering of responses from the FastCGI server.

When buffering is enabled, nginx receives a response from the FastCGI server as soon as possible, saving it into the buffers set by the fastcgi_buffer_size and fastcgi_buffers directives. If the whole response does not fit into memory, a part of it can be saved to a temporary file on the disk. Writing to temporary files is controlled by the fastcgi_max_temp_file_size and fastcgi_temp_file_write_size directives.

When buffering is disabled, the response is passed to a client synchronously, immediately as it is received. nginx will not try to read the whole response from the FastCGI server. The maximum size of the data that nginx can receive from the server at a time is set by the fastcgi_buffer_size directive.

Buffering can also be enabled or disabled by passing “yes” or “no” in the “X-Accel-Buffering” response header field. This capability can be disabled using the fastcgi_ignore_headers directive.

我们大致可以知道当 Nginx 接收来自 FastCGI 的响应时,若大小超过限定值不适合以内存的形式来存储的时候,一部分就会以临时文件的方式保存到磁盘上。

再通过一些资料了解[1][2],这个阈值的大小大概在 32KB 左右,并且又根据 Risks of nginx fastcgi buffering or, how iTunes can mess with your Nextcloud server 文章我们可以知道 Nginx 确实可以在 /var/lib/nginx/fastcgi 下产生临时文件。

那么接下来我们只需要简单验证一下,并看一下临时文件内容是什么。我这里简单使用了 python 产生了一个有顺序内容的 tmp 文件:

with open("tmp") as file:
  for i in range(500000):
    file.write("%5s" % str(i))

尝试测试,发现虽然产生了文件夹,但是没有文件,于是我加上了 inotify 监控一下文件行动,并且可以使用 strace 进一步确认:

我们可以从 inotify 中看到,几乎 Nginx 是创建完文件就立即删除了,但是我们可以基本确认 Nginx 确实可以产生临时文件,只不过创建就被删除了导致我们无法判断文件内容到底是啥。

同时我们可以发现 Niginx 创建临时文件有所规律,为了检查文件内容,我们可以计算出下一次 Nginx 产生临时文件的位置,再对其上级目录使用 chattr +a 临时禁止临时文件删除,这样我们就可以看到文件内容了:

可以看到临时文件内容就是我们远程 vps 上放的 tmp 文件内容的一部分。

How Nginx Produce Tmp Files

接着问题来了:为什么 Nginx 创建文件就立即删除了?有没有窗口期?能不能使用竞争包含呢?

为了弄懂这些问题,便只能直接看 Nginx 源码了,于是直接参考一些 debug 教程弄一个 debug 环境起来即可。

Nginx 关于临时文件的地方并不多,不难找到 ngx_open_tempfile这个函数:

ngx_fd_t
ngx_open_tempfile(u_char *name, ngx_uint_t persistent, ngx_uint_t access)
{
    ngx_fd_t  fd;

    fd = open((const char *) name, O_CREAT|O_EXCL|O_RDWR,
              access ? access : 0600);

    if (fd != -1 && !persistent) {
        (void) unlink((const char *) name);
    }

    return fd;
}

Fastcgi 产生临时文件时候的调用栈:

ngx_create_temp_file(ngx_file_t * file, ngx_path_t * path, ngx_pool_t * pool, ngx_uint_t persistent, ngx_uint_t clean, ngx_uint_t access) (/home/zeddy/Desktop/nginx-1.18.0/src/core/ngx_file.c:143)
ngx_write_chain_to_temp_file(ngx_temp_file_t * tf, ngx_chain_t * chain) (/home/zeddy/Desktop/nginx-1.18.0/src/core/ngx_file.c:114)
ngx_event_pipe_write_chain_to_temp_file(ngx_event_pipe_t * p) (/home/zeddy/Desktop/nginx-1.18.0/src/event/ngx_event_pipe.c:843)
ngx_event_pipe_read_upstream(ngx_event_pipe_t * p) (/home/zeddy/Desktop/nginx-1.18.0/src/event/ngx_event_pipe.c:277)
ngx_event_pipe(ngx_event_pipe_t * p, ngx_int_t do_write) (/home/zeddy/Desktop/nginx-1.18.0/src/event/ngx_event_pipe.c:49)
ngx_http_upstream_process_upstream(ngx_http_request_t * r, ngx_http_upstream_t * u) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_upstream.c:3944)
ngx_http_upstream_handler(ngx_event_t * ev) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_upstream.c:1286)
ngx_epoll_process_events(ngx_cycle_t * cycle, ngx_msec_t timer, ngx_uint_t flags) (/home/zeddy/Desktop/nginx-1.18.0/src/event/modules/ngx_epoll_module.c:901)
ngx_process_events_and_timers(ngx_cycle_t * cycle) (/home/zeddy/Desktop/nginx-1.18.0/src/event/ngx_event.c:247)
ngx_single_process_cycle(ngx_cycle_t * cycle) (/home/zeddy/Desktop/nginx-1.18.0/src/os/unix/ngx_process_cycle.c:310)
main(int argc, char * const * argv) (/home/zeddy/Desktop/nginx-1.18.0/src/core/nginx.c:379)

我们从中可以知道如果要让 Nginx 保存临时文件,得满足一个 if 条件,然而我们仔细看该条件,由于是条件,我们可以知道得同时满足才能进入该 if 条件,我们分析一下该 if 条件

  • fd != -1 : fdopen 函数的返回值,我们可以知道只有当 open 函数打开失败的时候才会返回 -1 ,也就是该临时文件不存在的情况下,换句话说就是只要临时文件被 open 函数成功打开,这个条件就是成立的
  • persistent: 该条件从函数上下文我们看不出来有什么关系,需要更进一步分析,通过分析代码,我们可以发现该变量主要在以下三个地方可能被赋值为 1 :
    • 一个地方是 src/http/ngx_http_request_body.c#456 处:tf->persistent = r->request_body_in_persistent_file;
    • 另一个地方是 src/http/ngx_http_upstream.c#4087 处: tf->persistent = 1;
    • 还有一个地方是 src/http/ngx_http_upstream.c#3144 处: p->temp_file->persistent = 1;

我们分别对这几个地方进行详细分析及跟进。

client_body_in_file_only

第一个地方是 src/http/ngx_http_request_body.c#456 处:tf->persistent = r->request_body_in_persistent_file;,继续跟进 request_body_in_persistent_file 成员变量,找到其赋值的地方为 src/http/ngx_http_core_module.c#1315 中的 ngx_http_update_location_config 函数当中。

if (clcf->client_body_in_file_only) {
    r->request_body_in_file_only = 1;
    r->request_body_in_persistent_file = 1;
    r->request_body_in_clean_file =
        clcf->client_body_in_file_only == NGX_HTTP_REQUEST_BODY_FILE_CLEAN;
    r->request_body_file_log_level = NGX_LOG_NOTICE;
}

此处我们可以根据上下文判断出该处主要是用于判断是否开启 client_body_in_file_only 选项,根据文档我们可以知道:

Syntax:client_body_in_file_only on | clean | off;
Default:client_body_in_file_only off;
Context:http, server, location

Determines whether nginx should save the entire client request body into a file. This directive can be used during debugging, or when using the $request_body_file variable, or the $r->request_body_file method of the module ngx_http_perl_module.

When set to the value on, temporary files are not removed after request processing.

The value clean will cause the temporary files left after request processing to be removed.

在该选项开启后,Nginx 对于请求的 body 内容会以临时文件的形式存储起来,但是默认为 off ,题目并没有开启,所以这里不用考虑。

fastcgi_store

另一个地方是 src/http/ngx_http_upstream.c#4087 处:

static void
ngx_http_upstream_store(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
    //...
    tf = u->pipe->temp_file;

    if (tf->file.fd == NGX_INVALID_FILE) {

        /* create file for empty 200 response */

        tf = ngx_pcalloc(r->pool, sizeof(ngx_temp_file_t));
        if (tf == NULL) {
            return;
        }

        tf->file.fd = NGX_INVALID_FILE;
        tf->file.log = r->connection->log;
        tf->path = u->conf->temp_path;
        tf->pool = r->pool;
        tf->persistent = 1;

        if (ngx_create_temp_file(&tf->file, tf->path, tf->pool,
                                 tf->persistent, tf->clean, tf->access)
            != NGX_OK)
        {
            return;
        }

        u->pipe->temp_file = tf;
    }
    //...
}

往上找该函数调用:

if (u->peer.connection) {
    if (u->store) {
        if (p->upstream_eof || p->upstream_done) {
            tf = p->temp_file;
            if (u->headers_in.status_n == NGX_HTTP_OK
                && (p->upstream_done || p->length == -1)
                && (u->headers_in.content_length_n == -1
                    || u->headers_in.content_length_n == tf->offset))
            {
                ngx_http_upstream_store(r, u);
            }
        }
    }
//...
}

得知此处有几个条件,可能都相对比较苛刻,于是我先看 u->store 成员变量的赋值,找到该成员变量主要是在 src/http/ngx_http_upstream.c# 610 处的 ngx_http_upstream_init_request 函数中得到赋值:

static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{
    //...
    u = r->upstream;
#if (NGX_HTTP_CACHE)
    if (u->conf->cache) {
        ngx_int_t  rc;
        rc = ngx_http_upstream_cache(r, u);
        //...
    }
#endif
    u->store = u->conf->store;
    //...
}

我们可以根据此处上下文,并且查阅一些相关源码资料知道此处 u->conf->store 来自解析配置 fastcgi_store

Syntax:fastcgi_store on | off | string;
Default:fastcgi_store off;
Context:http, server, location

Enables saving of files to a disk. The on parameter saves files with paths corresponding to the directives alias or root.

默认为关闭状态,当我们将这个选项开启为 on 的时候,可以发现我们产生的临时文件最后才消失。因为这个地方需要手动开启,所以在默认情况下我们也很难利用。

cache

还有一个地方是 src/http/ngx_http_upstream.c#3144 处:

static void
ngx_http_upstream_send_response(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
    //...
    p->cacheable = u->cacheable || u->store;
    //...
    if (p->cacheable) {
        p->temp_file->persistent = 1;
    }
    //...
}

很明显,这个临时文件是作缓存使用的,u->store 上面我们知道了是需要通过配置设置,所以我们接下来,但是我们仍然可以跟一下条件中的 p->cacheable 成员变量,其中只有在 src/http/ngx_http_upstream.c#860 处的 ngx_http_upstream_cache 函数被设置成了 1 ,但是该函数需要开启宏 NGX_HTTP_CACHE ,我们可以在 auto/modules#99 处找到该宏定义

if [ $HTTP_CACHE = YES ]; then
  have=NGX_HTTP_CACHE . auto/have
  HTTP_SRCS="$HTTP_SRCS $HTTP_FILE_CACHE_SRCS"
fi

接着可以在 auto/options 找到 $HTTP_CACHE 的定义默认为 YES ,只有当编译增加选项 --without-http-cache 才会将该宏定义为 FALSE ,也就是说如果正常开启, Nginx 是默认开启这个宏的。

但是该函数还会受到 src/http/ngx_http_upstream.c#569 处的限制 u->conf->cache ,并且通过查看一些文档[3][4] ,发现知道这里的 config->cache 也是与 proxy_cache 配置有关的,查阅文档知道 proxy_cache 配置选项默认为 off ,所以这里我们也不考虑。

Tmp Files After Deleted

由于 Nginx 在 ngx_open_tempfile 函数中,创建临时文件后又立马删掉了临时文件,并且从以上源码审计来看,没有很好的方式让 persistent 变量为 1 ,所以在不能修改默认配置的情况下,直接让临时文件保存下来是基本不可能的。

那我们有没有一个时间窗去包含临时文件呢?由于这创建、删除函数间隔非常短,即使有能让 Nginx Crash 的方法,也很难把握这个时间点,基本上也是没有一个时间窗去直接包含的。

但是我在审计的同时,也产生了一个问题:既然 Nginx 将临时文件用于存储 Fastcgi 响应的临时存储,但是为什么创建之后就删除了?为什么删除之后还持续向里面写内容?难不成删除以后的读写操作还仍然有效???

我觉得这是从开发角度思考来说,仅通过审计这些代码无法解释以上问题,但是这里如果熟悉 Linux 的同学就能意识到,其实以上这些问题可能都不是问题。

On Linux, the set of file descriptors open in a process can be accessed under the path /proc/PID/fd/, where PID is the process identifier.

众所周知 ( 我应该是全世界最后一个知道的人了吧 ),如果打开一个进程打开了某个文件,某个文件就会出现在 /proc/PID/fd/ 目录下,但是如果这个文件在没有被关闭的情况下就被删除了呢?

我们大概可以用 c 简单复刻一个大概的 demo ,使用如下代码模拟 Nginx 对于临时文件处理的行为,但是最后不关闭文件句柄,使用 sleep 模拟进程挂起的状态:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <unistd.h>

int main() {
    puts("[+] test for open/unlink/write [+]\n");
    int fd = open("test.txt", O_CREAT|O_EXCL|O_RDWR, 0600);
    printf("open file with fd %d,try unlink\n",fd);

    unlink("test.txt");
    printf("unlink file, try write content\n");
    if(write(fd, "<?php phpinfo();?>", 19) != 19)
    {
        printf("write file error!\n");
    }

    char buffer[0x10] = {0};
    lseek(fd, 0,SEEK_SET);
    int size = read(fd, buffer , 19);
    printf("read size is %d\n",size);
    printf("read buffer is %s\n",buffer);

    while(1) {
        sleep(10);
    }
    // close(fd);
    return 0;
}

编译运行以上代码,我们可以在对应的 /proc/pid/fd 下找到我们删除的文件 ,可以看到虽然显示是被删除了,但是我们依然可以读取到文件内容,所以我们是不是可以直接用 php 进行文件包含呢?

Bypass PHP File Stat

虽然这并不是第一次出现过这个技巧了,但是可能比赛的时候大多数人都没想起来,对于 include 函数,在进行包含的时候,会使用 php_sys_lstat 函数判断路径,这里已经有师傅整理过很详细的文章了:php源码分析 require_once 绕过不能重复包含文件的限制

php_sys_lstat()实际上就是linux的lstat(),这个函数是用来获取一些文件相关的信息,成功执行时,返回0。失败返回-1,并且会设置errno,因为之前符号链接过多,所以errno就都是ELOOP,符号链接的循环数量真正取决于SYMLOOP_MAX,这是个runtime-value,它的值不能小于_POSIX_SYMLOOP_MAX

所以虽然直接包含会显示文件不存在,但是这里依然适用于使用多层符号链接绕过的场景,进而包含执行 php 代码,并且根据一开始我们实验的图看到,其实 Nginx 对于临时文件句柄的关闭往往在最后才进行关闭,所以这个过程中有足够的时间让我们去进行竞争包含。

Chain Together

所以到这里我们可以有了一个大概的想法:竞争包含 proc 目录下的临时文件。但是最后一个问题就是,既然我们要去包含 Nginx 进程下的文件,我们就需要知道对应的 pid 以及 fd 下具体的文件名,怎么才能获取到这些信息呢?

这时我们就需要用到文件读取进行获取 proc 目录下的其他文件了,这里我们只需要本地搭个 Nginx 进程并启动,对比其进程的 proc 目录文件与其他进程文件区别就可以了。

而进程间比较容易区别的就是通过 /proc/cmdline ,如果是 Nginx Worker 进程,我们可以读取到文件内容为 nginx: worker process 即可找到 Nginx Worker 进程;因为 Master 进程不处理请求,所以我们没必要找 Nginx Master 进程。

当然,Nginx 会有很多 Worker 进程,但是一般来说 Worker 数量不会超过 cpu 核心数量,我们可以通过 /proc/cpuinfo 中的 processor 个数得到 cpu 数量,我们可以对比找到的 Nginx Worker Pid 数量以及 CPU 数量来校验我们大概找的对不对。

那怎么确定用哪一个 PID 呢?以及 fd 怎么办呢?由于 Nginx 的调度策略我们确实没有办法确定具体哪一个 worker 分配了任务,但是一般来说是 8 个 worker ,实际本地测试 fd 序号一般不超过 70 ,即使爆破也只是 8*70 ,能在常数时间内得到解答。

总结起来整个过程就是:

  • 让后端 php 请求一个过大的文件
  • Fastcgi 返回响应包过大,导致 Nginx 需要产生临时文件进行缓存
  • 虽然 Nginx 删除了/var/lib/nginx/fastcgi下的临时文件,但是在 /proc/pid/fd/ 下我们可以找到被删除的文件
  • 遍历 pid 以及 fd ,使用多重链接绕过 PHP 包含策略完成 LFI

这里需要注意的是把握好自己生成的 tmp 文件大小以及 curl 命令,可以生成后自己 debug 看一下 fd 目录下的文件存活多久。

Counter && Nginx Request Body Temp LFI

Counter 也是一道 PHP 的题目,也是一道与 LFI 有关的题目,但是在这篇文章中,我并不想写这道题的预期,我们继续来写这个题的非预期。

还记得我们上文分析的 client_body_in_file_only 选项看到的文档说明吗?

Determines whether nginx should save the entire client request body into a file

虽然这个也需要配置,但是结合我们上文分析的结果,以及之前我们也看到 Nginx 有个相关的目录 /var/lib/nginx/body ,难不成 Nginx 对于过大的 Request Body 也会产生临时文件?

这里只要试一试就可以了,就不再过多分析了,基本与 Fastcgi 产生临时文件一致,创建临时文件的时候的调用栈:

ngx_create_temp_file(ngx_file_t * file, ngx_path_t * path, ngx_pool_t * pool, ngx_uint_t persistent, ngx_uint_t clean, ngx_uint_t access) (/home/zeddy/Desktop/nginx-1.18.0/src/core/ngx_file.c:143)
ngx_write_chain_to_temp_file(ngx_temp_file_t * tf, ngx_chain_t * chain) (/home/zeddy/Desktop/nginx-1.18.0/src/core/ngx_file.c:114)
ngx_http_write_request_body(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request_body.c:483)
ngx_http_request_body_save_filter(ngx_http_request_t * r, ngx_chain_t * in) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request_body.c:1132)
ngx_http_request_body_length_filter(ngx_chain_t * in, ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request_body.c:921)
ngx_http_request_body_filter(ngx_http_request_t * r, ngx_chain_t * in) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request_body.c:855)
ngx_http_do_read_client_request_body(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request_body.c:292)
ngx_http_read_client_request_body(ngx_http_request_t * r, ngx_http_client_body_handler_pt post_handler) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request_body.c:185)
ngx_http_fastcgi_handler(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/modules/ngx_http_fastcgi_module.c:748)
ngx_http_core_content_phase(ngx_http_request_t * r, ngx_http_phase_handler_t * ph) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:1247)
ngx_http_core_run_phases(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:868)
ngx_http_handler(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:851)
ngx_http_internal_redirect(ngx_http_request_t * r, ngx_str_t * uri, ngx_str_t * args) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:2530)
ngx_http_index_handler(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/modules/ngx_http_index_module.c:277)
ngx_http_core_content_phase(ngx_http_request_t * r, ngx_http_phase_handler_t * ph) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:1254)
ngx_http_core_run_phases(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:868)
ngx_http_handler(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_core_module.c:851)
ngx_http_process_request(ngx_http_request_t * r) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request.c:2060)
ngx_http_process_request_headers(ngx_event_t * rev) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request.c:1480)
ngx_http_process_request_line(ngx_event_t * rev) (/home/zeddy/Desktop/nginx-1.18.0/src/http/ngx_http_request.c:1151)

这里简单只用了一句话 include 作为测试:

我们可以把发送的报文 Content-Length 头部增加一定数额,并且在发送完数据的时候 sleep 避免 Nginx 过早关闭,可以看到我们可以直接包含了临时文件执行了 php 代码,这个方法相对 Nginx Fastcgi 的方法来说更实用。

这里需要注意的是把握好不要过早关闭 socket ,会导致 Nginx 过早关闭文件句柄导致我们无法竞争到。

Conclusion

由于我比较菜,比赛的时候只分析了部分 Nginx 源码,通过各类文章深入理解了一下 Nginx 实现原理,一直卡在如何让临时文件保存的问题上,所以最后也没有做出来。

整体做(坐)下来,我感觉整个题很符合我的味口,不仅因为之前自己尝试挖临时文件的一些 Tricks 最后没有挖到,导致看到这个题的时候就会感觉很兴奋(尽然有人能挖出来了!我学爆!),尽管自己比赛没做出来;而且也因为通过两天坐牢阅读 Nginx 源码、调试 Nginx ,我觉得其中过程的收获与最后知道怎么结题、这个比较通用的新颖的 LFI 技巧是相等的。

当然,这里只是根据题目按图索骥,利用 Nginx 特性完成 LFI 的利用,其他 Server 也可能存在类似的利用特点,但是说到底终究还是个 LFI ,至于其他 Server 的利用就等待师傅们继续深入挖掘了。

另外,赛后跟几个队以及作者交流了一下,作者对于 Fastcgi Tmp 的做法表示是预期做法,但是 Body 做法确实非预期了;@Super Guesser 队伍使用了 Body 非预期做法解掉了 Counter ;@pasten ,这场比赛的冠军队伍,曾试图竞争 Fastcgi Tmp 文件,导致打了很多流量。

23:23 <0xbb> yes pasten did that for sure
23:23 <zeddyu> i think it is impossible
23:24 <zeddyu> lol
23:24 <zeddyu> how
23:24 <0xbb> nope you can do it with a lot of force
23:24 <0xbb> 16 x core Germany
23:24 <zeddyu> gods
23:24 <0xbb> we saw peek 1.7 gigabits I think :D
23:24 <0xbb> but they were gentle :D

赛后官方放出了流量统计图:

可以看到 includer’s revenge 这个题流量交互确实很频繁 2333

另外的另外,不得不佩服作者的出题技巧,虽然整体看起来可能并不是特别难,但是我觉得国内能与作者抗衡的也只有那位某队副队、曾在某个比赛拿下 250 万奖金的成功人士了吧(

另外的另外的另外,由于 Counter 题目的预期与本文介绍的 LFI 技巧没太大关系, 所以不写在本文,如果有同学感兴趣,或者也想了解一些有趣的 Web CTF 题目的解题方法的,可以尝试了解一下我目前运营的星球:

我正在「Funny Web CTF」和朋友们讨论有趣的话题,你⼀起来吧?https://t.zsxq.com/7y7iAuf

References

  1. Nginx FastCGI response buffer sizes
  2. Nginx FastCGI response buffer sizes
  3. Nginx 中 upstream 机制的实现
  4. Nginx Proxy Cache分析