SQL注入是最常见的(web)漏洞之一。所有SQL注入练习,在这里找到,使用MySQL作为后端。当SQL查询中包含SQL注入时,缺少用户控制输入的编码/转义。
根据信息在查询中的添加方式,您需要不同的东西来破坏语法。
有三种不同的方法可以在SQL语句中回显信息:
- 使用引号:单引号或双引号。
- 使用反引号。
- 直接注入:将数据直接从数据库服务器发送到攻击者控制的计算机。
例如,如果你想使用信息作为一个字符串,你可以这样做:
SELECT * FROM user WHERE name="root";
或者
SELECT * FROM user WHERE name='root';
如果你想使用整数信息,你可以这样做:
SELECT * FROM user WHERE id=1;
最后,如果你想使用信息作为列名,你需要这样做:
SELECT * FROM user ORDER BY name;
或者
SELECT * FROM user ORDER BY `name`;
也可以使用整数作为字符串,但会更慢:
SELECT * FROM user WHERE id ='1';
回显信息的方式,甚至使用什么分隔符,将决定使用的检测技术。但是,您没有此信息,您需要尝试猜测它。您需要制定假设并尝试验证它们。这就是为什么花时间浏览liveCD上的例子非常重要的原因。
db.php文件代码:
<?php $lnk = mysql_connect("localhost", "pentesterlab", "pentesterlab"); $db = mysql_select_db('exercises', $lnk); ?>
Example 1
<?php require_once('../header.php'); require_once('db.php'); $sql = "SELECT * FROM users where name='"; $sql .= $_GET["name"]."'"; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require_once '../footer.php'; ?>
在第一个例子中,我们可以看到参数是一个字符串,我们可以在表中看到一行。要理解服务器端代码,我们需要开始探索:
如果我们添加额外的字符,如“1234”,使用?name=root1234,表中不会显示任何记录。从这里,我们可以猜测请求在某种匹配中使用我们的值。
如果我们在请求中注入空格,则使用?name=root+++(编码后),显示记录。MySQL(默认情况下)在执行比较时将忽略字符串中的尾随空格。
如果我们使用双引号,?name=root”则表中不会显示任何记录。
如果我们使用注入单引号,?name=root’表格就会消失。我们可能打破了一些……
从第一部分开始,我们可以推断出整个必须如下:
SELECT * FROM users WHERE name='[INPUT]';
现在,让我们验证这个假设。
如果我们是对的,以下注入应该给出相同的结果。
- ?name=root’ and ‘1’=’1:初始查询中的引用将在注入结束时关闭。
- ?name=root’ and ‘1’=’1′ #(不要忘记编码#):初始查询中的引用将被注释掉。
- ?name=root’ and 1=1 #(别忘了编码#):在初始查询引用将被注释掉我们不需要’1’=’1’。
- ?name=root’ #(不要忘记编码#):初始查询中的引用将被注释掉,我们不需要1=1。
现在这些请求可能不会返回相同的内容:
- ?name=root’ and ‘1’=’0:初始查询中的引用将在注入结束时关闭。页面不应返回任何结果(空表),因为选择标准始终返回false。
- ?name=root’ and ‘1’=’1 #(不要忘记编码#):初始查询中的引用将被注释掉。我们应该与上面的查询具有相同的结果。
- ?name=root’ or ‘1’=’1:初始查询中的引用将在注入结束时关闭。or将选择所有结果,第二部分始终为真。它可能会给出相同的结果,但不太可能,因为该值用作此示例的过滤器(而不是一次只显示一个结果的页面)。
- ?name=root’ or ‘1’=’1′ #(不要忘记编码#):初始查询中的引用将被注释掉。我们应该与上面的查询具有相同的结果。
通过所有这些测试,我们可以确保我们有一个SQL注入。此培训仅侧重于检测。您可以查看其他PentesterLab高级课程,并了解如何利用此类问题。PentesterLab高级课程后面会讲到的。PentesterLab高级课程也是指web渗透测试平台:Web Pentester II。
Example 2
<?php require_once('../header.php'); require_once('db.php'); if (preg_match('/ /', $_GET["name"])) { die("ERROR NO SPACE"); } $sql = "SELECT * FROM users where name='"; $sql .= $_GET["name"]."'"; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
在此示例中,错误消息提供了开发人员创建的保护:ERROR NO SPACE。只要在请求中注入空格,就会显示此错误消息。它阻止我们使用该’ and ‘1’=’1方法或任何使用空格字符的指纹识别。但是,使用制表符(HT或\t)可轻松绕过此过滤。您将需要使用编码,以在HTTP请求中使用它。使用此简单绕过,您应该能够看到如何检测此漏洞。
Example 3
<?php require_once('../header.php'); require_once('db.php'); if (preg_match('/\s+/', $_GET["name"])) { die("ERROR NO SPACE"); } $sql = "SELECT * FROM users where name='"; $sql .= $_GET["name"]."'"; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
在此示例中,开发人员阻止空格和制表符。有一种方法可以绕过这个过滤器。您可以在关键字之间使用注释来构建有效请求,而无需任何空格或制表。可以使用以下SQL注释:/**/。通过使用此注释替换前面示例中的所有空格/列表,您应该能够测试此漏洞。
Example 4
<?php require_once('../header.php'); require_once('db.php'); $sql="SELECT * FROM users where id="; $sql.=mysql_real_escape_string($_GET["id"])." "; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
此示例表示对如何防止SQL注入的典型误解。在前面的3个示例中,使用该功能mysql_real_escape_string可以防止漏洞。在此示例中,开发人员使用相同的逻辑。但是,使用的值是整数,并且不会在单引号之间回显。由于该值直接放在查询中,因此使用mysql_real_escape_string不会阻止任何操作。在这里,您只需要能够添加空格和SQL关键字来打破语法。检测方法与用于基于字符串的SQL注入的方法非常类似。您只需在有效负载的开头不需要引号。例如:?id=2 or 1=1。
另一种检测方法是使用整数。最初的要求是?id=2。通过使用值2,我们可以检测到SQL注入:
- ?id=2 #(#需要编码)应该返回相同的东西。
- ?id=3-1应该返回相同的东西。数据库将自动执行减法,您将得到相同的结果。
- ?id=2-0 应该返回相同的东西。
- ?id=1+1(+需要编码)应该返回相同的东西。数据库将自动执行添加,您将获得相同的结果。
- ?id=2.0 应该返回相同的东西。
并且以下内容不应返回相同的结果:
- ?id=2+1。
- ?id=3-0。
Example 5
<?php require_once('../header.php'); require_once('db.php'); if (!preg_match('/^[0-9]+/', $_GET["id"])) { die("ERROR INTEGER REQUIRED"); } $sql = "SELECT * FROM users where id="; $sql .= $_GET["id"] ; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
这个例子与以前的检测方式非常相似。如果查看代码,您将看到开发人员试图通过使用正则表达式来阻止SQL注入:
if (!preg_match('/^[0-9]+/', $_GET["id"])) {
die("ERROR INTEGER REQUIRED");
}
但是,使用的正则表达式是不正确的; 它只是确保参数id 开始以数字。先前使用的检测方法可用于检测此漏洞。例如:?id=2 or 1=1。
Example 6
<?php require_once('../header.php'); require_once('db.php'); if (!preg_match('/[0-9]+$/', $_GET["id"])) { die("ERROR INTEGER REQUIRED"); } $sql = "SELECT * FROM users where id="; $sql .= $_GET["id"] ; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
这个例子是另一种方式。开发人员再次在正则表达式中犯了一个错误:
if (!preg_match('/[0-9]+$/', $_GET["id"])) {
die("ERROR INTEGER REQUIRED");
}
此正则表达式仅确保参数以数字id 结尾(由于$符号)。它不能确保参数的开头有效(缺失^)。您可以使用之前学到的方法。
您只需要在有效负载的末尾添加一个整数。这个数字可以是有效载荷的一部分,也可以放在SQL注释之后:?id=1 or 1=1 # 123。(#需要编码)!
Example 7
<?php require_once('../header.php'); require_once('db.php'); if (!preg_match('/^-?[0-9]+$/m', $_GET["id"])) { die("ERROR INTEGER REQUIRED"); } $sql = "SELECT * FROM users where id="; $sql .= $_GET["id"]; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr><th>id</th><th>name</th><th>age</th></tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
另一个坏正则表达式的例子:
if (!preg_match('/^-?[0-9]+$/m', $_GET["id"])) { die("ERROR INTEGER REQUIRED"); }
在这里我们可以看到字符串的开始(^)和结束($)被正确检查。但是,正则表达式包含修饰符PCRE_MULTILINE(/m)。多行修饰符仅验证其中一行仅包含整数,因此以下值有效(由于其中包含新行):
123\nPAYLOAD;
PAYLOAD\n123;
PAYLOAD\n123\nPAYLOAD。
在URL中使用时,需要对这些值进行编码,但是通过使用编码和先前看到的技术,您应该能够检测到此漏洞。例如:
http://192.168.1.11/sqli/example7.php?id=2%0A%20or%201=1
Example 8
<?php require_once('../header.php'); require_once('db.php'); $sql = "SELECT * FROM users ORDER BY `"; $sql .= mysql_real_escape_string($_GET["order"])."`"; $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr> <th><a href="example8.php?order=id">id</th> <th><a href="example8.php?order=name">name</th> <th><a href="example8.php?order=age">age</th> </tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
在此示例中,参数名称表示它将在SQL查询中回显的位置。如果查看MySQL文档,有两种方法可以在ORDER BY语句中提供值:
- 直接:ORDER BY name;
- 在反引号之间:ORDER BY `name`。
该ORDER BY声明不能与单引号或双引号内的值一起使用。如果使用它,则不会对任何内容进行排序,因为MySQL将这些视为常量。
要检测此类漏洞,我们可以尝试使用不同的有效负载获得相同的结果:
- name` #(#需要编码)应该给出相同的结果。
- name` ASC #(#需要编码)应该给出相同的结果。
- name`, `name:初始查询中的反向标记将在注入结束时关闭。
以下有效负载应该给出不同的结果:
- name` DESC #(#需要编码)。
- name` 不应该给出任何结果,因为语法不正确。
Example 9
<?php require_once('../header.php'); require_once('db.php'); $sql = "SELECT * FROM users ORDER BY "; $sql .= mysql_real_escape_string($_GET["order"]); $result = mysql_query($sql); if ($result) { ?> <table class='table table-striped'> <tr> <th><a href="example9.php?order=id">id</th> <th><a href="example9.php?order=name">name</th> <th><a href="example9.php?order=age">age</th> </tr> <?php while ($row = mysql_fetch_assoc($result)) { echo "<tr>"; echo "<td>".$row['id']."</td>"; echo "<td>".$row['name']."</td>"; echo "<td>".$row['age']."</td>"; echo "</tr>"; } echo "</table>"; } require '../footer.php'; ?>
这个例子类似于前一个例子,但不是反向引号“`
在这种情况下可以使用其他方法,因为我们之前没有反向引号直接注入请求。我们可以使用MySQL IF语句生成更多有效负载:
- IF(1, name,age) 应该给出相同的结果。
- IF(0, name,age)应该给出不同的结果。您可以看到列按年龄排序,但sort函数将值比较为字符串,而不是整数(10小于2)。这是一个副作用IF,如果列中的一个包含字符串,则会将值排序为字符串。