代码审计常见场景之CSRF与变量覆盖
CSRF
1、挖洞经验:
(1)黑盒:CSRF主要是用于越权操作,管理后台、会员中心、论坛帖子以及交易管理等,这几个场景里面,管理后台又是最高危的地方。我们在挖掘CSRF的时候可以先搭建好环境,打开几个有非静态操作的页面,抓包看看有没有token,如果没有token的话,再直接请求这个页面,不带referer。如果返回的数据还是一样的话,那说明很有可能有CSRF漏洞了。
(2)白盒:只要读代码的时候看看几个核心文件里面有没有验证token和referer相关的代码,这里的核心文件指的是被大量文件引用的基础文件,或者直接搜"token"这个关键字也能找,如果在核心文件没有,再去看看你比较关心的功能点的代码有没有验证。
可以看YzmCMS v5.7审计的那个CSRF漏洞,那个cms虽然做了CSRF处理,但可以绕过,用哪个场景更能理解,天目MVC v1.381中的CSRF如下面的场景一般,跟本没有检测referer和token。虽可以复现但做完也可能让人摸不着头脑。
2、搜索关键词
token,referer,common
3、场景代码
前端登录login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="./login.php" method="post">
账号:<input type="text" name="username"><br >
密码:<input type="password" name="passwd"> <br >
<input type="submit" value="提交" name="login">
</form>
</body>
</html>
前端添加用户reg.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="reg.php" method="POST">
username: <input type="text" name="username"><br>
passwd: <input type="password" name="passwd"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>
后端登录验证login.php
<?php
header("content-type:text/html;charset=utf-8");
if (!isset($_POST['login'])){
echo '<script>';
echo 'window.location.href="login.html"';
echo '</script>';
exit("非法访问");
}else{
$username = $_POST['username'];
$passwd = $_POST['passwd'];
$connect = mysql_connect("127.0.0.1","root","123456");
mysql_select_db("admin",$connect);
$sql = "SELECT * FROM user WHERE username = '$username' and passwd = '$passwd'";
$result = mysql_query($sql);
mysql_close();
if($row = mysql_fetch_array($result)){
session_start();
$_SESSION['username'] = $row['username'];
echo $_SESSION['username'].'欢迎登录';
echo "<a href = 'reg.html'>添加用户</a>";
}else{
echo "fale!";
}
}
</html>
后端添加用户reg.php
<?php
session_start();
header("content-type:text/html;charset=utf-8");
if(!isset($_SESSION['username'])){
echo '<script>';
echo 'alert("请登入用户");window.location.href="login.php"';
echo '</script>';
exit();
}
if(!empty($_POST['submit'])){
$username = addslashes($_POST['username']);
$passwd = addslashes($_POST['passwd']);
$connect = mysql_connect("127.0.0.1","root","123456");
mysql_select_db("admin",$connect);
$sql = "INSERT INTO user (username,passwd) VALUE ('$username','$passwd');";
$result = mysql_query($sql) or die("执行命令失败".mysql_error());
mysql_close();
if($result){
echo "regist success!";
}else{
echo "regist fail!";
}
}else{
echo "not POST!";
}
?>
4、修复方案
• 验证码
• 添加 Referer 验证
• 添加 Token 验证
• 修改密码之类的功能验证原密码
理解:对于CSRF这种的防御机制可认为是在访问页面时,后台生成一个随机数,提交的请求中会带着这个随机数,在后台验证,与后台的相同则来自网页请求,不同则可能是通过其他方式发送的请求,而这个随机数可以是验证码(用户填写)、可以是Referer(后台获取)、可以是Token(自动获取且后台生成用户不参与,可存于cookie或提交的参数中)(以上为个人理解,如有错误欢迎指正,心灵脆弱轻喷!)
变量覆盖
1、挖洞经验
由于变量覆盖漏洞通常要结合应用其他功能代码来实现完整攻击,所以挖掘一个可用的变量覆盖漏洞不仅仅要考虑的是能够实现变量覆盖,还要考虑后面的代码能不能让这个漏洞利用起来。要挖可用的变量覆盖漏洞,一定要看漏洞代码行之前存在哪些变量可以覆盖并且后面有被使用到。由函数导致的变量覆盖比较好挖掘,只要搜寻参数带有变量的extract()、parse_str()函数,然后去回溯变量是否可控,extract()还要考虑它的第二个参数,具体细节我们后面在详细介绍这个函数的时候再讲。import_request_variables()函数则相当于开了全局变量注册,这时候只要找哪些变量没有初始化并且操作之前没有赋值的,然后就大胆地去提交这个变量作为参数吧,另外只要写在import_request_variables()函数前面的变量,不管是否已经初始化都可以覆盖,不过这个函数在PHP 4~4.1.0和PHP 5~5.4.0的版本可用。
关于上面我们说到国内很多程序使用双$$符号注册变量会导致变量覆盖,我们可以通过搜“$$”这个关键字去挖掘,不过建议挖掘之前还是先把几个核心文件通读一遍,了解程序的大致框架。
2、常见危险函数
• $$
• extract()函数
• import_request_variables()函数
• parse_str()函数执行
函数介绍
foreach()
手册:https://www.php.net/manual/zh/control-structures.foreach.php
<?php
header("Content-type:text/html;charset=utf-8");
$a = 10;
foreach (array('_GET','_POST',) as $request) {//进入遍历后$request依次等于_GET、_POST,注意没有前面$符
foreach ($$request as $_key=>$_value){//此处$$request使上面的_GET、_POST变成了$_GET,$POST。并且是一个数组形式,单纯的$_GET无法用foreach函数。
echo '未赋值前<br>';//未赋值前$key已被get传入a,即$key=a,又因为之前a的值为10,所以$$key为10,而传入值被赋值在$value中,所以$value=2
echo '$key:'.$_key.'<br>';
echo '$$key:'.$$_key.'<br>';
echo '$value:'.$_value.'<br>';
$$_key = $_value;//根据为赋值前的关系,$$key代表$key的值,$value代表传入值,所以将传入值赋值给$$key替换掉a,形成覆盖.
echo '赋值后<br>';
echo '$key:'.$_key.'<br>';
echo '$$key:'.$$_key.'<br>';
echo '$value:'.$_value.'<br>';
}
}
echo '最后a='.$a;
//形成的数组形式,个人理解
//[0]=>['_GET']=>['value1']
//[0]=>['_POST']=>['value2']
//http://localhost:63342/WWW/blfg.php?a=2
//get
//未赋值前
//$key:a
//$$key:10
//$value:2
//赋值后
//$key:a
//$$key:2
//$value:2
//最后a=2
//参考资料:https://www.cnblogs.com/keleyu/p/3171606.html
extract ( array &$array
[, int $flags
= EXTR_OVERWRITE [, string $prefix
= NULL
]] ) : int
• 本函数用来将变量从数组中导入到当前的符号表中。
• 检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突,主要功能根据第二个参数不同而有差距,主要介绍三种,其他详细看手册
• $flags危险参数:
• EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
• EXTR_PREFIX_SAME - 如果有冲突,在变量名前加上前缀 prefix。自 PHP 4.0.5 起,这也包括了对数字索引的处理
• EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。可以用在已经定义了一组合法的变量,然后要从一个数组例如 $_REQUEST 中提取值覆盖这些变量的场合。本标记是 PHP 4.2.0 新加的。
<?php
header("Content-type:text/html;charset=utf-8");
//第一种
$passwd = 'pwd';
$arr = array(
'username' => 'username',
'passwd' => 'passwd',
'rand' => 'rand',
);
extract($arr,EXTR_PREFIX_SAME,'pwd');
echo "$username,$passwd,$rand";
echo ','.$pwd_passwd;
//username,pwd,rand,passwd
//第二种
$passwd = 'pwd';
$arr = array(
'username' => 'username',
'passwd' => 'passwd',
'rand' => 'rand',
);
extract($arr,EXTR_OVERWRITE);
echo "$username,$passwd,$rand";
//username,passwd,rand
//第三种
$passwd = 'pwd';
$arr = array(
'username' => 'username',
'passwd' => 'passwd',
'rand' => 'rand',
);
extract($arr,EXTR_IF_EXISTS,'pwd');
echo "$passwd";
//passwd
parse_str ( string $encoded_string
[, array &$result
] ) : void
• 如果 encoded_string
是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result
则会设置到该数组里 )。
<?php
header("Content-type:text/html;charset=utf-8");
$passwd = 'pwd';
parse_str($passwd='a');
echo $passwd;
//a
import_request_variables ( string $types
[, string $prefix
] ) : bool
• 将 GET/POST/Cookie 变量导入到全局作用域中。如果你禁止了 register_globals,但又想用到一些全局变量,那么此函数就很有用。
<?php
header("Content-type:text/html;charset=utf-8");
$a = 0;
import_request_variables("G");
if($a==1){
echo 'success';
}else{
echo "fail";
}
//?a=1,输出success
3、修复方案
• 在php.ini文件中设置register_globals=OFF
• 使用原始变量数组,如$_POST,$_GET等数组变量进行操作
• 不使用foreach语句来遍历$_GET变量,而改用[(index)]来指定
• 验证变量是否存在,注册变量前先判断变量是否存在