前言
2020年1月10日,ThinkPHP团队发布一个补丁更新,修复了一处由不安全的SessionId导致的任意文件操作漏洞。该漏洞允许攻击者在目标环境启用session的条件下创建任意文件以及删除任意文件,在特定情况下还可以getshell。
根据tp官方的提交记录来看,是对setId函数(\vendor\topthink\framework\src\think\session\Store.php)做了修改:
环境配置
首先该漏洞只存在于think6.0.0-6.0.1,因此如果版本太高要先进行降级操作。由于我的版本是6.0.2,与降级后的区别只是setId函数的不同,所以为了方便就直接在源码修改了。
public function setId($id = null): void
{
//$this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}
另外,thinkphp6默认是没有开启session功能的,我们需要在app\middleware.php文件中,取消session中间件的注释,设置为如图:
在app\controller\Index.php中:
-
引入命名空间:
use think\facade\Session;
-
index方法修改代码为:
public function index()
{
Session::set('name','<?php phpinfo();?>');
return 1;
}
漏洞验证
抓包修改cookie为PHPSESSID=aaaabbbbccccddddeeeeffff1234.php
这里PHPSESSID值必须为32位的php文件名
改包之后发送,页面回显的cookie同样为PHPSESSID=aaaabbbbccccddddeeeeffff1234.php
即代表将内容为<?php phpinfo();?>
的代码成功写入文件名为sess_aaaabbbbccccddddeeeeffff1234.php
的文件。
接着访问默认生成session文件的目录\runtime\session,可以看到的确写入shell成功:
这里序列化形式是php存储session信息的默认方法,由于我们在index界面对session赋值Session::set('name','<?php phpinfo();?>');
,所以该序列化形式就是对数组session['name']='<?php phpinfo();?>'
的序列化结果。由于这里以php文件格式解析了,所以会执行代码。
漏洞分析
回溯写入Session
时的$filename
(vendor/topthink/framework/src/think/session/driver/File.php):
这里跟进writeFile函数可以看到是写入文件的操作:
可以得知文件名filename
对应是参数sessID
经过getFileName
函数得到的,跟进:
也即在sessID
的基础上添加了sess_
前缀。
接着再看看sessID
是怎么来的,且write函数是在哪里被调用(vendor/topthink/framework/src/think/session/Store.php):
跟进getID:
直接返回id,再看看id是如何被设置的:
根据源码可知$id
从cookie中的PHPSESSID
变量中获取的:
从上面的分析就可以知道,session的id安全问题就是由于setID函数,当id的长度为32位时,直接就取对应的id作为值,而没有像版本修复后那样进行md5操作。因此当我们控制cookie的PHPSESSID
为32位时,就会对session的内容以数组形式先进行序列化处理,然后执行write
函数,也即对PHPSESSID
进行前缀添加,然后最终被当做写入文件的文件名。
刚刚关注的是session文件名的生成,而这里的文件内容data
对应就是session的内容,如果后端代码中存在可以控制session内容的操作,如Session::set('name',$_POST['c']);
,就可以写入shell。
另外,原文中提到的可以删除任意文件,回溯到对delete函数的调用:
可以得知,需要$this->data
为空,也就是session设置的变量键值等于空,也即存在如Session::set('',$_POST['c']);
这样的代码,才会进行删除操作。