应重点查询 sys.dm_exec_query_stats、sys.dm_exec_sql_text 和 sys.dm_exec_requests 三者联动结果,尤其关注 query_hash 高频重复但 sql_handle 不同的即席查询,结合 execution_count > 50、逻辑读/执行次数 > 1000、plan_handle IS NULL 等特征识别异常 SQL 行为。

查哪些 DMV 能暴露异常 SQL 行为
SQL Server 自带的动态管理视图(DMV)本身不标记“这是注入”,但能暴露典型攻击痕迹:短生命周期、高频率、结构异常的查询。关键目标是捕获那些 sys.dm_exec_query_stats + sys.dm_exec_sql_text + sys.dm_exec_requests 三者联动的结果,尤其是 query_hash 高频重复但 sql_handle 不同的语句——这往往意味着攻击者在遍历参数(如 1 OR 1=1、1 AND 1=2)。
常见错误现象:DBA 只查 sys.dm_exec_requests 看当前运行语句,却忽略已执行完但高频出现的恶意模式;或只用 text 字段查关键字,漏掉被注释符(--)、空格/换行/Unicode 混淆绕过的变体。
- 优先组合查询:
sys.dm_exec_query_stats过滤execution_count > 50且total_logical_reads / execution_count > 1000(单次读取量大,可能含子查询探测) - 关联
sys.dm_exec_sql_text(sql_handle)时,用SUBSTRING(text, 1, 400)截取开头,避免因长文本拖慢查询 - 警惕
plan_handle IS NULL的记录:说明该语句未生成执行计划,多为一次性即席查询(Adhoc),注入语句常见于此
如何识别字符型 vs 数字型注入痕迹
字符型注入常在 WHERE 条件中插入单引号破坏语法,SQL Server 会报错并记录到 sys.dm_exec_query_stats 的 last_worker_time 和 last_elapsed_time 异常抖动;而数字型注入(如 id=123; DROP TABLE users--)更隐蔽,靠分号分隔多语句,需重点检查 sys.dm_exec_sessions 的 is_user_process = 1 + last_request_end_time 接近但 status = 'sleeping' 的会话——说明它发了多语句,前半句执行完就挂起,后半句可能已生效。
使用场景:当 Web 日志显示大量 500 错误且 URL 含 ' 或 --,但数据库没开 user options 的 ANSI_WARNINGS,错误可能被静默吞掉,此时必须依赖 DMV 中 creation_time 与 last_execution_time 时间差突增来定位。
- 字符型线索:从
sys.dm_exec_sql_text提取的语句中匹配'%''%'(两个连续单引号)或结尾带--且无后续有效 SQL - 数字型线索:检查
sys.dm_exec_query_stats中statement_start_offset和statement_end_offset是否指向同一语句内多个分号分隔块(需配合sys.dm_exec_sql_text解析) - 避免误杀:不要仅凭含
UNION或SELECT就判定为注入——合法报表查询也常用这些关键词
为什么不能只依赖 sys.dm_exec_cached_plans
sys.dm_exec_cached_plans 存的是已编译计划缓存,而多数 SQL 注入语句走的是即席编译(Adhoc),尤其带随机字符串或注释时,SQL Server 默认不缓存它们(除非启用 optimize for ad hoc workloads)。这意味着你查缓存,很可能什么也看不到。
性能影响:直接 SELECT * FROM sys.dm_exec_cached_plans 会触发全缓存扫描,锁住计划缓存结构,高峰期可能加剧阻塞。真实排查应先用 sys.dm_exec_query_stats 找出可疑 plan_handle,再按需 JOIN 查询缓存项。
- 正确做法:用
sys.dm_exec_query_stats的plan_handle去 LEFT JOINsys.dm_exec_cached_plans,而非反向操作 - 兼容性注意:SQL Server 2016+ 支持
sys.dm_exec_query_plan_stats(实际执行计划快照),但默认关闭,需开启QUERY_PLAN_CAPTURE_MODE数据库范围配置 - 容易踩的坑:把
cacheobjtype = 'Compiled Plan'当作“所有计划”,其实Adhoc计划类型是'Compiled Plan Stub',会被过滤掉
结合登录与会话上下文锁定攻击源
单看 SQL 文本不够,必须绑定到具体会话和登录名。sys.dm_exec_sessions 的 login_name 和 host_name 是关键,但要注意:Web 应用通常用统一账号连接数据库(如 app_user),此时需结合 sys.dm_exec_connections 的 client_net_address 和 most_recent_sql_handle 关联原始请求 IP。
容易被忽略的点:SQL Server 默认不记录客户端程序名(program_name),但若应用层设置了 Application Name=xxx 在连接字符串里,这里就能看到真实来源(如 WebApp-Login 或 curl/7.68.0)。
- 实操建议:写一个定时作业,每 5 分钟跑一次以下逻辑:JOIN
sys.dm_exec_requests→sys.dm_exec_sessions→sys.dm_exec_connections→sys.dm_exec_sql_text,WHEREstart_time > DATEADD(mi, -5, GETDATE()),INSERT 到审计表 - 别信
original_login_name:它可能被EXECUTE AS伪造,login_name才是实际认证身份 - 如果发现多个不同
client_net_address共享同一个login_name且都执行高度相似的畸形语句,基本可确认是自动化扫描工具
真正难的不是查到某条语句像注入,而是区分它是测试脚本、运维误操作,还是真实攻击流量。DMV 给的是“证据链”,不是“判决书”——得把时间戳、IP、登录名、执行频次、错误模式、前后语句变化串起来看。漏掉任意一环,都可能把开发查问题的调试语句当成攻击,或者放过正在执行二阶注入的 sleeping 会话。

















