onethink 登陆绕过

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /onethink/wwwroot/admin.php?s=/Public/login.html HTTP/1.1
Host: 192.168.1.105:9097
Content-Length: 89
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.1.105:9097
Referer: http://192.168.1.105:9097/onethink/wwwroot/admin.php?s=/Public/login.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: PHPSESSID=1a4ecfcc5a76854ffe525750db9ed20e; XDEBUG_SESSION=11437
Connection: close

username[]=xxxxx(payload)&password=&verify=njadc

onethink是基于tp3.2框架的cms,很早就知道了tp3.2的一些注入漏洞,但一直没自己亲手跟过,所以这次借这个onethink的漏洞对tp3.2一些经典的注入做一个基本了解。

首先确定漏洞点为后台口登陆时触发,本地搭建环境,因为我们知道大部分的thinkphp的框架都可以通过开启报错功能查看调用栈,我们稍微修改一下payload,让网页报错,可以看到调用栈如下:

upload successful

upload successful

前面为程序运行时需要加载的thinkphp的框架,我们注意点在后面,从Admin\Controller\PublicController->login(Array, ‘’, ‘njadc’)处跟进,在Application/Admin/Controller/PublicController.class.php中line32:

upload successful
跟进login方法,在/Application/User/Api/UserApi.class.php line41:
upload successful
根据上面框架打印的调用栈可知,login方法在Application/User/Model/UcenterMemberModel.class.php line128:
upload successful
前面根据登陆类型将$username即我们登陆时传入的用户名的值赋值给$map[‘username’],即$map我们可控,接下来我们注意下面,$user = $this->where($map)->find();。首先跟进where方法,在/ThinkPHP/Library/Think/Model.class.php line1766:
upload successful

前面都是根据传入的where参数和parse参数的具体情况(这里$parse为null)对$where变量进行数组键值对的赋值处理,在最后一个if循环中我们注意到,如果传入的$where为数组,将会直接无过滤的赋值给$this->options[‘where’],最后函数返回该值。接下来我们跟进find方法,在/ThinkPHP/Library/Think/Model.class.php line724:
upload successful

前面通过$this->getPK()获得主键记录,并且如果$pk参数为数组,即为复合主键的情况下,根据id获取主键记录,然后执行$options[‘where’]=$where的赋值操作。这里我们重点关注下面的分析表达式语句,跟进$this->_parseOptions方法

upload successful

在/ThinkPHP/Library/Think/Model.class.php line631:

upload successful

这里主要说明了根据表名(自动获取、手动指定)对数据表字段列表的获取,并且对数组查询条件进行字段类型检查(bigint、int、bool、double等类型)的操作,最后会进入到$this->_options_filter方法,跟进该私有方法:

upload successful

没有做任何操作的空函数,所以最开始传入的$where(即username[])的值可以毫无过滤的赋值给$options,最终_parseOptions方法返回没有经过任何过滤的$options变量。
返回find()方法,继续跟进数据库查询操作select()方法,在/ThinkPHP/Library/Think/Db.class.php line778:

upload successful

先通过$this->buildSelectSql方法获得要执行的sql语句,再查询缓存检测,然后通过$this->query函数执行sql语句,这里我们跟进buidSelectSql方法看下是怎么获取到数据库要执行的语句的。

upload successful

前面为根据页面计算limit以及sql执行语句创建缓存,缓存是由thinkphp自带的缓存框架实现的,这里我们不过多探讨,跟进$this->parseSql方法:

upload successful

这里做了一次替换操作,将$sql的值换成对应的options数组的值,因为我们触发点是登陆时输入的账号密码,按照sql语句应该是select * from xxx where username=’xxx’ and password=’xxx’,所以我们需要关注图上的$this->parsewhere方法,在line 412:

upload successful

这里对查询字段的条件情况进行了判断,如果$where是不是数组而是字符串那么直接赋值给$wherestr,但我们这里是登陆处,并且根据断点情况也能知道$options[‘where’]即$where为数组形式,即条件为where username=’xxx’ and password=’xxx’所以会进入else,接着会根据数组里是否含有and or xor的关键字进行具体的逻辑运算操作,如果没有,默认就会对多个条件进行and运算。然后进入foreach,如果不是特殊表达式的话走进一层正则判断,如果开头不是A-Z _ | & - . a-z 0-9 ( ) 这些字符的话,则会抛出错误,接下来按照逻辑走进$this->WhereItem方法:

upload successful

其中,在in运算和between运算中不存在^和$的正则匹配,当然比较运算也是直接正则匹配后,将$val数组拼接赋值给$wherestr。最后返回完整的where条件。到了这里我们可以肯定整个where条件赋值的过程中不存在任何过滤,即传入的username[]参数完全可控。
接下来,数据库解释执行的地方不再赘述,接下来回到Application/User/Model/UcenterMemberModel.class.php中,进入验证用户密码阶段:

upload successful

根据用户名的查询获取到密码即为$user[‘password’],然后将传入的password进行md5加密与获取到的密码进行比对,如果完全相等则登陆成功。我们先看看key是个什么东西。

upload successful

可以发现,key非常复杂,并且是/Application/User/Conf/config.php里面,该文件为UCenter客户端配置文件,熟悉的朋友可以知道这里加密的key针对不同的网站值是不同的随机的,我们也无法通过文件读取漏洞读到该key的值,所以这里我们跟进think_ucenter_md5():

upload successful
发现传入的密码即$password如果为空,则结果返回为空,这里我们可以通过之前确定的sql注入利用联合查询指定密码字段位(密码字段位数为3)为空,从而达到绕过登陆进入后台的操作。(in操作涉及到mysql弱类型转换的问题)

成功登陆后台:

upload successful

know it then enjoy it~

打赏
  • © 2020 sommous

老板,来杯卡布奇诺~

支付宝
微信