当前位置:首页 > 知识

sql注入攻击与防御

2015年,入攻某电商平台的击防安全团队发现数据库中多了一张从没见过的管理员账号。密码是入攻MD5加密的,但用户名赫然写着“hacker”。击防追查日志后确认,入攻攻击者只用了不到三分钟,击防通过一个商品搜索框就拿到了整个用户表的入攻读取权限。那个搜索框的击防代码是这么写的:SELECT * FROM products WHERE name = '" + keyword + "'。攻击者输入了 ' OR '1'='1,入攻条件永远为真,击防数据库乖乖交出了所有数据。入攻

这就是击防SQL注入。它的入攻本质不是“黑客技术”,而是击防程序员把用户输入当成了代码来执行。数据库分不清哪个字符是入攻用户数据、哪个是SQL指令,只要拼接字符串,就必然存在这个风险。很多人以为过滤单引号就万事大吉,但攻击者可以用 CHAR(39) 绕过,也可以用注释符 -- 截断后续语句。防御不是靠黑名单,而是靠参数化。

参数化查询(Prepared Statement)是唯一被证明可靠的防御手段。它的原理很简单:SQL语句的结构先发给数据库编译,用户输入的值后传进去,数据库永远只把它当数据,不当代码。以Java为例:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); 

这样即使用户输入 ' OR '1'='1,数据库也只会去匹配一个叫“' OR '1'='1”的用户名,不会改变查询逻辑。我见过不少团队用ORM框架(比如MyBatis、Hibernate)却仍然中招,原因就是他们在XML里写了 ${ } 而不是 #{ }。${ } 是直接拼接,#{ } 才是参数化。一个符号的差别,就是安全与沦陷的分界线。

那为什么参数化这么难推广?因为有些场景确实“不方便”。比如动态表名、动态排序字段(ORDER BY 后面的列名),参数化不支持。这时候只能白名单校验——把允许的表名、列名写死在代码里,用户传什么过来都只做映射。我见过一个后台系统,排序参数直接拼进SQL,攻击者用 CASE WHEN 盲注把管理员密码一位一位猜了出来。白名单不优雅,但有效。

另一个常见误区是“用了存储过程就安全”。存储过程内部如果还是拼接字符串,照样注入。比如 EXEC('SELECT * FROM users WHERE name = ''' + @name + ''''),这跟直接拼SQL没区别。安全与否取决于数据是否被当作代码执行,而不是用了什么包装。

还有人迷信WAF(Web应用防火墙)。WAF能挡掉大部分自动化扫描工具,但挡不住手工构造的绕过。比如用 /*!50000union*/ 这种MySQL内联注释,或者用 %bf%27 这种畸形编码。WAF是辅助,不是替代。把WAF当唯一防线,等于把家门钥匙藏在脚垫下面。

实际排查中,我发现最容易出问题的地方是“内部系统”。开发人员觉得后台只有自己人用,搜索框、导出功能、报表参数全都直接拼SQL。结果一旦有一个低权限账号被钓鱼,整个数据库就暴露了。攻击者不需要多高明的手段,一个 sqlmap 跑一晚上,什么都能拖出来。

最后说一个细节:报错信息。很多应用把数据库原始错误直接返回给前端,比如 You have an error in your SQL syntax near '...'。这等于给攻击者递了一张地图。关掉详细报错,记录到日志里,前端只显示“系统繁忙”。这不能阻止注入,但能大幅增加盲注的难度。防御是分层的事,每一层都不完美,但叠起来就能让攻击成本高到不值得。

分享到: