6.3 会话认证漏洞
会话认证是一个非常大的话题,涉及各种认证协议和框架,如cookie、session、sso、oauth、openid等,出现问题比较多的在cookie上面,cookie是Web服务器返回给客户端的一段常用来标识用户身份或者认证情况的字符串,保存在客户端,浏览器下次请求时会自动带上这个标识,由于这个标识字符串可以被用户修改,所以存在安全风险,一般这块的认证安全问题都出在服务端直接取用cookie的数据而没有校验,其次是cookie加密数据存在可预测的情况。另外是session是保存在服务器端的信息,如果没有代码操作,客户端不能直接修改session,相对比较安全。
sso、oauth、openid与cookie、session相比不是一个维度的东西,由于这块在应用代码审计没有什么合适的案例,暂时先不介绍。
6.3.1 挖掘经验
认证漏洞在代码审计的时候能遇到比较多的是出现在cookie验证上面,通常是没有使用session来认证,而是直接将用户信息保存在cookie中,程序使用的时候直接调用。一般这个过程都有一个统一的函数去取数据调用,容易导致SQL注入和越权等漏洞。在挖掘登录认证漏洞的时候,可以先看一下程序的登录功能代码,看看整个登录过程的业务逻辑有没有可以控制session值或者直接绕过密码验证的漏洞;另外需要关注程序验证是否为登录的代码,通俗的说是验证cookie的代码,是不是直接去取cookie里面的值,然后怎么去判断这个值来验证是否登录。以前见过相当粗糙的验证是直接判断cookie里面的username参数是否为空,还有就是以cookie里面的用户名来作为当前用户,这种情况直接把用户名改成admin等管理员用户名就直接是管理员权限了。
6.3.1.1 cookie认证安全
cookie可以保存任何字符串,各个浏览器保存cookie字节数大小不一样,一般都不超过4096个字节,通常cookie用来保存登录账号的标识信息,比如用户名或者sessionid等,浏览器每次请求的时候都会再次带上对应这个域名的cookie信息,服务器应用程序可以对cookie进行读取修改或者删除等任意操作。
cookie出现问题比较多的是cookie的SQL注入等常见漏洞,以及Web应用程序在服务端直接读取cookie的用户名或者ID值来操作当前这个用户的数据,这里存在很大的一个问题是cookie可以伪造,从而就导致了伪造用户身份登录的漏洞。
通常一个cookie验证的代码大概如下:
< ? php
session_start ();
function login ()
{
if (账号密码正确)
{
setcookie ( 'username' , 'admin' );
$_SESSION['username'] = 'admin' ;
}
}
// 判断 cookie 里面的用户名是否和 session 里的用户名一致
if ( $_COOKIE['username']===$_SESSION['username'] )
{
// 操作 $_SESSION['username'] 用户的数据
}
else
{
login ();
}
这样的写法一般情况不会出现验证上面的安全问题,下面我们通过案例来看看有问题的写法。
6.3.1.2 Espcms任意用户登录分析
我们这里以乌云漏洞编号为WooYun-2015-90324的“ESPCMS所有版本任意用户登录”漏洞来做一个简单的分析。
在文件/interface/memebermain.php的in_center()函数可以看到如下代码:
function in_center () {
if ( $this->CON['mem_isucenter'] ) {
include_once admin_ROOT . 'public/uc_client/client.php' ;
}
parent :: start_pagetemplate ();
parent :: member_purview ();
$lng = ( admin_LNG == 'big5' ) ? $this->CON['is_lancode'] : admin_LNG ;
$db_where = "userid=$this->ec_member_username_id AND username='$this-> ec_member_username' " ;
$db_table1 = db_prefix . 'member AS a' ;
$db_table2 = db_prefix . 'member_value AS b' ;
$db_sql = "SELECT * FROM $db_table1 LEFT JOIN $db_table2 ON a.userid = b.userid WHERE a.userid = $this->ec_member_username_id " ;
$rsMember = $this->db->fetch_first ( $db_sql );
$rsMember['rankname'] = $this->get_member_purview ( $rsMember['mcid'] , 'rankname' );
$userid = $this->ec_member_username_id ; // 获取 userid
if ( empty ( $userid )) {
exit ( 'user err ! ' );
}
$db_table = db_prefix . "order" ;
$db_where = " WHERE userid=$userid" ;
在代码中$userid=$this->ec_member_username_id;这行代码设置当前用户ID,随后根据这个$userid变量去直接操作这个id的用户数据,而这个$this->ec_member_username_id变量的值又是从哪来的呢?注意代码最开始的地方有调用parent::member_purview()函数,我们跟过去看看,在/public/class_connector.php文件的member_purview()函数,代码如下:
function member_purview ( $userrank = false , $url = null , $upurl = false ) {
$this->ec_member_username = $this->fun->eccode ( $this->fun-> accept ( 'ecisp_member_username' , 'C' ), 'DECODE' , db_pscode );
$user_info = explode ( '|' , $this->fun->eccode ( $this->fun-> accept ( 'ecisp_member_info' , 'C' ), 'DECODE' , db_pscode ));
list ( $this->ec_member_username_id , $this->ec_member_alias , $this-> ec_member_integral , $this->ec_member_mcid , $this->ec_member_email , $this->ec_member_lastip , $this->ec_member_ipadd , $this->ec_member_useragent , $this->ec_member_adminclassurl ) = $user_info ;
可以看到list()函数中使用$user_info数组为$this->ec_member_username_id变量进行赋值,而$user_info数组是从cookie中解密出来的,关于这个算法的加密代码在/public/class_function.php文件的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 ;
}
这是一个很明显的可逆算法,这里就不再重点分析这个算法。
6.3.2 漏洞防范
所有用户输入的值都是不完全可信的,所以在防御认证漏洞之前,我们应该先了解认证的业务逻辑,严格限制输入的异常字符以及避免使用客户端提交上来的内容去直接进行操作。应该把cookie和session结合起来使用,不能从cookie中获取参数值然后进行操作。另外在设置session时,需要保证客户端不能操作敏感session参数。
特别需要注意的是敏感数据不要放到cookie中,目前还有不少应用会把账号和密码都直接放入到cookie中,cookie在浏览器端以及传输过程中都存在被窃取的可能性,如果程序限制了一个用户只能同时在一个IP上面登录,这时候即使别人拿到了你不带密码的cookie也会使用不了,但是如果cookie里面保存了用户名和密码,这时候攻击者就可以尝试用密码直接登录了。