宝塔面板是一款使用方便、功能强大且终身免费的服务器管理软件,支持Linux与Windows系统。支持一键配置:LAMP/LNMP、网站、数据库、FTP、SSL,让用户可以通过Web端轻松管理服务器。

宝塔Linux面板7.4.2/Windows面板6.8版本中引入了一个未授权访问漏洞。攻击者可以通过面板开放的特定端口和url,无需认证直接访问到面板的phpmyadmin数据库管理界面,进而获得数据库的全部权限。

漏洞复现

漏洞影响版本

  • 宝塔Linux面板7.4.2
  • 宝塔Windows面板6.8

受影响版本离线包

LinuxPanel-7.4.2.zip

漏洞环境

由于宝塔官方只提供了在线安装一种方式,无法安装历史版本。可以考虑安装最新版之后,再用7.4.2版本的离线包进行替换,但是这种方法比较麻烦,而且可能遇到各种报错和问题。在此提供一个存在漏洞的宝塔7.4.2的docker镜像:

1
docker pull sayers3/baota_unauthorized

漏洞复现

漏洞分析

代码分析

根据漏洞通告内容和时间节点及版本,遍历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。