1. 项目概述:为什么SQL漏洞是面试官的“心头好”?
干了这么多年安全,也面过不少人,我发现一个挺有意思的现象:无论你是应聘渗透测试、安全开发还是安全运维,面试官几乎都会把SQL注入漏洞拎出来问一遍。从“什么是SQL注入”这种基础概念,到“如何绕过WAF”、“如何利用二次注入”这种进阶实战,再到“如何从架构层面防御”,它就像一张考卷,能快速检验出你对Web安全的理解深度和实战经验。这项目标题“Hw常问sql漏洞问题”,说白了,就是一份针对安全岗位(尤其是HW/红蓝对抗相关)面试的SQL注入考点精讲与实战复盘。
为什么SQL注入这么受青睐?因为它太“经典”了。它不像一些复杂的0day,SQL注入的原理直白,危害巨大(直接拖库、篡改数据、甚至getshell),而且贯穿了整个Web应用的发展史。从十几年前用‘ or ‘1’=‘1就能通杀一片的“上古时代”,到现在各种过滤、预编译、WAF层层设防,攻防双方围绕它展开了无数轮博弈。能讲清楚SQL注入,意味着你至少懂Web基础(HTTP、数据库)、懂代码(至少能看懂SQL语句)、懂一点绕过技巧、懂防御原理。所以,这不仅仅是一个技术点,它是一个完整的安全能力切面。
接下来,我会结合我这些年做渗透、代码审计以及面试别人的经验,把面试官常问的那些SQL注入问题掰开揉碎了讲。我们不止讲“是什么”,更重点讲“为什么”和“怎么防/怎么绕”,并附上大量我实际踩过的坑和总结的技巧。目标很明确:让你下次被问到SQL注入时,能回答得有深度、有细节、有实战感,而不是只会背教科书上的定义。
2. 核心原理与分类:理解攻击的“根”
面试往往从这里开始:“简单说一下什么是SQL注入?” 你如果只回答“用户输入被拼接到SQL语句中执行”,那就太单薄了。我们需要把这个过程具象化,并引出其核心分类。
2.1 注入的本质:数据与代码的边界模糊
SQL注入的根本原因,在于程序没有清晰地区分“代码”和“数据”。在一条SQL语句中,代码是那些固定的关键字、操作符和结构(如SELECT,FROM,WHERE,=),而数据是来自用户输入、需要查询或操作的具体值(如用户名、搜索关键词)。
一个安全的程序应该这样处理:先构建好SQL语句的“代码骨架”(也叫预编译语句),这个骨架里留有“占位符”专门用来接收“数据”。程序把用户输入的数据,原封不动地、作为纯字符串填充到占位符里,数据库引擎会严格区分这两者,输入中的任何SQL关键字都不会被当作指令执行。
而存在漏洞的程序则是反过来的:它直接把用户输入(数据)和程序自身的SQL代码字符串拼接在一起,然后一股脑交给数据库执行。如果用户在输入里精心混入了SQL代码片段(比如一个单引号‘来闭合字符串,后面跟上or 1=1),数据库引擎就无法分辨哪些是程序的本意,哪些是用户的恶意输入,最终导致恶意代码被执行。
注意:这里常有一个误区,很多人认为SQL注入是因为“没过滤单引号”。过滤是治标不治本的手段,真正的治本之道是“使用参数化查询(预编译)”,从根源上分离代码与数据。面试时强调这一点,能体现你的理解深度。
2.2 主要注入类型与实战场景
根据注入点参数类型、数据库报错信息、结果回显方式的不同,SQL注入主要分为以下几类。面试官可能会让你举例说明,或者问“在盲注的情况下你会怎么做?”
1. 基于错误回显的注入这是最“友好”的情况。当应用程序将数据库的错误信息直接显示给用户时,攻击者可以通过构造非法参数,触发数据库报错,从而从错误信息中获取数据库结构、字段名甚至数据内容。
- 常见于:开发调试模式未关闭的站点。
- 面试点:如何利用报错信息?例如MySQL的
updatexml()、extractvalue()函数,或者SQL Server的convert()类型转换错误,都可以用于在报错信息中带出查询结果。 - 实战技巧:
and updatexml(1, concat(0x7e, (select user()), 0x7e), 1)这类语句,目的是让数据库执行一个会产生错误的函数,并将我们想查询的数据(如select user())拼接到错误信息里返回。
2. 联合查询注入当页面会直接显示数据库查询结果时(如新闻列表、用户信息页),这是最高效的方式。利用UNION操作符,将恶意查询的结果“附加”到原始查询结果后面,一起显示出来。
- 关键步骤:
- 确定列数:使用
order by 5或union select 1,2,3,4,5来试探,直到页面正常回显,从而确定原始查询的字段数量。 - 确定回显点:在
union select中,用数字(如1,2,3)或易识别的字符串(如‘a’,@@version)替换字段,看哪个数字/内容显示在了页面上,这些位置就是我们可以利用来回显数据的地方。 - 获取数据:在回显点替换为我们想要的查询,如
union select 1, database(), 3, 4。
- 确定列数:使用
- 面试点:
UNION查询的前提是前后两个SELECT语句的列数必须相同,且对应列的数据类型要兼容。常问“如何快速判断列数?”
3. 布尔盲注页面没有明确的数据回显,也没有详细的报错,但会根据SQL语句执行的真假(True/False),返回不同的页面状态(如“存在”与“不存在”、“正常”与“错误”)。
- 攻击逻辑:像“猜数字”一样,通过构造逻辑判断,一位一位地猜解数据。例如:
and ascii(substr(database(),1,1))>100。如果页面返回“正常”状态,说明数据库名第一个字符的ASCII码大于100;否则小于等于100。通过二分法可以快速定位。 - 面试点:效率极低,如何提高效率?二分查找法是必答项。此外,可以问及工具(如sqlmap的
--level和--risk参数对盲注的影响)或脚本自动化。
4. 时间盲注这是最隐蔽的一种。页面无论SQL执行真假,返回的内容都一样。此时,我们通过构造让数据库执行延迟的语句,根据页面响应时间的长短来判断真假。
- 核心函数:MySQL的
sleep()、benchmark();PostgreSQL的pg_sleep();MSSQL的WAITFOR DELAY ‘0:0:5’。 - 攻击示例:
and if(ascii(substr(database(),1,1))>100, sleep(5), 0)。如果第一个字符ASCII码大于100,页面会延迟5秒返回;否则立即返回。 - 面试点:时间盲注最大的挑战是什么?网络延迟的不稳定性。如何规避?多次请求取平均时间,或设置一个较大的时间阈值差。另外,时间盲注速度极慢,在实际HW中,若非必要,通常会优先寻找其他突破口。
5. 堆叠查询注入有些数据库支持一次性执行多条SQL语句,以分号;分隔。如果存在注入点,攻击者可以注入;后接任意SQL语句,如; DROP TABLE users; --,危害极大。
- 支持情况:MySQL的
mysqli_multi_query()函数在某些配置下支持,SQL Server、PostgreSQL普遍支持。PHP+MySQL的mysql_query()函数通常不支持。 - 面试点:堆叠注入与联合注入的区别?联合注入是“扩充查询结果”,而堆叠注入是“执行新的命令”。它的利用更灵活,可以用于增删改查任何操作。
3. 手工注入实战全流程拆解
知道原理还不够,面试官喜欢问过程:“给你一个疑似注入点id=1,你会怎么一步步验证和利用?” 下面我以一个虚拟的GET型参数注入为例,拆解完整的手工流程。假设后端是MySQL数据库。
3.1 第一步:探测与确认注入点
目标URL:http://target.com/news.php?id=1
- 初步试探:在参数后添加一个单引号
‘。- 访问:
http://target.com/news.php?id=1‘ - 观察:如果页面出现数据库错误(如MySQL的
You have an error in your SQL syntax),说明可能存在注入,且未过滤单引号。如果页面空白或跳转404,也可能存在注入但被处理了,需要进一步测试。
- 访问:
- 逻辑测试:利用
and 1=1和and 1=2进行布尔逻辑判断。id=1 and 1=1-> 页面应正常显示(因为1=1永真,SQL语句整体为真)。id=1 and 1=2-> 页面应显示异常(无数据、空白或与上一步不同,因为1=2永假)。- 如果两者返回结果明显不同,则基本确认存在布尔型注入。
- 注释符测试:判断注入点是否在语句中间,以及注释符是否生效。
id=1‘ and ‘1’=‘1-> 如果正常,说明需要用单引号闭合。id=1‘ --+-> 如果正常,说明--(空格)注释掉了后面的语句。+在URL中代表空格。也可以用#(URL编码为%23)。- 实操心得:
--后面必须跟一个空格,否则可能注释失败。在浏览器URL中,空格会被编码,所以常用--+,因为+被服务器解码为空格。#在URL中需要编码为%23,否则会被当作锚点。
3.2 第二步:判断数据库类型与获取基本信息
确认注入后,首先要判断是什么数据库,因为不同数据库的语法、函数差异很大。
- 数据库版本:
- MySQL:
id=1‘ and @@version>0 --+或id=1‘ union select 1,version(),3 --+ - MSSQL:
id=1‘ and @@version>0 --+ - Oracle:
id=1‘ and (select banner from v$version where rownum=1) is not null --+
- MySQL:
- 当前数据库用户与库名:
id=1‘ union select 1,user(),database(),4 --+(假设有4个回显列)user()返回当前数据库连接用户,database()返回当前使用的数据库名。知道库名是后续查表的前提。
- 判断列数(为UNION注入做准备):
- 使用
order by二分法:id=1‘ order by 5 --+,如果页面正常,说明至少有5列;然后order by 10,如果报错,则列数在5-10之间,逐步缩小范围,直到找到精确列数N(order by N正常,order by N+1报错)。 - 注意事项:
order by后面的数字代表按第几列排序,数字不能超过实际列数,否则语法错误。这是判断列数最可靠的方法。
- 使用
3.3 第三步:利用UNION注入获取数据
假设我们通过order by判断出有4列,并找到了回显点在页面的第2和第3列。
- 爆出所有数据库名:
id=-1‘ union select 1,group_concat(schema_name),3,4 from information_schema.schemata --+- 解释:
id=-1是为了让原查询不返回结果,使得页面只显示我们union查询的结果。information_schema.schemata是MySQL的系统表,存放所有数据库信息。group_concat()函数将多行结果合并成一个字符串,方便查看。
- 爆出指定数据库(假设为‘app_db’)的所有表名:
id=-1‘ union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=‘app_db’ --+- 这里可能会得到
users,admin,products,orders等表名。我们通常对users或admin这类表最感兴趣。
- 爆出指定表(假设为‘admin’)的所有列名:
id=-1‘ union select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=‘app_db’ and table_name=‘admin’ --+- 可能会得到
id,username,password,email等列名。
- 最终:拖取数据:
id=-1‘ union select 1,concat(username, ‘:’, password),3,4 from app_db.admin --+- 这样就能一次性把管理员账号和密码(可能是明文,也可能是哈希值)都查出来。
重要技巧:
information_schema数据库是SQL注入的“百科全书”,在MySQL、MSSQL(略有不同)、PostgreSQL中都有类似功能的系统视图。掌握如何查询schemata、tables、columns这几个核心表,是手工注入的基本功。
3.4 盲注场景下的数据提取
如果页面没有回显,我们就进入“盲猜”模式。以布尔盲注为例,目标是获取数据库名。
- 猜解数据库名长度:
id=1‘ and length(database())=8 --+- 通过变换数字,直到页面返回“正常”状态,假设结果是8,说明库名长度8位。
- 逐位猜解数据库名:
- 猜第一位:
id=1‘ and ascii(substr(database(),1,1))>100 --+,根据页面真假,用二分法(>100? >150? ...)快速定位其ASCII码。假设得到97,对应字母‘a’。 - 猜第二位:
id=1‘ and ascii(substr(database(),2,1))>100 --+,重复此过程。 - 自动化:这个过程极其繁琐,必须借助工具或脚本。面试时你需要说明这个原理,并提到可以用Python写一个循环脚本,或者直接使用sqlmap的
--technique=B参数。
- 猜第一位:
4. 高级绕过技巧与WAF对抗
现在的应用多少都有点防护,直接上单引号可能就被WAF(Web应用防火墙)拦了。面试官最爱问的就是:“如果遇到WAF,你怎么绕过?” 这里需要分层次思考。
4.1 基于关键词混淆的绕过
WAF通常基于正则表达式匹配危险关键词(如union,select,sleep,or等)。混淆的目的就是让我们的payload“看起来”不像这些关键词。
- 大小写混合:
UnIoN SeLeCt。一些简单的WAF规则可能只匹配全小写。 - 双写关键词:
uniunionon selselectect。如果WAF采用简单替换删除策略(如把union替换为空),那么删除后剩下的字符正好拼成union。 - 插入注释/空白符:MySQL中,注释
/**/可以插在关键词中间。u/**/nion sele/**/ct- 也可以用
%0a(换行符)、%0d(回车符)、%09(制表符)等URL编码的空白符:u%0anion%0dselect。
- 使用等价函数或操作符:
or 1=1可以换成or 1 like 1、or 1 regexp 1、or 1 between 0 and 2。sleep(5)可以换成benchmark(10000000, md5(‘test’)),通过大量计算来延时。
- 编码绕过:
- 十六进制编码:
select->0x73656c656374。在MySQL中,union select 1,2可以写成union 0x73656c656374 1,2。对于库名、表名、列名,用十六进制表示常常能绕过字符串检测。 - URL编码:对payload整体或部分进行二次、三次URL编码,可能绕过一些简单的解码层检测。
- 十六进制编码:
4.2 基于特殊场景的绕过
- 参数污染:当服务器接受多个同名参数时(如
id=1&id=2),不同中间件/后端处理逻辑不同。可能WAF检查第一个id=1,而后端实际使用的是最后一个id=2 union select...。 - HTTP参数污染:将payload拆散放到不同的HTTP参数或位置,如放在
Cookie、User-Agent、X-Forwarded-For头中,如果后端程序不规范地从这些地方取参数,而WAF只检查了GET/POST,就可能绕过。 - 分段传输:利用
Transfer-Encoding: chunked,将payload拆分成多个小块传输,可能绕过一些基于完整包检测的WAF。 - 非常规请求方式:WAF可能只防护了
GET和POST,尝试用PUT、DELETE等方法提交数据,或许有奇效。
4.3 云WAF与动态Payload生成
面对像阿里云盾、腾讯云WAF、Cloudflare这样的云WAF,它们往往有智能语义分析和机器学习模型。硬碰硬地混淆可能效果有限。
- 思路转变:从“绕过检测”变为“让检测失效”。
- 慢速攻击:极慢地发送HTTP请求,每次只发几个字节,延长请求时间到几分钟甚至更长。有些云WAF有超时机制,超时后可能会放行请求到源站。
- 利用数据库特性生成动态Payload:这是高阶技巧。例如,在MySQL中:
select {schema_name} from {information_schema}.{schemata}。这里用反引号包裹的标识符,在SQL中是合法的,但WAF可能难以识别。- 利用
concat()函数动态拼接关键词:id=1‘ and (select ‘sel’ ‘ect’) = ‘select’ --+。或者更复杂的:id=1‘ and (select mid(‘unionselect’,1,5)) = ‘union’ --+。
- 实战心得:面对高级WAF,手工fuzz效率很低。通常我会先用sqlmap的
tamper脚本(如charencode.py,space2comment.py,randomcase.py)进行自动化测试,观察哪些脚本能成功,再分析其原理,用于手工精调。永远不要依赖单一的绕过方法,组合拳才是王道。
5. 防御体系构建:从代码到架构
“如何防御SQL注入?” 这是面试的终极问题。你不能只说“用预编译”,那太初级了。一个合格的回答应该体现纵深防御的思想。
5.1 根本大法:参数化查询
这是唯一被证明能从根本上杜绝SQL注入的方法。原理就是我们第一节讲的:预先定义好SQL语句的结构,用户输入只作为参数传入,不会被解析为SQL代码。
- Java (PreparedStatement):
String sql = “SELECT * FROM users WHERE username = ? AND password = ?”; PreparedStatement stmt = connection.prepareStatement(sql); stmt.setString(1, username); // 安全,即使username包含‘ or ‘1’=‘1 stmt.setString(2, password); ResultSet rs = stmt.executeQuery(); - Python (DB-API):
cursor.execute(“SELECT * FROM users WHERE username = %s AND password = %s”, (username, password)) - PHP (PDO):
$stmt = $pdo->prepare(“SELECT * FROM users WHERE username = :user AND password = :pass”); $stmt->execute([‘:user’ => $username, ‘:pass’ => $password]);
核心要点:参数化查询起作用的关键在于,数据库驱动(如mysql-connector-java, pymysql, PDO_MYSQL)在底层实现了真正的“数据与代码分离”。它发送给数据库的是两个独立的部分:1. 带占位符的SQL模板;2. 参数值列表。数据库先编译模板,再代入值,因此参数中的SQL语法绝不会被执行。
5.2 辅助措施:输入验证与输出编码
参数化查询是核心,但其他措施能提供额外保护层。
- 白名单验证:对于已知有限集合的输入(如状态status=‘active’/‘inactive’,类型type=1/2/3),使用白名单是最严格的。
if (!in_array($status, [‘active‘, ‘inactive’])) { die(‘Invalid status’); }
- 类型强制转换:对于数字型参数,在拼接SQL前,强制转换为整数/浮点数。
$id = (int)$_GET[‘id’]; // 非数字会变成0- 这能有效防御数字型注入,但绝不能替代字符串参数的参数化查询。
- 最小权限原则:连接数据库的应用程序账号,不应使用
root或dbo等高权限账户。应为其创建仅具备必要权限(如SELECT,INSERT在特定表上)的专用账户。这样即使发生注入,攻击者也无法执行DROP TABLE,LOAD_FILE,INTO OUTFILE等高危操作。 - 存储过程:将SQL逻辑封装在数据库的存储过程中,应用程序只调用存储过程并传参。这也能起到隔离作用,但存储过程本身如果使用动态SQL拼接,依然存在注入风险,所以存储过程内部也应使用参数化查询。
- Web应用防火墙:在应用层前部署WAF,可以拦截大量已知的、模式化的攻击payload,作为一道有效的缓冲带。但它不是银弹,可能存在误报、漏报,且无法防御未知的或精心构造的绕过攻击。
- 错误信息处理:绝对不要将详细的数据库错误信息直接返回给前端用户。应使用自定义的、模糊的错误页面(如“服务器内部错误”),同时在后台记录详细的错误日志供开发者排查。这能极大增加攻击者进行错误注入的难度。
5.3 安全开发流程SDL集成
在团队和项目层面,防御应该前置。
- 安全编码规范:将“禁止使用字符串拼接SQL”、“必须使用参数化查询或ORM框架的安全方法”写入开发规范。
- 代码审计与自动化扫描:在代码提交(Git Hook)或持续集成(CI)流程中,集成SAST(静态应用安全测试)工具,自动扫描源代码中的SQL拼接风险点。
- 定期安全培训:让每一位开发人员都理解SQL注入的原理和危害,知道正确的防御方法。
- ORM框架的正确使用:像Hibernate、MyBatis、Eloquent ORM这样的框架,如果使用不当(如MyBatis的
${}拼接),依然会产生注入。必须确保开发者使用的是安全的#{}(参数化)语法,而非不安全的${}(拼接)语法。
6. 实战案例与深度问题剖析
面试官可能会追问一些基于真实场景的深度问题,考察你的实战经验和应变能力。
6.1 二次注入:潜伏的杀手
这是非常经典且容易被忽略的高危漏洞。
- 场景:一个用户注册功能,对输入的用户名做了严格的转义(如将
‘转义为\’),然后存入数据库。后来在另一个“修改昵称”的功能里,程序从数据库里取出这个用户名(此时存储的是转义后的\’),未经再次过滤,就直接拼接到SQL语句中执行。当从数据库取出时,转义符\会被解释掉,\’又变回了单引号‘,从而引发注入。 - 攻击链:
- 注册用户,用户名为
admin‘ --(注意,这里的单引号被转义为\’存储)。 - 登录后,进入修改密码功能,该功能执行的SQL是:
UPDATE users SET password=‘new_pass’ WHERE username=‘$username’。 - 从数据库取出的
$username值是admin‘ --。拼接后SQL变为:UPDATE users SET password=‘new_pass’ WHERE username=‘admin‘ -- ’。 - 注释符
--生效,语句变成了UPDATE users SET password=‘new_pass’ WHERE username=‘admin‘,成功修改了管理员admin的密码。
- 注册用户,用户名为
- 防御:所有从外部(包括数据库、文件、网络)获取的数据,在进入SQL执行前,都应视为不可信的,必须经过参数化查询处理。不能因为数据是“自己存进去的”就放松警惕。
6.2 宽字节注入:转义函数的“魔咒”
主要发生在使用GBK、GB2312等宽字符集,且使用addslashes()或mysql_real_escape_string()(在特定配置下)进行转义的PHP环境中。
- 原理:转义函数会在单引号
‘前加反斜杠\,变成\’。但GBK编码中,0xbf27不是一个合法字符,0xbf5c却代表一个繁体字“誠”。如果我们在0xbf后面输入一个单引号‘(0x27),转义后变成0xbf5c27。当数据库以GBK解读时,会将0xbf5c解析为“誠”,而0x27(单引号)则被孤立出来,成功闭合了前面的字符串,导致注入。 - Payload示例:
id=%bf%27 or 1=1 --+ - 防御:
- 统一使用UTF-8编码,避免多字节字符集问题。
- 使用参数化查询(PDO/mysqli),这是终极解决方案。
- 在PHP中,设置
mysql_set_charset(‘gbk’)或使用mysqli::set_charset(),配合mysql_real_escape_string(),可以在一定程度上修复此问题,但不如方案1和2彻底。
6.3 SQLMap核心参数与使用技巧
在HW中,时间紧迫,我们不可能所有点都手工测试。sqlmap是必备神器。面试官可能会问:“你平时用sqlmap哪些参数比较多?怎么判断一个点能不能用sqlmap跑?”
- 基础探测:
-u “URL”: 指定目标。--batch: 自动选择默认选项,非交互模式。--level和--risk: 调整测试的深度和风险等级。对于可疑点,我通常会从--level 2 --risk 2开始。
- 注入技术指定:
--technique=BEUSTQ: 指定注入技术(B布尔盲注,E报错注入,U联合查询,S堆叠查询,T时间盲注,Q内联查询)。如果手工确认了是时间盲注,可以直接用--technique=T提高效率。
- 绕过WAF:
--tamper=space2comment,between: 使用tamper脚本混淆payload。常用的有space2comment(空格转注释)、randomcase(随机大小写)、charencode(URL编码)。--delay=1: 设置每次请求的延迟,避免触发WAF的速率限制。--time-sec=5: 设置时间盲注的延迟时间,根据网络情况调整。
- 数据获取:
--dbs: 枚举数据库。-D dbname --tables: 枚举指定数据库的表。-D dbname -T tablename --columns: 枚举指定表的列。-D dbname -T tablename -C “col1,col2” --dump: 拖取指定列的数据。
- 高阶技巧:
--os-shell: 尝试获取操作系统shell(需满足数据库是高权限、有写权限等苛刻条件)。--sql-query=“SELECT user()”: 执行自定义SQL语句。- 使用心得:不要一上来就
--dbs。先加--batch --flush-session快速跑一下,看是否能检测到注入。对于有WAF的点,先用手工方式确认注入存在并找到可用的绕过方法(如特定的注释符、编码方式),再将这些信息通过--prefix、--suffix、--tamper参数告诉sqlmap,能极大提高成功率。另外,--proxy参数设置代理,方便在Burp Suite中观察sqlmap发出的payload,对于学习绕过和调试非常有用。
7. 面试高频问题精炼与回答思路
最后,我整理了一份清单,涵盖了从初级到高级的常见面试题,并附上我认为比较出彩的回答要点。
| 问题类别 | 典型问题 | 回答要点与深度扩展 |
|---|---|---|
| 基础原理 | 1. 什么是SQL注入? | 核心:数据与代码未分离。举例:SELECT * FROM users WHERE id=‘“ + input + ”’。危害:数据泄露、篡改、删库、getshell。 |
| 2. SQL注入有哪些类型? | 分类维度:按回显方式(报错、联合、布尔、时间),按数据库操作(查询、堆叠)。重点区分布尔与时间盲注的适用场景。 | |
| 手工利用 | 3. 给你id=1,如何手工判断注入? | 步骤化:单引号测 -> 逻辑(and 1=1/1=2)测 -> 注释符测。强调观察:页面内容变化、错误信息、响应时间。 |
| 4. 如何获取数据库名、表名、列名? | 必答:information_schema库。举例:union select group_concat(table_name) from information_schema.tables where table_schema=database()。 | |
| 防御手段 | 5. 如何防止SQL注入? | 第一答案:参数化查询(预编译)。展开:说明原理(代码/数据分离)。补充:输入验证(白名单)、最小权限、错误处理、WAF。切忌:只说“过滤”或“转义”。 |
| 6. 预编译语句为什么能防注入? | 底层原理:数据库引擎分两步处理:1. 编译带占位符的SQL结构;2. 将参数值作为纯数据绑定。参数中的SQL语法在第一步编译时未被解析,故无法执行。 | |
| 进阶绕过 | 7. 如何绕过WAF? | 分层回答: 1.混淆:大小写、双写、注释符、编码(十六进制、URL)。 2.等价替换: or 1=1->or 1 like 1;sleep()->benchmark()。3.特殊场景:参数污染、HTTP头注入、分块传输。 4.云WAF:慢速攻击、动态payload拼接。 |
| 8. 什么是宽字节注入? | 场景:GBK编码 +addslashes。原理:利用编码特性“吃掉”转义的反斜杠。防御:使用UTF-8;正确设置字符集;参数化查询。 | |
| 实战经验 | 9. 你在实际项目中怎么发现/利用SQL注入的? | 讲故事:从黑盒(Burp Suite fuzz参数,观察异常)、白盒(代码审计找拼接点)两个角度举例。提工具:sqlmap的--tamper、--delay参数在实战中的调整。 |
10. 除了information_schema,还有其他方式获取表结构吗? | MySQL:sys库(5.7+)。盲注场景:通过select count(*) from guessed_table_name的错误或布尔状态来猜解表名和列名(非常耗时)。 | |
| 深度思考 | 11. ORM框架一定安全吗? | 不一定。举例MyBatis的${}是拼接,#{}才是参数化。Hibernate的HQL如果拼接字符串,同样存在注入。安全取决于用法。 |
| 12. 时间盲注如何对抗网络延迟? | 多次请求取平均;设置合理的time-sec(如5秒);使用benchmark替代sleep(计算型延迟受服务器负载影响小,但更耗资源)。 |
把这些点吃透,面试时关于SQL注入的问题,你就能做到心中有数,对答如流了。记住,面试官想看到的不仅是你知道这个知识点,更是你理解其背后的原理、有过实战的体会、并具备在受限环境下解决问题的思路。安全是一个持续对抗的过程,SQL注入作为Web安全的“活化石”,它的攻防演进史,本身就是一部浓缩的Web安全史。