数据库存储过程(Stored Procedure)在常规认知里是封装业务逻辑、提升性能的利器,但在红蓝对抗和权限审计的视角下,它往往是数据库提权攻击链条中最隐蔽的一环。攻击者一旦通过SQL注入或弱口令拿下了一个低权限数据库账户,他们的下一个目标往往不是直接读表,而是寻找那些拥有高权限定义者(Definer)的存储过程,利用其内部的逻辑缺陷或SQL注入漏洞,从普通用户一跃成为数据库管理员甚至操作系统控制者。

权限上下文切换:SQL SECURITY陷阱

存储过程之所以能用于权限提升,核心在于创建时的SQL SECURITY属性。这个属性有两个值:DEFINER和INVOKER。默认情况下,如果不显式指定,MySQL等数据库通常默认为DEFINER。这意味着当一个只拥有基础查询权限的用户调用该存储过程时,存储过程体内的代码是以创建者(Definer)的权限执行的。如果创建者是root或具有高权限的运维账户,攻击者就相当于借用了一把尚方宝剑。

审计时,第一个要点就是排查所有非系统存储过程的定义者身份。不要只看表面,要深入到元数据表。在MySQL中,可以通过查询information_schema.ROUTINES来获取精确数据。执行以下语句,重点关注那些DEFINER为root或高权限账号,且SQL SECURITY为DEFINER的存储过程:

SELECT 
    ROUTINE_SCHEMA,
    ROUTINE_NAME,
    DEFINER,
    SECURITY_TYPE
FROM 
    information_schema.ROUTINES
WHERE 
    SECURITY_TYPE = 'DEFINER' 
    AND DEFINER NOT LIKE 'mysql.sys%'
    AND ROUTINE_TYPE = 'PROCEDURE';

拿到这份清单后,需要逐一审查这些存储过程的业务逻辑。如果一个低权限用户(如仅拥有EXECUTE权限)能够触发一个由root创建的存储过程,且该过程内部执行了系统命令调用或修改用户权限表的操作,这就是一个典型的权限提升通道。

SQL注入的二次利用:存储过程内部的动态SQL

很多开发人员认为存储过程是预编译的,天然防注入,这是一个极其危险的误解。存储过程内部如果使用了动态SQL拼接,且拼接的字符串来源于外部传入的参数,那么SQL注入依然存在。更可怕的是,这种注入发生在高权限定义者的上下文中。

审计时,必须对存储过程体进行静态代码扫描。重点搜索PREPARE、EXECUTE IMMEDIATE(MySQL)、EXEC(SQL Server)等动态执行关键字。例如在MySQL中,如果看到这样的代码逻辑,就需要拉响警报:

CREATE PROCEDURE search_data(IN table_name VARCHAR(64))
SQL SECURITY DEFINER
BEGIN
    SET @sql = CONCAT('SELECT * FROM ', table_name);
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END

攻击者调用search_data时,传入的参数如果是"users; DROP TABLE users; -- ",虽然存储过程执行的是SELECT,但利用堆叠查询或者构造子查询,完全可以实现越权操作。更隐蔽的利用方式是通过构造恶意参数去调用那些原本只有高权限才能访问的系统函数,比如MySQL中的load_file或sys_exec(如果数据库开启了不安全的UDF)。审计人员需要模拟恶意输入,测试参数是否被严格过滤或是否使用了白名单机制。

UDF与系统命令执行的暗门

在MySQL中,如果存储过程的定义者拥有FILE权限或系统管理权限,攻击者往往会利用存储过程来创建用户自定义函数(UDF),从而执行操作系统命令。审计时,要特别关注存储过程体内是否包含对mysql.func表的操作,或者是否出现了so文件(动态链接库)的写入操作。

一个典型的恶意利用路径是:攻击者发现了一个DEFINER为root的存储过程,且该过程内部允许通过动态SQL写入文件。攻击者会利用SELECT ... INTO DUMPFILE语句,将一段编译好的恶意UDF动态库二进制代码写入到插件目录下,然后通过存储过程执行CREATE FUNCTION命令加载该UDF。一旦加载成功,攻击者就获得了通过数据库执行系统命令的能力。

审计要点不仅仅是看代码,还要检查数据库的secure_file_priv参数设置。如果该参数为空或指向了一个可写的目录,且存在高权限定义者的存储过程,风险等级就是严重。检查插件目录路径可以通过"show variables like '%plugin%';"获取,然后反向排查哪些存储过程具有写入该路径的潜在能力。

