ThinkPHP是中国顶想信息科技公司的一套基于PHP的、开源的、轻量级Web应用程序开发框架,在国内被广泛应用。

ThinkPHP 6.0.0-6.0.1中存在一个任意文件操作漏洞。由于对SessionId的处理逻辑存在错误,导致攻击者在目标环境启用session的条件下,可以通过发送特定构造的Session Cookie值,实现任意文件创建和删除操作,在特定情况下还可以实现getshell。

环境搭建

环境依赖

  • PHP:ThinkPHP V6的运行环境要求PHP7.1+版本
  • Composer:ThinkPHP V6版本开始仅支持Composer安装及更新

安装Composer

  • Linux/Unix/macOS 环境

官方提供了快速安装脚本,可以执行如下命令:

1
2
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php

命令执行完成后会生成composer.phar文件,将其移动到/usr/local/bin下,就可以对composer进行全局调用:

1
sudo mv composer.phar /usr/local/bin/composer

验证composer安装情况:

1
composer --version
  • Windows环境

官方提供了EXE安装程序https://getcomposer.org/Composer-Setup.exe,安装程序会自动安装最新版本的Composer并设置PATH环境变量,安装完毕后即可在任意路径下以命令行方式调用Composer。

安装ThinkPHP 6.0.0

  • 用Composer安装thinkphp

    1
    composer create-project topthink/think tp60     #tp60为自定义名称
  • 将thinkphp版本切换为受漏洞影响的6.0.0版本

    修改tp/composer.json文件,将”topthink/framework”: “^6.0.0”修改为6.0.0,如下图:

    修改版本号

    执行命令composer update 将thinkphp代码更新到6.0.0版本。

  • 启动thinkphp:

    1
    ./think run --host 0.0.0.0 --port 8080

漏洞分析

代码分析

根据漏洞通告内容和时间节点及版本,遍历ThinkPHP源代码commit记录,将漏洞修复代码定位到commit 1bbe75019ce6c8e0101a6ef73706217e406439f2

修复代码

由上述代码看出,针对漏洞的修复主要是对sessionId的值添加了限制,要求id中的所有字符必须为字母或数字。结合漏洞描述,推测漏洞成因可能是sessionId中的特殊字符导致session存储时产生了目录遍历等情况。因此重点关注session的存储实现。

首先,通过代码看到session name为PHPSESSID:

sessioname

查看实现session存储的save()方法代码如下:

save session

save()方法中,当$data变量不为空时,将$data内容序列化后调用了$this->handler->write()函数;当$data为空时,调用了​$this->handler->delete()函数。跟踪write()delete()函数,跟踪到vendor/topthink/framework/src/think/session/driver/File.php中:

write

delete

可以看到在write()delete()函数中,都调用getFileName()函数,根据sessionId获取文件名,write()函数紧接着根据参数配置判断是否对数据进行gz压缩,最后调用writeFile()将数据写入指定路径文件。而delete()函数直接调用unlink()函数删除文件。继续跟踪getFileName()函数,发现其实现内容就是以sessionId作为文件名,拼接上配置中的存储路径,形成写入文件的路径:

getFileName

writeFile()函数,则直接调用了file_put_contents()函数执行写文件操作:

writeFile

从上述调用链条来看,当存储session文件时,写入文件名由cookie中PHPSESSID的值控制,而代码中对PHPSESSID值的校验,只判断其满足长度为32的字符串即可。因此,可以使用../等字符,构造长度为32的文件名,实现目录穿越,当$data不为空时,可以实现任意文件创建/写入,当$data为空时,可以实现任意文件删除。

从代码来看,$data的值默认为空,可以通过setData()set()函数进行赋值:

setdata

setdataset

在框架代码中进行搜索,发现原始代码中并没有对session进行赋值的操作。因此,默认环境下,只能利用此漏洞进行文件删除操作。如果要想实现任意文件创建写入,需要配合实际业务开发代码中有session赋值操作才能实现。

利用复现

  • 启用session

    ThinkPHP 6.0默认不启用session,如果想要触发此漏洞,需要首先启用session。修改app/middleware.php,将\think\middleware\SessionInit::class前的注释去掉即可。启用session后,session默认存储路径为runtime/session

  • 文件删除

    如上述分析所说,文件删除可以在默认配置环境下触发,只需要在32位长度的PHPSESSID值中,利用../等特殊字符,将目标文件路径从runtime/session穿越到指定目录下即可。利用效果如下:

    文件删除

  • 文件上传/GetShell

    触发文件上传/GetShell,需要ThinkPHP业务开发代码中,有session赋值操作。为了复现漏洞,这里简单在app/controller/Index.php中添加一个Session:set()操作,然后再启动ThinkPHP服务:

    setsession

    构造请求如下:

    1
    2
    3
    4
    GET /?username=%3C?php%20phpinfo()%20?%3E HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
    Cookie: PHPSESSID=../../../../public/sayers123.php;

    提交请求后,即可看到ThinkPHP路径下public路径中被创建了sayers123.php文件,其内容如下:

    1
    a:1:{s:4:"user";s:18:"<?php phpinfo() ?>";}

    访问http://127.0.0.1:8000/sayers123.php,可以看到phpinfo()被执行:

    result

同理,将phpinfo()替换为一句话木马,即可实现GetShell。