浅析GeoServer property 表达式注入代码执行(CVE-2024-36401)

漏洞复现分析

从公告来看,漏洞来源于geotools这个库使用apache xpath解析xpath导致的问题

https://github.com/geoserver/geoserver/security/advisories/GHSA-6jj6-gm7p-fcvv

https://github.com/geotools/geotools/pull/4797

https://github.com/geotools/geotools/security/advisories/GHSA-w3pj-wh35-fq8w

之后简单看看geotools的commit可以发现有很多

https://github.com/geotools/geotools/pull/4797/commits/e53e5170ba71521728875a436c80616cfb03c1e8

比如,从上到下依次看有很多能触发的方式,这里我们简单有个印象即可

1
2
3
4
5
6
7
rg.geotools.appschema.util.XmlXpathUtilites.getXPathValues(NamespaceSupport, String, Document)
org.geotools.appschema.util.XmlXpathUtilites.countXPathNodes(NamespaceSupport, String, Document)
org.geotools.appschema.util.XmlXpathUtilites.getSingleXPathValue(NamespaceSupport, String, Document)
org.geotools.data.complex.expression.FeaturePropertyAccessorFactory.FeaturePropertyAccessor.get(Object, String, Class<T>)
org.geotools.data.complex.expression.FeaturePropertyAccessorFactory.FeaturePropertyAccessor.set(Object, String, Object, Class)
org.geotools.data.complex.expression.MapPropertyAccessorFactory.new PropertyAccessor() {...}.get(Object, String, Class<T>)
org.geotools.xsd.StreamingParser.StreamingParser(Configuration, InputStream, String)

再看geoserver的公告,以下这些都能被利用

image-20240703162342214

首先以最简单的GetPropertyValue为例,从官方文档可以看到具体的使用方法,https://docs.geoserver.org/latest/en/user/services/wfs/reference.html#getpropertyvalue

image-20240703224128239

我比较懒找了个之前的老环境代码方便我本地调试

https://versaweb.dl.sourceforge.net/project/geoserver/GeoServer/2.21.3/geoserver-2.21.3-war.zip?viasf=1

可以看到在org.geoserver.wfs.GetPropertyValue#run,红框中的代码从请求中获取了valuereference参数,之后调用工厂类的property方法获取PropertyName对象

image-20240703224938791

我们来看看这个工厂类的调用,直接返回一个被AttributeExpressionImpl包装的对象

image-20240703230134192

同时实例化时将参数赋给attPath

image-20240703230505213

接下来再来看看evaluate的调用,在这里会通过PropertyAccessors.findPropertyAccessors获取合适的属性访问器,之后遍历调用其get方法,其中就包括了org.geotools.data.complex.expression.FeaturePropertyAccessorFactory.FeaturePropertyAccessor#get,官方公告列出来的就有这个

image-20240703232348317

在下面的代码中可以解析xpath表达式,因此从上面分析下来这个xpath就是valuereference中的值,整个流程也就走通了

image-20240703233055237

路由分析

同时像我这种好奇宝宝一般是比较好奇一些路由方法的调用,就比如为什么通过参数中的request能调用对应方法,这个项目主体框架是spring

以我下载的war为例,先看web.xml,通常而言这就是我们项目的主入口,但是点进去一看,在配置文件中大多只有Servlet的过滤器链的配置,而没有具体接口的配置,当然唯一的可以看到将请求都通过spring的DispatcherServlet派发

1
2
3
4
5
6
7
8
9
10
11
12
<!-- spring dispatcher servlet, dispatches all incoming requests -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<!-- single mapping to spring, this only works properly if the advanced dispatch filter is
active -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

因此接下来我们就得看看,spring项目的一些其他配置文件,比如\geoserver\WEB-INF\lib\gs-wfs-2.21.3.jar!\applicationContext.xml,看着这个配置文件就会更为亲切,当然又扯远了,回到正文

