验证码外部控制(绕过)实例
TheKingOfDuck 渗透测试 10357浏览 · 2019-03-24 02:15

漏洞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。

1 条评论
某人
表情
可输入 255