一、基本概念
1、序列化與反序列化
(1)序列化:將對象寫入IO流中,ObjectOutputStream類的writeobject()方法可以實現序列化
(2)反序列化:從IO流中恢復對象,ObjectinputStream類的readObject()方法用于反序列化
(3)意義:序列化機制允許將實現序列化的Java對象轉換為字節序列,這些字節序列可以保存到磁盤上,或通過網絡傳輸,以達到以后恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在
(4)序列化與反序列化是讓Java對象脫離Java運行環境的一種手段,可以有效的實現多平臺之間的通信、對象持久化儲存。主要應用在以下場景:
HTTP:多平臺之間的通信,管理等,也可以用于流量帶外
RMI:是Java的一組擁護開發分布式應用程序的API,實現了不同操作系統之間程序的方法調用。值得注意的是,RMI的傳輸100%基于反序列化,Java RMI的默認端口是1099端口
JMX:JMX是一套標準的代理和服務,用戶可以在任何Java應用程序中使用這些代理和服務實現管理,中間件weblogic的管理頁面就是基于JMX開發的,而JBoss則整個系統都基于JMX框架
(5)Java代碼審計思路
如果是Java原生類,則需要入口類readObject方法,同時實現了序列化接口,使其可以進行有效的反序列化,此時如果存在DNS解析,或者實現反序列化(利用Runtime對象進行類反射操作)
需要有最終的執行函數(可以執行代碼或者命令),比如Runtime.getRuntime().exec,ProcessBuilder().start,getHostAddress,文件讀寫...等等,這些函數需要自己平常去收集,這樣審計起來會更得心應手
2、Java類反射機制
(1) 反射機制的作用:通過Java語言中的反射機制可以操作字節碼文件(可以讀和修改字節碼文件),可以通過另外的方式調用到類的屬性和方法,甚至私有屬性和方法
(2)反射機制的相關類在java.lang.reflect.*包下面
(3)反射機制的相關類有哪些:Constructor、Field、Method、Class等類
(4)java.lang.Class代表字節碼文件,代表整個類
(5)java.lang.reflect.Method代表字節碼中的方法字節碼,代表類中的方法java.lang.reflect.Constructor代表字節碼中的構造方法字節碼,代表類中的構造方法java.lang.reflect.Field代表字節碼中的屬性字節碼,代表類中的屬性
(6)Java中為什么要使用反射機制,直接創建對象不是更方便?
如果有多個類,每個用戶所需求的對象不同,直接創建對象,就要不斷的去new一個對象,非常不靈活。而Java反射機制,在運行時確定類型,綁定對象,動態編譯最大限度發揮了java的靈活性
(7)獲取成員變量
(8)獲取并調用方法
(9)獲取構造方法
(10)訪問私有屬性
二、類反射機制實踐
package com.woniu.vul;import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;class Test{public String name = "蝸牛學苑";public int age = 8;private String addr = "西安";private int price = 10000;public Test() {}public Test(int price){this.price = price;}public int setPrice(int price){System.out.println("新價格為:" + price);return price;}public int getPrice(){return this.price;}private void getAddr(){System.out.println("私有方法:" + addr);}}public class Reflect {public static void main(String[] args) throws Exception {/*Test t = new Test();System.out.println(t.getPrice());Test t1 = new Test(15000);System.out.println(t1.getPrice());System.out.println(t.name);*///使用反射機制實現屬性和方法的調用(包括構造方法)//使用Class.forName可以獲取到類本身,在JVM中動態加載Test類//Class clazz = Class.forName("com.woniu.vul.Test");//Class clazz = Test.class;//使用new Instance進行實例化//Test t = (Test) clazz.newInstance();//System.out.println(t.getPrice());//Object o = clazz.newInstance(); //實例化動態加載的類,類型必須是Object// Method m1 = clazz.getMethod("getPrice");
// int price1 = (int)m1.invoke(o,null);
// System.out.println(price1);
//
// Method m2 = clazz.getMethod("setPrice",int.class);
// int price2 = (int)m2.invoke(o,15000);
// System.out.println(price2);//調用price私有屬性和getAddr私有方法,getFiled只能調用公有屬性,getDeclareField才能調私有屬性//Field f1 = clazz.getDeclaredField("price");//f1.setAccessible(true); //設置私有屬性可訪問//System.out.println(f1.get(o));//getMethod只能調用公有方法,而getDeclareMethod才能嗲用私有方法//Method m1 = clazz.getDeclaredMethod("getAddr");//m1.setAccessible(true);//m1.invoke(o,null);//構造方法如果有參數,怎么辦?Class clazz = Class.forName("com.woniu.vul.Test");Constructor c = clazz.getConstructor(int.class); //獲取到一個帶參數的構造器Object o = c.newInstance(10); //用構造器去構造一個動態加載的類Method m1 = clazz.getDeclaredMethod("getPrice");int price = (int) m1.invoke(o,null);System.out.println(price);//遍歷所有方法或操作Method[] methods = clazz.getDeclaredMethods();for (Method method : methods) {System.out.println(method + " " + method.getName() + " " + method.getModifiers());}Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {System.out.println(field + " " + field.getName() + " " + field.getModifiers());}}
}
三、序列化與反序列化
序列化的實現代碼
package com.woniu.vul;import java.io.*;class Student implements Serializable {public String name = "";public int id = 0;public String phone = "";public Student(){System.out.println("構造方法運行");}public void study(){System.out.println("學生正在學習");}public void sleep(){System.out.println("學生正在休息");}
}public class Unserial {public void serial() throws Exception {Student s = new Student();s.name = "張三";s.id = 12345;s.phone = "188123456786";FileOutputStream fos = new FileOutputStream("./data/student.ser");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(s);}public void unserial() throws Exception {FileInputStream fis = new FileInputStream("./data/student.ser");ObjectInputStream ois = new ObjectInputStream(fis);Student obj =(Student) ois.readObject();System.out.println(obj.name);obj.study();}public static void main(String[] args) throws Exception {Unserial us = new Unserial();//us.serial();us.unserial();}
}
序列化的內容如下:
?然后進行反序列化,切記不要去改我們的序列化內容,不然無法反序列化回去
?我們再補充兩個小問題
正常情況下,高亮部分是可以被序列化的,但是如果在這之前加上transient來修飾的話就無法序列化
高亮部分的意思就是定義一個序列化版本的編號,也就是唯一標識
?
這個標識是用來干嘛的
接下來我們看看
我們重新反序列化看看效果
報錯信息告訴我們是一個不可用的類
為什么不可用,讓我們繼續看報錯信息
序列化的時候類的ID是前面的那一個,但是反序列的時候類的ID是后面那一個,序列化和反序列化的時候標識號是不一樣的,意思就是這個類并不是我們需要反序列化的類
我們將代碼中的ID改為其序列化時候原本的ID,那我們就可以完成反序列化的操作了
也就是說這個類在序列化的時候會記錄下其類的標識UID
接下來我們看看反序列化產生的機制
首先這個序列化對象一旦有重寫的方法,那我們在反序列化的時候會優先調用重寫的readObject
?因為我們重寫了readObject,所以就會先調用readObject,這就是Java反序列化的起點,也是唯一的起點
也就是說Java反序列化漏洞能夠被利用,我們得有一個最基本的前提,就是目標類必須重寫readObject方法,只有這樣,代碼才會被自動調用,否則就沒有起點
如果不重寫readObject方法的話,就不會發生Java反序列化漏洞
而我們的代碼已經重寫了readObject方法,所以我們可以對其利用
我們可以直接在重寫方法的下面加上終點,也就是攻擊者想要達到的效果,有始有終,整個攻擊鏈才算完整
當然我們也可以使用類反射機制的手段去執行命令
因為getRuntime的實例不是純粹的new出來的,而是通過調用getRuntime這個方法來獲取其實例的,然后再通過這個實例去調用exec
加載java.lang.Runtime類
獲取Runtime類中的getRuntime方法
調用Runtime方法,獲取Runtime類的實例
獲取Runtime類中的exec(string)方法
調用exec(String)方法,運行外部命令ifconfig
?運行代碼,發現沒有報錯,說明應該是利用成功了,我不知道為啥不會顯示執行ifconfig命令的內容,如果是Windows的話,可以將ifconfig改為calc.exe,大概率會顯示出計算器
當然為了執行命令,不僅僅只有Runtime,還有ProcessBuilder
接下來我們看看其反射的調用
根據正常的調用來構造反射
先使用Class.forName這個方法來加載java.lang.ProcessBuilder這個類
然后使用Class對象的getConstructor來獲取Processbuilder類的構造函數
接著使用Constructor對象的newInstance方法來創建ProcessBuilder的實例
然后使用Class對象的getMethod方法去獲取ProcessBuilder中的start方法
最后使用Method對象的invoke方法去調用start方法
?然后運行,發現報錯,是類型出現了錯誤
?我們先去看看ProcessBuilder的構造方法,它不是嚴格意義上的String,是String...(可變長的字符串)如果是whoami /user這條命令的話,我們得寫到兩個字符串里面,在Java中,對于可變長的字符串是將其放入到數組當中去
?然后我們將其修改為String[].class
然后繼續運行,然后還是報錯說類型不匹配
就是因為我們上面定義的是數組,下面是字符串,所以會報錯
所以我們要將下面的類型轉換為數組類型就可以了,也就是將其放到數組中就可以了,如下
繼續運行,發現還是報錯,報錯信息還是類型不匹配
?我們去看看newInstace的構造方法,發現還是一個數組,它的類型是數組,數組里面的參數還是一個數組
所以這個cmd的類型要定義成二維數組,二維數組只需加兩個{}即可,如下
然后去運行,發現沒有報錯,但是也沒有回顯命令的內容,應該是電腦的問題,如果是Windows的話在命令那一塊改為calc.exe就可以打開計算器了
?