在这个项目中,org.geoserver.ows.Dispatcher继承了AbstractController并实现了handleRequestInternal方法

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
protected ModelAndView handleRequestInternal(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws Exception {
this.preprocessRequest(httpRequest);
Request request = new Request();
request.setHttpRequest(httpRequest);
request.setHttpResponse(httpResponse);
Service service = null;

try {
try {
request = this.init(request);
REQUEST.set(request);

Object result;
try {
service = this.service(request);
} catch (Throwable var11) {
this.exception(var11, (Service)null, request);
result = null;
return (ModelAndView)result;
}

if (request.getError() != null) {
throw request.getError();
}

Operation operation = this.dispatch(request, service);
request.setOperation(operation);
if (request.isSOAP()) {
this.flagAsSOAP(operation);
}

result = this.execute(request, operation);
if (result != null) {
this.response(result, request, operation);
return null;
}
} catch (Throwable var12) {
if (isSecurityException(var12)) {
throw (Exception)var12;
}

this.exception(var12, service, request);
}

return null;
} finally {
this.fireFinishedCallback(request);
REQUEST.remove();
}
}

Object execute(Request req, Operation opDescriptor) throws Throwable {
Service serviceDescriptor = opDescriptor.getService();
Object serviceBean = serviceDescriptor.getService();
Object[] parameters = opDescriptor.getParameters();
Object result = null;

try {
if (serviceBean instanceof DirectInvocationService) {
String operationName = opDescriptor.getId();
result = ((DirectInvocationService)serviceBean).invokeDirect(operationName, parameters);
} else {
Method operation = opDescriptor.getMethod();
result = operation.invoke(serviceBean, parameters);
}
} catch (Exception var8) {
if (var8.getCause() != null) {
throw var8.getCause();
}

throw var8;
}

return this.fireOperationExecutedCallback(req, opDescriptor, result);
}

Operation dispatch(Request req, Service serviceDescriptor) throws Throwable {
if (req.getRequest() == null) {
String msg = "Could not determine geoserver request from http request " + req.getHttpRequest();
throw new ServiceException(msg, "MissingParameterValue", "request");
} else {
boolean exists = this.operationExists(req, serviceDescriptor);
if (!exists && req.getKvp().get("request") != null) {
req.setRequest(normalize(KvpUtils.getSingleValue(req.getKvp(), "request")));
exists = this.operationExists(req, serviceDescriptor);
}

Object serviceBean = serviceDescriptor.getService();
Method operation = OwsUtils.method(serviceBean.getClass(), req.getRequest());
if (operation != null && exists) {
Object[] parameters = new Object[operation.getParameterTypes().length];

for(int i = 0; i < parameters.length; ++i) {
Class<?> parameterType = operation.getParameterTypes()[i];
if (parameterType.isAssignableFrom(HttpServletRequest.class)) {
parameters[i] = req.getHttpRequest();
} else if (parameterType.isAssignableFrom(HttpServletResponse.class)) {
parameters[i] = req.getHttpResponse();
} else if (parameterType.isAssignableFrom(InputStream.class)) {
parameters[i] = req.getHttpRequest().getInputStream();
} else if (parameterType.isAssignableFrom(OutputStream.class)) {
parameters[i] = req.getHttpResponse().getOutputStream();
} else {
Object requestBean = null;
Throwable t = null;
boolean kvpParsed = false;
boolean xmlParsed = false;
if (req.getKvp() != null && req.getKvp().size() > 0) {
try {
requestBean = this.parseRequestKVP(parameterType, req);
kvpParsed = true;
} catch (Exception var14) {
t = var14;
}
}

if (req.getInput() != null) {
requestBean = this.parseRequestXML(requestBean, req.getInput(), req);
xmlParsed = true;
}

if (requestBean == null) {
if (t != null) {
throw t;
}

if ((!kvpParsed || !xmlParsed) && (kvpParsed || xmlParsed)) {
if (kvpParsed) {
throw new ServiceException("Could not parse the KVP for: " + parameterType.getName());
}

throw new ServiceException("Could not parse the XML for: " + parameterType.getName());
}

throw new ServiceException("Could not find request reader (either kvp or xml) for: " + parameterType.getName() + ", it might be that some request parameters are missing, please check the documentation");
}

Method setBaseUrl = OwsUtils.setter(requestBean.getClass(), "baseUrl", String.class);
if (setBaseUrl != null) {
setBaseUrl.invoke(requestBean, ResponseUtils.baseURL(req.getHttpRequest()));
}

if (requestBean != null) {
if (req.getService() == null) {
req.setService(this.lookupRequestBeanProperty(requestBean, "service", false));
}

if (req.getVersion() == null) {
req.setVersion(normalizeVersion(this.lookupRequestBeanProperty(requestBean, "version", false)));
}

if (req.getOutputFormat() == null) {
req.setOutputFormat(this.lookupRequestBeanProperty(requestBean, "outputFormat", true));
}

parameters[i] = requestBean;
}
}
}

if (this.citeCompliant) {
if (!"GetCapabilities".equalsIgnoreCase(req.getRequest())) {
if (req.getVersion() == null) {
throw new ServiceException("Could not determine version", "MissingParameterValue", "version");
}

if (!req.getVersion().matches("[0-99].[0-99].[0-99]")) {
throw new ServiceException("Invalid version: " + req.getVersion(), "InvalidParameterValue", "version");
}

boolean found = false;
Version version = new Version(req.getVersion());
Iterator var20 = this.loadServices().iterator();

while(var20.hasNext()) {
Service service = (Service)var20.next();
if (version.equals(service.getVersion())) {
found = true;
break;
}
}

if (!found) {
throw new ServiceException("Invalid version: " + req.getVersion(), "InvalidParameterValue", "version");
}
}

if (req.getService() == null) {
throw new ServiceException("Could not determine service", "MissingParameterValue", "service");
}
}

Operation op = new Operation(req.getRequest(), serviceDescriptor, operation, parameters);
return this.fireOperationDispatchedCallback(req, op);
} else {
String msg = "No such operation " + req;
throw new ServiceException(msg, "OperationNotSupported", req.getRequest());
}
}
}

从上面的代码中我们很容易发现,通过dispatch的代码我们很容易发现会通过这个request对象查找对应的方法,获取到后之后再通过execute执行,因此答案也就有了

image-20240704000520770

当然这个方法可以仔细看看对请求的解析部分,里面对多种请求方式的解析也可以了解了解

一些具体的流程可参考如下逻辑

image-20240704103211697

后话

相比较其他利用还是觉得GetProperty的利用比较舒服,不像GetFeature之类的里面到处都是触发点,会导致xpath被解析很多次,当然poc就不贴了学习思路为主,在GetProperty中也有一个比较好用的对抗流量设备的点

在这里可以看到在获取参数时会把[]中的内容替换为空,但很可惜是贪婪匹配(至少我这个老代码是这样的),不过也可以拿来做一些利用,比如我们的java.lang.Runtime可以写成java.lang.Ru[Hacked By Y4]ntime

1
PropertyName propertyNameNoIndexes = this.filterFactory.property(request.getValueReference().replaceAll("\\[.*\\]", ""), this.getNamespaceSupport());

依然是可以触发的

image-20240704001829248

凌晨了,洗洗睡了…

参考链接

https://github.com/vulhub/vulhub/tree/master/geoserver/CVE-2024-36401