CVE-2017-14322 登录认证绕过分析
首先这并不是一篇翻译文章,是看了原文之后我对这个漏洞的一点分析和理解。
https://security.infoteam.ch/en/blog/posts/narrative-of-an-incident-response-from-compromise-to-the-publication-of-the-weakness.html
Interspire Email Marketer 是一款商业软件,在6.1.0之后的版本由于错误使用逻辑运算符和忽视弱类型特点产生了安全问题,并在6.1.6做了修复,但中间竟然相隔了5年之久,最坑的是好像更新版本还需要支付额外的费用~
作者是在对客户进行应急响应时发现了这个0day,其应急过程和思路很值得学习和思考。
先是客户发现某个账户发邮件异常,作者通过排查Web日志定位问题,注意到蓝色框是referer字段,表示这条访问记录是从Google过来的,非常像是 Google Hacking 手法,
这个IP使用GET访问了管理后台页面大小是5197,紧接着又一个GET访问这时响应变成了302并且页面大小变成了33478,这表明已经登录成功进入后台了,
非常奇怪,因为并没有通过表单验证帐密,仅使用两个GET是不足以完成正常的登录认证流程。
这时应急就要考虑可能是身份凭证泄漏或者认证绕过等漏洞,因为种种迹象表明肯定和cookie有关系,但是使用Google Hacking这种撒网式攻击并不符合XSS之类的攻击流程,
虽然我也去看了一下cookie也没有加HttpOnly。
既然判断前者可能性较低那就着重排查一下是不是有认证绕过之类的漏洞吧
作者也是很有经验,首先就去看了处理cookie认证那部分代码,有幸我在网上找到了6.1.2版本的源代码,可以一窥究竟。
在./admin/com/init.php:484~509行,这是出现漏洞的代码,有两个问题
1、使用 == 进行判断,会受PHP弱类型特性影响,进行强转
2、整套源码中多处使用 ! 写法,猜测这里开发者可能错误理解了 ! 的作用,原意可能是对比 $tempUser->settings['LoginCheck'] == $tempCookie['rand'] ,如果相等则为真,然后用 ! 取反,这样就不会进入到这个 if 判断里,但是用法错误,因为 ! 的优先级比 == 要高
$tempCookie = IEM::requestGetCookie('IEM_CookieLogin', array()); ··· 略 ··· $tempUser = new User_API(); $tempUser->Load(intval($tempCookie['user'])); ··· 略 ··· if (!$tempUser->settings['LoginCheck'] == $tempCookie['rand']) { break; }
下面这个测试就是很好的证明
首先 '1' === 1,必然是False,因为类型不同 然后 !'1'===1,结果也是False,可见并不是先进行'1'===1的判断再取反
先看一下 $tempCookie 来源是 IEM::requestGetCookie('IEM_CookieLogin', array())
requestGetCookie函数在./admin/com/lib/IEM.class.php:242~249行,是 IEM_CookieLogin base64解码后在反序列化
public static function requestGetCookie($cookieName, $devaultValue = '', $callback = null) { $value = $devaultValue; if (isset($_COOKIE) && array_key_exists($cookieName, $_COOKIE)) { $value = @unserialize(base64_decode($_COOKIE[$cookieName])); } return self::_requestProcess($value, $callback); }
在./admin/functions/login.php:200~207行,调用requestSetCookie()设置两个cookie
$_COOKIE['IEM_CookieLogin']的值是 usercookie_info 传参到 requestSetCookie 函数进行序列化然后base64编码。
# ./admin/com/lib/IEM.class.php:250~261 行 public static function requestSetCookie($cookieName, $cookieValue, $expiry = 0) { $value = @serialize($cookieValue); if ($value === false) { return false; } $expiry = intval(abs($expiry)); if ($expiry != 0) { $expiry += time(); } return @setcookie($cookieName, base64_encode($value), $expiry, '/'); } # ./admin/functions/login.php:200~207 行 if ($rememberdetails) { if (!$user) { $user = IEM::userGetCurrent(); } if (!$rand_check) { $rand_check = uniqid(true); } $usercookie_info = array('user' => $user->userid, 'time' => time(), 'rand' => $rand_check, 'takemeto' => $redirect); IEM::requestSetCookie('IEM_CookieLogin', $usercookie_info, $oneyear); $usercookie_info = array('takemeto' => $redirect); IEM::requestSetCookie('IEM_LoginPreference', $usercookie_info, $oneyear); }
既然知道了cookie的算法,我们就可以控制 IEM_CookieLogin 的值
再来看一下 $tempUser->settings['LoginCheck'] 是从哪里过来的,跟进 Load 函数,发现是从数据库里取出的数据
# ./admin/com/init.php:497 行 $tempUser->Load(intval($tempCookie['user'])); # ./admin/functions/api/user.php:541~657 行 function Load($userid=0, $load_permissions=true) { $userid = intval($userid); if ($userid <= 0) { return false; } $query = "SELECT * FROM [|PREFIX|]users WHERE userid={$userid}"; $result = $this->Db->Query($query); if (!$result) { return false; } ··· 略 ··· if ($user['settings'] != '') { $this->settings = unserialize($user['settings']); } if (is_null($this->segmentadmintype)) { $this->segmentadmintype = $this->AdminType(); } return true; }
还记得这些代码吗?
$tempUser->settings['LoginCheck'] 值为 15aee9a0b0cd9b ,取反后为 False,然后$tempCookie是IEM_CookieLogin经过base64_decode后反序列化,得到是一个数组,取出数组中的rand做对比
$tempCookie = IEM::requestGetCookie('IEM_CookieLogin', array()); ··· 略 ··· $tempUser = new User_API(); $tempUser->Load(intval($tempCookie['user'])); ··· 略 ··· if (!$tempUser->settings['LoginCheck'] == $tempCookie['rand']) { break; }
所以只要设置$tempCookie['rand']值是 True 就可以了,利用PHP弱类型的特点,$tempCookie['rand'] 可以是任何值,当然不能为空或者0
Payload:
原作者则是利用序列化可以设置key数据类型,将rand设置一个布尔型,这样即使用 === 全等也可以绕过
新版是怎么修复的呢?拿到 6.16 的代码 diff 一下,哈哈,这样判断应该就万无一失了,除非 $tempUser->settings['LoginCheck'] 能返回假,但是在 ./admin/com/init.php 也做了判断应该绕不过了。
在遭到这类漏洞攻击的情况下,我们首先要做的是更改相关服务的登录凭据以限制攻击。
然后当然是修复漏洞,加固应用啦。
https://github.com/joesmithjaffa/CVE-2017-14322
http://php.net/manual/zh/language.operators.precedence.php
https://blog.csdn.net/iamduoluo/article/details/8491746