基于游标和异常处理的隐蔽提权

高水平的攻击者不会直接使用明显的系统命令执行函数,而是利用存储过程的高级特性,比如游标(Cursor)和异常处理(Handler)来绕过审计。例如,在SQL Server中,攻击者可能利用存储过程内的游标去遍历系统视图,提取敏感信息;或者构造特定的异常处理块,在异常发生时执行高权限操作。

审计这类存储过程时,不能只看主逻辑,必须仔细分析DECLARE HANDLER部分。有些开发人员为了调试方便,会在异常处理块中记录日志,而日志表可能涉及敏感操作。攻击者可以故意触发异常,使得存储过程进入异常处理分支,执行非预期的代码。例如,故意传入超长字符串导致截断错误,进而触发一个存在漏洞的修复逻辑。审计人员需要绘制存储过程的控制流图,识别所有可能的执行路径。

Oracle与SQL Server的特有审计视角

不同数据库的存储过程提权手法虽有共性,但审计侧重点不同。在Oracle数据库中,重点审计AUTHID属性。如果存储过程创建时使用了AUTHID DEFINER,且该用户拥有DBA角色或特定的系统权限,低权限用户调用时就能继承这些权限。Oracle中有一个经典的审计盲区:角色权限在存储过程中默认是失效的,除非显式授予直接权限。但很多DBA为了方便,直接授予了DBA角色给存储过程所有者,这就导致提权风险。

对于SQL Server,重点在于“所有权链接”。如果存储过程和它引用的表属于同一个所有者,SQL Server不会检查调用者对底层表的权限。这意味着如果一个低权限用户能执行一个高权限用户拥有的存储过程,且该过程操作了同属主的高敏感表,权限检查就被绕过了。审计时需要打破这种所有权链条的假设,检查跨数据库的权限配置。

审计日志的盲区与取证

单纯依赖数据库自带的审计日志往往不够。很多数据库默认不记录存储过程内部的详细执行步骤,只记录调用事件。这就导致攻击者在存储过程内部进行了100次恶意操作,日志里可能只有一条CALL语句。审计人员必须开启更细粒度的审计策略。

在MySQL中,建议开启general_log(虽然性能影响较大,但在应急响应期间是必要的)或使用审计插件记录所有SQL语句。在分析日志时,要建立时间线关联:一个低权限用户在非业务高峰期突然调用了某个高权限存储过程,紧接着出现了权限变更或文件读写操作,这就是铁证。此外,存储过程的最后修改时间也是关键指标,攻击者为了持久化,可能会修改原有的存储过程,插入恶意代码。审计人员需要对比源代码仓库中的存储过程定义与线上数据库的当前定义是否一致。

防御性编码与权限最小化审计

从审计视角转向防御,核心原则是权限最小化。审计报告不仅要指出漏洞,还要给出修复标准。对于所有业务存储过程,应强制要求显式声明SQL SECURITY INVOKER,除非有极其充分的理由需要DEFINER权限。对于必须使用DEFINER的场景,应创建一个权限严格受限的专用账户作为定义者,而不是直接使用root或系统管理员账户。

在代码层面,审计标准应规定:存储过程内部严禁使用未经参数化处理的动态SQL。如果业务确实需要动态排序或动态表名,必须使用白名单机制。例如,传入的表名参数必须在存储过程内部与预定义的合法表名列表进行比对,比对不通过则直接抛出异常。审计人员可以编写自动化脚本,扫描存储过程定义中是否存在字符串拼接函数(如CONCAT、||、+)与EXECUTE关键字同时出现的情况。

对于文件操作类存储过程,如涉及LOAD DATA INFILE或SELECT INTO OUTFILE,应列为高危对象进行专项审查。确认这些过程是否对输入路径进行了严格的目录限制,是否禁用了绝对路径,以及是否在配置层面将secure_file_priv设置为只读目录或完全禁用。

最终,存储过程权限审计不能停留在数据库层面,要结合应用层逻辑。一个普通用户是如何获得该存储过程执行权限的?是否通过Web应用的接口间接调用?如果是,那么Web应用是否存在越权漏洞,允许普通用户调用管理员功能?这种跨层的权限穿透是实战中攻击者最青睐的攻击链。审计人员需要串联应用日志与数据库日志,确认调用来源的合法性。只有将存储过程视为一个需要严格管控的高权限执行单元,而不是一段普通的业务代码,才能有效遏制这种隐蔽的提权攻击。