漏洞CMS:ThinkCMF 1.X-2.X
0x01 前言
源于上周到广州参加培训时的实战经历。
同事给出了一个9000+的站点列表,这么多的站点要是一个一个看得话得看到猴年马月啊。于是随手写了一个脚本先去探测基础信息:
https://github.com/TheKingOfDuck/bcScan
先摸清楚网站是否可正常访问,以及中间件的一些基础信息。
查看返回的基础信息时有注意到多个thinkcmf字眼,比较眼熟,再有了本文。
0x02 过程
按照先知社区phpoop
师傅的文章某CMFX_2.2.3漏洞合集指示,访问README.MD
文件可获取版本信息。
基础信息thinkcmf 1.5
根据水泡泡
师傅的文章某cmf 1.6.0版本从sql注入到任意代码执行中提供的漏洞,1.6的漏洞利用在1.5身上应该是odbk的。悲剧的1.6的注入应用到1.5身上无任何回显。本地测试时通过MySQLMonitor监测发现语句确实拼接进去了。就是没有回显,盲注没有解决这个问题。1.6的任意代码执行在1.5确实可行,但是需要等待具有内容管理的权限用户登录触发这东西,这是一个被魔改了的站点。注册后台模块都被删除或是修改了的,无法自行触发。
仔细读文章时有主意到这么一条讯息,我面对的版本是1.5的,他的验证码获取url长这样:
http://127.0.0.1/index.php?g=Api&m=Checkcode&a=index&code_len=4&font_size=15&width=100&height=35&charset=1234567890
值得一提的是1.5的验证码是可以复用的。官方在1.6中修复了该问题。
主意观察url参数会发现charset
字段,验证码内容可以外部控制?实在证明,的确是这样。
将charset修改为2222
绕后访问:
这样一来就无论你则么刷新验证码的值都始终为2222了。
(此处另一个问题:验证码的尺寸可控,可调整对应值发起DDoS攻击。)
那么问题来了,在1.6中可否通过该缺陷解决上文截图中水泡泡师傅提到的验证码问题呢?1.6的获取验证码的url长这样:
http://127.0.0.1/1.6/index.php?g=Api&m=Checkcode&a=index&length=4&font_size=25&width=238&height=50
官方删除了charset
参数,自己加上去是否可行?
验证得出的确可行。
0x03 分析与利用
直接定位到生成验证码的文件:
1.6/application/Api/Controller/CheckcodeController.class.php
代码篇幅不长这里就删除非必要的注释后直接贴上来了:
<?php
/**
* 验证码处理
*/
namespace Api\Controller;
use Think\Controller;
class CheckcodeController extends Controller {
public function index() {
$length=4;
if (isset($_GET['length']) && intval($_GET['length'])){
$length = intval($_GET['length']);
}
//设置验证码字符库
$code_set="";
if(isset($_GET['charset'])){
$code_set= trim($_GET['charset']);
}
$use_noise=1;
if(isset($_GET['use_noise'])){
$use_noise= intval($_GET['use_noise']);
}
$use_curve=1;
if(isset($_GET['use_curve'])){
$use_curve= intval($_GET['use_curve']);
}
$font_size=25;
if (isset($_GET['font_size']) && intval($_GET['font_size'])){
$font_size = intval($_GET['font_size']);
}
$width=0;
if (isset($_GET['width']) && intval($_GET['width'])){
$width = intval($_GET['width']);
}
$height=0;
if (isset($_GET['height']) && intval($_GET['height'])){
$height = intval($_GET['height']);
}
$config = array(
'codeSet' => !empty($code_set)?$code_set:"2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY", // 验证码字符集合
'expire' => 1800, // 验证码过期时间(s)
'useImgBg' => false, // 使用背景图片
'fontSize' => !empty($font_size)?$font_size:25, // 验证码字体大小(px)
'useCurve' => $use_curve===0?false:true, // 是否画混淆曲线
'useNoise' => $use_noise===0?false:true, // 是否添加杂点
'imageH' => $height, // 验证码图片高度
'imageW' => $width, // 验证码图片宽度
'length' => !empty($length)?$length:4, // 验证码位数
'bg' => array(243, 251, 254), // 背景颜色
'reset' => true, // 验证成功后是否重置
);
$Verify = new \Think\Verify($config);
$Verify->entry();
}
}
emmmmm,这个代码逻辑相当简单,就是字节在参数中获取字符集了。1.6的前端代码中没加这个参数,为空就将字符集设为第50行中的那些了。
经过验证发现,该问题可以在1.x到2.x的版本中都存在。
如何利用?
以爆破为例,登录验证之前先携带着爆破用的cookie访问一遍加了charset
参数的验证码请求链接,完了再发送包含了固定验证码的登录验证包即可。
0x04 总结
到最后1.5的那个站点是如何日下的?后台登录验证模块被删了的,登录界面存在,但是发送请求后404(可以看到验证码),注册页面被删。前台登录模块没删但是访问后发现验证码被删,登录包发送过去提示验证码缺失,解决方案是利用0x03
中提到的先请求一遍加了固定charset
参数,再将固定的验证码加上去,就可以实现爆破。最终进去触发代码执行,实现Getshell。