ARTICLE DETAIL

资讯详情

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

pg_cron 高危漏洞解析:PostgreSQL 定时任务扩展的权限提升风险

pg_cron 高危漏洞解析:PostgreSQL 定时任务扩展的权限提升风险 这阵子在复盘一个数据库安全应急项目内部编号 HGVE-2024-E001对应公开漏洞 CVE-2024-0985。刚拿到这个编号时我第一反应是“又一个 PostgreSQL 内核级问题”但顺着链路排查下去才发现真正的突破口根本不在数据库内核而在很多团队会顺手安装的定时任务扩展 pg_cron 上。这个漏洞的杀伤力非常直接一个只有低权限的数据库账号可以借助它拿到数据库超级用户权限影响面比很多人想象中大得多。如果你正在用或者准备用 pg_cron 来管理 PostgreSQL 的定时任务我建议你花二十分钟把这篇看完。我会把漏洞原理、自查步骤、修复命令和应急排查经验一次性讲清楚。这篇文章不是教你复现攻击而是让你在最短时间里知道该检查哪些地方以及为什么这些检查项能挡住问题。1. 漏洞还原一个定时任务扩展引发的权限危机1.1 pg_cron 到底是个什么组件pg_cron 是 PostgreSQL 社区非常流行的定时任务扩展核心作用是把原本放在操作系统 crontab 里的定时任务直接挪进数据库里管理。开发者和 DBA 可以通过 SQL 创建任务、查看执行状态、修改调度时间不需要登录服务器去改 crontab也不需要额外部署独立调度服务。它的使用方式很直白启用扩展后写一条 INSERT 或调用函数就能注册任务。比如最常见的日志清理CREATE EXTENSION IF NOT EXISTS pg_cron; SELECT cron.schedule( daily-log-cleanup, 0 3 * * *, $$ DELETE FROM app_logs WHERE created_at now() - interval 7 days $$ );第一个参数是任务名第二个是 cron 表达式第三个是真正要执行的 SQL。pg_cron 会启动一个后台 worker 进程按照计划时间逐条取任务、执行 SQL。正因为这套设计足够轻量很多生产环境都在用尤其适合做数据清理、定期刷新物化视图、生成统计报表这类运维型工作。但问题也恰恰出在这个“轻量”上。pg_cron 内部有自己的一组表和函数而这些对象在创建扩展时的默认权限并不像很多人以为的那样“只有超级用户能碰”。1.2 CVE-2024-0985 的核心危害CVE-2024-0985 是 pg_cron 扩展中的安全漏洞官方定性为 SQL 注入漏洞最终可以达到权限提升的效果。CVSS 评分 8.8高危等级。直观地说如果普通数据库账号对 pg_cron 使用的 cron schema 拥有写权限攻击者就可以在任务名jobname这一类原本应该只是“描述字符串”的字段里塞入经过构造的 SQL 片段。这些片段会在调度执行时被拼接进数据库内部要执行的动态 SQL 中并且以扩展加载者的权限上下文运行——在大多数场景下这个上下文就是超级用户。用一个不严谨但好懂的类比你把一张门禁卡交给了保安保安本身只能在门口巡查。结果巡逻路线表里的“备注”栏没有做任何格式校验管理员填表时怎么填巡逻程序就怎么念。如果有人在备注栏写了一句“到 12 楼后拉掉机房的电闸”巡逻程序会照单全收而且是以管理员本人的身份去执行的。CVE-2024-0985 的本质就是这么一条链路用户可控字符串没有经过安全处理被拼接进了高权限上下文中执行。1.3 影响边界谁需要马上关注这个漏洞影响 pg_cron 3.0.0 到 3.1.0 之间的版本官方在 3.1.1 中修复了问题后续的 3.2.0 等版本也包含修复。所以影响范围是明确且有边界的版本状态说明pg_cron 3.0.0 - 3.1.0存在漏洞jobname 等字段存在 SQL 注入风险可能提权pg_cron 3.1.1已修复官方对任务名字段做了过滤和参数化处理pg_cron 3.2.0 及以上已修复包含安全修复与后续功能更新不过“版本在影响范围内”并不等于“一定有风险”。实际可利用还叠加了一个前提条件普通用户必须能对 cron schema 或相关表、函数做写操作。很多生产环境为了方便会把 cron 相关权限放开给业务账号或者干脆在公共 schema 上放得太宽这正是风险最集中的运营场景。换句话说下面三类环境需要最优先排查数据库与应用共用账号业务账号权限远超实际需要多人共用开发测试实例为了“方便排查问题”给了大量写权限把 pg_cron 装在了核心业务库里却没有对扩展对象做权限隔离。2. 技术原理与攻击路径拆解2.1 为什么 SQL 注入会出现在定时任务工具里很多人第一反应是pg_cron 的任务命令不就是 SQL 吗既然最终都要执行 SQL那是不是 SQL 注入本来就无关紧要这里有一个关键区分任务的 command 字段是一条完整的 SQL执行它确实是用户的本意但执行上下文是什么、由谁去执行才是问题所在。在正常设计里普通用户创建一个定时任务是“请调度器帮我执行这段 SQL”但调度器本身没有能力判断“这段 SQL 是否合法”它只会忠实执行。如果调度器在内部实现时连用户提供的任务名都拿来拼接 SQL那就把一个本应友好的任务描述字段变成了注入点。CVE-2024-0985 的问题路径大致如下攻击者先拿到 cron schema 的写权限可以往 cron.job 表插入或更新任务在 jobname 字段中写入恶意构造的字符串利用拼接逻辑闭合掉其他文本插入自己想执行的 SQL调度 worker 在下一个调度周期读取任务时把拼接后的完整 SQL 发送给执行器这段 SQL 以扩展所有者身份执行攻击者因此完成权限提升。我在溯源时最注意的一点是这个漏洞从数据库日志看完全像是“正常的任务调度”。没有新的外部连接没有异常的 SQL 语法错误也不像传统注入那样需要猜接口、遍历参数。它藏在一个本身就拥有高权限执行能力的调度链路里天然很难一眼认出来。2.2 一个完整的利用链路长什么样下面只讲结构不展开成可直接抄的 payload。安全加固的关键在于理解链路而不是背诵某一段恶意字符串。第一步攻击者确认当前账号对 cron 相关表的操作权限以及 pg_cron 的版本第二步攻击者构造一个带有特殊闭合字符和多段函数调用的 jobname写入待执行任务第三步等到调度窗口触发或者想办法手动触发任务执行第四步恶意代码以超级用户权限被执行攻击者在数据库中新建一个超级用户账号、读取全部业务数据或者写入持久后门。整个过程中数据库实际上没有出现“外部网络访问”这恰恰是最容易让人放松警惕的地方。攻击者用的是合法的 SQL 接口、合法的扩展对象、合法的调度机制唯一不合法的是字段里的内容被当成了代码。我在应急分析时有一个经验与其绞尽脑汁研究某个恶意 payload 长什么样不如直接问自己一个问题——普通业务账号到底需不需要对 cron 调度表有写权限绝大多数场景下答案是“不需要”。把这个问题解决掉CVE-2024-0985 的利用条件就直接消失了。2.3 为什么这类漏洞很难被常规扫描发现大部分数据库安全扫描工具关注的是弱口令、空账号、不安全的监听地址、过时的补丁版本。能深入检查“某个扩展内部字段被拼接到动态 SQL”的工具非常少而且就算扫描器把 CVE 编号读出来了到运营环节“确认影响并修复”也需要人工介入。真正导致它隐蔽的还有一层pg_cron 的 jobname 更像一个“配置属性”而不是“用户输入”。传统 Web 注入有 URL 参数、表单请求这类明确入口安全测试会盯着这些入口打。但数据库扩展内部的字段被拼接到 SQL 里几乎没有现成的自动化工具能覆盖。更麻烦的是pg_cron 的 worker 进程在 pg_stat_activity 里显示为数据库连接jobname 字段的内容也不会直接暴露在应用日志里。除非专门去查询 cron.job 表否则恶意内容可能一直安静地躺在那里等到编排好的执行时间点才发作。所以这类漏洞的检测必须主动做不能指望扫描器替你发现。3. 自查与检测从上手到确认3.1 第一步确认 pg_cron 版本最直接的做法是登录到对应数据库查询扩展版本SELECT extversion FROM pg_extension WHERE extname pg_cron;如果返回结果为空有两个可能当前数据库没有安装 pg_cron或者扩展安装在别的数据库里。可以再确认一下当前实例装了哪些可用扩展SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name pg_cron;如果 installed_version 字段有值说明扩展已安装如果只有 default_version 有值说明实例支持安装但当前数据库没装。得到版本号后对号入座3.0.0 到 3.1.0 属于受影响区间建议尽快升级。这里要提一个容易忽略的点PostgreSQL 的版本号与 pg_cron 的版本号不是一回事你看到 SELECT version() 返回的是数据库内核版本不代表扩展版本一定记得分开查。3.2 第二步检查 cron.job 表里有没有异常任务确认版本在影响区间后马上看看任务表里实际有什么SELECT jobid, jobname, schedule, command, username, active FROM cron.job ORDER BY jobid;正常情况下jobname 应该是一段可读的、由字母数字组成的简短描述例如 daily-log-cleanup、refresh_mv_revenue 这类命名风格。如果发现 jobname 里混入了分号、括号、|| 拼接符、-- 注释符、多层函数调用等特征就要高度警惕。可以跑一条简单的正则查询进行快速筛查SELECT jobid, jobname, schedule, command, active FROM cron.job WHERE jobname ~ (;|\|\||\(\)|--);这条查询匹配的是分号、双竖线、空括号对和 SQL 注释符。正常的任务名基本不会出现这些字符一旦命中就该把这行记录单独拎出来和业务方核对。还需要注意的是只看 cron.job 并不完整。pg_cron 在较新版本中还支持通过 cron.schedule 函数注册一次性任务这些任务同样会被写入内部状态表。建议同时查询 cron 相关的全部表不能只盯着任务表看。3.3 第三步做一次权限面体检排查漏洞是否可利用最关键的是看权限分配。查询当前哪些角色对 cron schema 有权限SELECT grantee, privilege_type FROM information_schema.role_table_grants WHERE table_schema cron AND table_name job; SELECT grantee, privilege_type FROM information_schema.schema_privileges WHERE schema_name cron;如果结果显示 PUBLIC 对 cron schema 的任意对象有 INSERT、UPDATE 或 USAGE 权限那么影响面基本拉满了。即使你当前没有发现恶意任务也应该把权限问题当成紧急事项处理。另一种隐蔽情况是单个用户没有被直接授权但用户属于某个业务角色组角色组被授予了权限。PostgreSQL 的权限模型支持角色继承排查时必须把角色树追完整光看当前用户名的 grants 是不够的。3.4 第四步从会话与日志侧找线索任务注入不一定等到你查表才暴露。在确认版本和任务表之前可以先看数据库里有没有正在运行的 pg_cron 相关会话以及它们正在执行什么内容SELECT pid, usename, application_name, backend_type, state, query FROM pg_stat_activity WHERE application_name ILIKE %cron% OR query ILIKE %cron.schedule% OR backend_type ILIKE %cron%;正常由 pg_cron 触发的会话query 内容应该是任务里注册的 SQL。如果看到某个连接在跑与你预期完全无关的 SQL或者频繁出现 CREATE ROLE、ALTER ROLE、COPY TO PROGRAM 之类的敏感语句基本可以认定已经中招。在日志侧如果在 postgresql.conf 里开启了 log_statement可以搜索日志中同一时间段内是否有大量异常 DDL 语句。这里要特别提醒不是所有实例都会开 log_statement如果没开立刻补齐但不要指望它能追溯太久之前的操作。4. 修复与加固完整行动手册4.1 升级到修复版本修复 CVE-2024-0985 最彻底的方式是升级 pg_cron 到 3.1.1 或更高版本。注意这里的升级分两层操作系统/容器内的 pg_cron 二进制文件要先升级然后在 PostgreSQL 里执行扩展更新。如果是通过 apt、yum 或镜像安装了新版 pg_cron在 PG 里执行ALTER EXTENSION pg_cron UPDATE TO 3.1.1;执行后再次确认版本SELECT extversion FROM pg_extension WHERE extname pg_cron;如果返回 3.1.1 或更高版本说明扩展更新完成。升级之前建议在测试环境完整跑一遍迁移流程。pg_cron 的升级脚本会重建内部函数和视图虽然官方脚本通常设计成平滑迁移但生产环境依然可能出现意外比如某个数据库对象被业务方手工修改过导致升级脚本冲突。这里补充一个我踩过的坑只升级了数据库软件包没有在数据库内执行 ALTER EXTENSION UPDATE会导致扩展版本号和实际二进制版本不一致。你在 pg_available_extensions 里看到的是新版本但 installed_version 还是旧的等于白升。升级动作以 ALTER EXTENSION 执行成功为准。4.2 权限收敛最小化 cron schema 暴露面如果因为各种原因不能立刻升级权限收敛是唯一能在几分钟内降低风险的应急手段。核心目标只有一个让普通账号没有写 cron 相关表的权利。最稳妥的做法是收回所有非必要角色的权限REVOKE ALL ON SCHEMA cron FROM PUBLIC; REVOKE ALL ON ALL TABLES IN SCHEMA cron FROM PUBLIC; REVOKE ALL ON ALL SEQUENCES IN SCHEMA cron FROM PUBLIC; REVOKE ALL ON ALL FUNCTIONS IN SCHEMA cron FROM PUBLIC;执行完之后只有超级用户能操作 cron schema。此前依赖普通账号自助创建任务的业务方会开始报错这其实是好事——我们可以借这个机会重建任务管理流程而不是继续容忍一个权限过宽的账号体系。如果确实有少量可信账号需要管理定时任务建议单独创建一个专用任务管理角色只给它最有限的调度权限而不是把整张 cron.job 表的写权限都交出去GRANT USAGE ON SCHEMA cron TO task_admin;这里注意不同版本 pg_cron 暴露的函数、权限模型有差异授权前务必查阅当前版本的官方文档确认最小可用的函数和表。原则是“给到刚好能完成任务的最小集合”一旦超出这个范围就需要重新评估是否真的有必要。4.3 监控告警让风险变成日常可发现升级和收权完成之后还需要建立持续监控否则下个同类漏洞出现时又会措手不及。我建议至少覆盖以下几项cron.job 表的 INSERT、UPDATE、DELETE 操作需要记录并告警尤其关注 jobname、command、username 字段的变更任何对 cron schema 的权限变更需要审计数据库扩展版本出现变化时需要通知尤其是 pg_cron 这类可以加载到共享库的扩展定期执行前文第 3.2 节的正则筛查发现可疑任务名自动推送告警。如果不引入额外审计工具最朴素的做法就是每天早上跑一遍筛查 SQL把异常结果写入一张监控表由外部定时任务读取并推送。虽然实现方式简陋但足以在攻击发生后的第一时间发现问题。如果条件允许可以考虑启用 PostgreSQL 的 pgaudit 扩展结合 pgaudit.log 参数对指定 schema 做细粒度审计。这类方案对运维基础设施要求更高但能够追溯完整的变更历史应急时价值非常大。4.4 应急响应已经中招的处理流程如果排查下来发现已经有恶意任务甚至已经出现了可疑超级用户角色不要急着清数据先按证据固定和止血的顺序处理。第一优先立即冻结权限。执行前面说过的 REVOKE收回所有非超级用户对 cron schema 的写权限切断后续注入路径。第二优先固定现场。把 pg_stat_activity 的快照、数据库日志、cron.job 表内容原样导出保存保留时间线。第三优先确认失陷范围。检查 pg_shadow / pg_authid 中是否有异常新增的超级用户账号检查近期的 DDL 日志是否有 CREATE ROLE、CREATE FUNCTION、COPY TO PROGRAM 等敏感语句。第四优先清理恶意对象。确认某条 cron.job 记录确认为恶意之后做好备份再删除DELETE FROM cron.job WHERE jobid 目标jobid;最后修改所有高权限账号的密码并对整个实例做一次全面的权限审计。这里要强调一个原则任何时候都不要在没有备份和确认的情况下直接删除看起来可疑的任务。如果这条任务是业务方合法创建的只是命名风格比较奇怪误删会影响生产调度造成二次事故。5. 复盘与个人经验5.1 现场复盘清单把这次应急排查的要点整理成一张可复用的清单方便后续遇到同类问题直接照单操作检查项操作方式状态扩展版本SELECT extversion FROM pg_extension WHERE extname pg_cron待确认任务内容查询 cron.job正则筛查 jobname待确认schema 权限information_schema.role_table_grants 查看 cron schema待确认会话活动pg_stat_activity 查看 cron worker 会话待确认日志审计检查 log_statement 是否开启审计 DDL 日志待确认高权限账号检查 pg_authid 中是否出现异常超级用户待确认升级窗口制定 ALTER EXTENSION 升级计划待执行权限收口REVOKE cron schema 上多余权限待执行这张表可以直接贴到团队文档里作为标准操作项。建议最终形成每季度巡检的固定任务而不是只在这一轮应急里使用。5.2 排查中容易踩的坑这一轮排查里我见过几个特别典型的误判值得单独写出来提醒。第一个误判是“pg_cron 只装在 postgres 库里所以业务库是安全的”。这个想法忽略了 PostgreSQL 跨库调用的复杂性也忽略了扩展本身可能注册在多个数据库中。排查时必须逐个数据库查扩展列表不能凭印象判断。第二个误判是“业务账号没有 CREATE EXTENSION 权限所以它不可能利用 pg_cron”。请注意能创建扩展和能利用已有扩展的对象是两个完全不同的权限级别。普通用户不能创建扩展但如果 cron schema 的权限没有收口他可以直接操作扩展建出来的表对象。第三个误判是“实例没有暴露到公网所以数据库安全漏洞不用优先处理”。数据库安全并不仅取决于网络边界应用层账号本身就是最直接的入口。当内部权限模型过于宽松时攻击者不需要网络穿透只需一个普通的数据库账号就能完成全部操作。第四个误判是“扫描器没报漏洞就等于安全”。常规扫描器通常不做扩展内部逻辑分析不报告 CVE-2024-0985 不代表你的 pg_cron 是安全的。一切以人工核查和版本比对为准。5.3 一点实在的建议我在这次应急里最深刻的体会是真正造成严重后果的往往不是某个漏洞本身有多复杂而是两个很常见的问题撞在一起——组件升级滞后加上权限开放过度。CVE-2024-0985 是一个足够清晰的提醒它告诉我们扩展类组件与数据库内核一样需要纳入补丁管理流程而数据库账号权限的最小化也不能停留在账面上。如果你现在还没有一个“数据库组件扩展清单”我建议从今天开始建立。不用做得很重一张表格记清楚每个实例上装了哪些扩展、什么版本、负责人是谁就能在下次有 CVE 发布时让你少熬好几个小时的夜。最后分享一个小技巧升级 pg_cron 这类扩展前如果担心影响现有任务可以先把当前 cron.job 数据完整导出升级完后再对比一遍任务数量、任务名和调度表达式。这个方法虽然朴素但每次都能帮我快速确认升级没有弄丢任何计划任务你下次升级时也可以试试。
返回列表