SQL注入攻击中,联合查询(UNION SELECT)是最直接的数据窃取手段之一。攻击者构造联合查询的前提,是必须准确知道原始查询返回的列数。这个判断过程本身,就暴露了攻击路径和防御要点。
判断列数主要有两种方法:ORDER BY子句排序法和UNION SELECT NULL值填充法。两种方法原理不同,但都利用了数据库对查询结构严格校验的特性。ORDER BY通过数字索引引用列,当数字超出实际列数时数据库报错;UNION SELECT则要求前后查询列数一致,通过不断增加NULL值试探匹配点。
ORDER BY列数判断原理与实操ORDER BY后面可以接列名,也可以接数字。数字代表SELECT列表中列的位置序号。比如ORDER BY 1表示按第一列排序,ORDER BY 2按第二列排序。当指定的数字超过查询返回的总列数时,数据库无法定位对应列,就会抛出错误。
攻击者通常这样操作:先提交ORDER BY 10,如果页面正常返回,说明列数大于等于10;接着尝试ORDER BY 20,如果报错,说明列数在10到19之间;然后用二分法逐步缩小范围,直到找到临界值。假设ORDER BY 15正常、ORDER BY 16报错,那么列数就是15。
实际注入中,报错的表现形式多样。有的网站直接回显数据库错误信息,比如"Unknown column '16' in 'order clause'";有的网站做了全局异常处理,返回空白页面或统一错误页;还有的网站虽然隐藏了错误细节,但页面布局出现变化,比如列表消失、内容区域空白。有经验的攻击者会观察响应长度、响应时间、页面DOM结构的变化来判断。
举个例子,假设存在注入点的URL是:
http://example.com/products.php?id=1 ORDER BY 1--
攻击者会依次递增数字,观察响应差异。这个过程的自动化程度很高,SQLMap等工具可以在秒级完成判断。
UNION SELECT NULL值填充法第二种方法是直接用UNION SELECT拼接NULL值。UNION操作符要求前后两个SELECT语句返回的列数完全相同,否则数据库报错。攻击者从UNION SELECT NULL开始,逐步增加NULL数量:
id=1 UNION SELECT NULL-- id=1 UNION SELECT NULL,NULL-- id=1 UNION SELECT NULL,NULL,NULL--
当NULL的数量与原始查询列数匹配时,查询语法正确,页面会正常返回。通常联合查询还会在页面上回显数据,攻击者能直接看到哪个NULL位置被替换成了实际数据。
这两种方法各有优劣。ORDER BY法不需要闭合前面的查询结果集,适合在报错可见的场景快速判断;UNION SELECT法需要处理前面查询不返回结果的情况,常用id=-1这样的负数让原查询无结果,使联合查询的数据独占回显区域。实际渗透测试中,两种方法经常配合使用。
列数判断中的干扰因素与绕过技巧WAF(Web应用防火墙)是列数判断的主要障碍。很多WAF规则会拦截包含ORDER BY数字、UNION SELECT NULL的请求。攻击者演变出了多种绕过手法。
针对ORDER BY的绕过:用内联注释包裹关键字,如ORDER//BY;用浮点数代替整数,如ORDER BY 1.0;用表达式代替直接数字,如ORDER BY (1);甚至用十六进制编码或双重URL编码混淆。
针对UNION SELECT的绕过:用UNION ALL SELECT替代;在关键字间插入空白字符、换行符、制表符;用科学计数法表示NULL,如UNION SELECT 0e0;用等效函数替代,如UNION SELECT COALESCE(NULL)。
还有一种情况是网站后端对参数做了类型强转。比如id参数被强制转换成整型,那么字符串注入载荷会被截断。这时攻击者需要寻找其他未做类型限制的参数,或者利用POST请求体、Cookie、HTTP头等位置的注入点。
列数确定后的数据提取策略列数确定只是联合查询注入的第一步。接下来攻击者需要找出哪些列的数据类型与目标数据兼容,以及哪些列的回显会出现在页面可见位置。
数据类型探测通常用NULL占位,逐个替换为字符串测试值。比如已经确定有5列,攻击者会构造:
id=-1 UNION SELECT 'test',NULL,NULL,NULL,NULL-- id=-1 UNION SELECT NULL,'test',NULL,NULL,NULL--
依次测试每列,看哪个位置能正常回显字符串。如果某列原本是整型,放入字符串可能报错或显示异常,攻击者就知道该列不适合提取文本数据。
回显位置确定后,攻击者用database()、user()、version()等函数替换对应NULL,获取数据库基本信息。然后通过information_schema查询表名、列名,最终提取敏感数据。整个过程高度流程化,这也是SQL注入至今仍位列OWASP Top 10的重要原因。
列数判断在盲注场景的延伸应用不是所有注入点都能直接回显数据。在盲注场景下,列数判断同样有价值。攻击者通过ORDER BY配合时间延迟或布尔判断,一样能确定列数。比如ORDER BY 16时页面正常,ORDER BY 17时触发SLEEP(5)延迟,说明列数为16。
更隐蔽的做法是利用条件语句。攻击者构造ORDER BY CASE WHEN (条件) THEN 1 ELSE 2 END,通过观察排序结果差异来推断信息。这种手法把列数判断和布尔盲注结合起来,绕过那些只检测ORDER BY数字的简单规则。
列数信息本身对攻击者也有直接价值。列数多说明数据表结构复杂,可能包含更多敏感字段;列数少则可能只是简单查询,攻击者会调整后续攻击策略。在自动化扫描工具中,列数判断是优先级最高的探测步骤之一。
开发层面的防御措施防御SQL注入,列数判断只是攻击链条中的一个环节。根本解决方案是杜绝注入漏洞本身。参数化查询是首选方案,使用预编译语句将SQL逻辑与数据分离,攻击者无法改变查询结构。
以PHP的PDO为例:
$stmt = $pdo->prepare("SELECT id, name, price FROM products WHERE category_id = ?");
$stmt->execute([$category_id]);
这种写法下,无论$category_id传入什么值,都只被当作字符串参数处理,不会参与SQL语法解析。ORDER BY和UNION SELECT都无法注入。
如果业务确实需要动态排序字段,比如用户可选择按价格或销量排序,必须用白名单校验。绝不能直接把用户输入拼接到ORDER BY后面。正确做法是:
$allowed_columns = ['price', 'sales', 'rating', 'create_time'];
$order_by = in_array($_GET['order'], $allowed_columns) ? $_GET['order'] : 'id';
$stmt = $pdo->prepare("SELECT * FROM products ORDER BY $order_by DESC");
这里$order_by虽然拼接进了SQL,但经过了严格的白名单过滤,只允许预定义的列名通过。
数据库层面的防护加固最小权限原则是数据库安全的基本准则。应用连接数据库的账号,只应拥有业务必需的权限。对于只读查询场景,赋予SELECT权限即可,禁止DROP、INSERT、UPDATE、DELETE等写权限。这样即使发生注入,攻击者也难以修改数据或写入WebShell。
MySQL的secure_file_priv配置限制了LOAD_FILE和INTO OUTFILE的文件操作范围。将该参数设置为空或指定安全目录,可以阻止攻击者通过注入读写服务器文件。很多攻击者在获取数据后,会尝试用INTO OUTFILE写一句话木马,这个配置直接封死了这条路径。
数据库错误信息不应暴露给前端。生产环境必须关闭display_errors,设置自定义错误页面。攻击者依赖错误回显判断注入是否成功,错误信息屏蔽能显著增加攻击难度和时间成本。
WAF与检测规则的优化WAF是防御SQL注入的重要补充层,但不能完全依赖。针对列数判断行为,WAF可以部署以下检测逻辑:检测请求参数中是否包含ORDER BY后跟数字的模式;检测连续递增的UNION SELECT NULL请求;对同一来源IP短时间内大量SQL关键字请求进行频率限制。
但规则不能过于僵化。正常业务中也可能出现ORDER BY 1这样的写法,比如某些前端表格组件默认按第一列排序。WAF需要结合上下文判断,避免误杀正常请求。更有效的做法是在WAF中集成语义分析引擎,解析SQL语法树,识别真正的注入攻击而非孤立的关键字匹配。
日志审计和异常检测同样重要。对数据库查询日志进行离线分析,识别ORDER BY数字异常递增、UNION SELECT NULL反复试探等行为模式,可以事后发现被绕过WAF的攻击行为,及时修补漏洞。
纵深防御体系的构建单点防御总有被突破的可能。有效的安全策略是构建纵深防御体系。代码层的参数化查询是第一道防线;数据库权限控制和错误信息屏蔽是第二道防线;WAF和RASP(运行时应用自我保护)是第三道防线;日志监控和入侵检测是第四道防线。
RASP技术近年来发展迅速。它嵌入应用运行时环境,能感知完整的SQL执行上下文。当检测到SQL语句结构被用户输入改变时,RASP可以在执行前拦截,即使代码中存在注入漏洞也能提供保护。相比WAF,RASP的误报率更低,因为它看到的是最终执行的SQL,而非原始HTTP请求。
列数判断作为SQL注入攻击的初始探测步骤,其技术细节揭示了攻击者和防御者之间的持续博弈。理解攻击手法不是为了利用漏洞,而是为了更精准地设计防御策略。安全人员应该定期用ORDER BY和UNION SELECT NULL手法测试自己的应用,验证防御措施是否真正生效。
渗透测试中发现的每一个可判断列数的注入点,都意味着参数化查询的缺失或白名单校验的疏漏。修复这些漏洞,比单纯调优WAF规则更有价值。代码安全才是根本,其他都是辅助手段。
