寫在前面
參考資料
https://blog.csdn.net/mocas_wang/article/details/107621010
https://juejin.cn/post/6844904025607897096#heading-15
https://zhuanlan.zhihu.com/p/72644638
https://segmentfault.com/a/1190000023876273
https://juejin.cn/post/6844903838927814669
IDEA快捷鍵使用
跟進類、方法:Ctrl+B
彈出structure框框:Alt+7
Java原生(反)序列化
基本使用
讓需要被(反)序列化的類實現(xiàn)一下Serializable接口就行了。
class Person implements Serializable{}
輸出的話,需要實例化一個”對象輸出流“對象,調(diào)用它的writeObject方法。
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("D://demo.txt"));
out.writeObject(wkz);
//out是“對象輸出流”對象,wkz是需要被序列化的對象。
讀入類似,換成“對象讀入流”和readObject就行了。
ObjectInputStream in=new ObjectInputStream(new FileInputStream("D://demo.txt"));
Person who=(Person) in.readObject();
//注意要一個強轉
有transient標識的對象不參與序列化。
方法重寫
我們當然不能滿足于上述的基本使用,而是稍微探尋一下它的原理和個性化功能。
事實上,類似PHP對象在被序列化時自動調(diào)用__sleep方法,在被反序列化時自動調(diào)用__wakeup方法,Java對象在被序列化時會自動調(diào)用writeObject方法,在被反序列化時自動會調(diào)用readObject方法。而這些方法都是可以在 需要進行序列化相關操作的類里 被“重寫”的。
//“重寫”打上引號的原因,就是它并不需要加Override
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
//我原先以為重寫writeObject能讓我們改變輸出的Java序列化字節(jié)碼的格式,甚至可以輸出人話;但實際上并不是這樣(至少我不會)。
//我們只是可以進行一些操作來改變對象屬性的值,最后還是得調(diào)用defaultWriteObject或WriteObject。
//這里的defaultWriteObject就相當于我們重寫前的WriteObject。
this.age=-1;
s.defaultWriteObject();
//此外,我們還可以干一些和序列化不相干的事,比如命令執(zhí)行。
Runtime.getRuntime().exec("calc");
}
//這里跟上面差不多,就不多贅述了
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException,ClassNotFoundException{
s.defaultReadObject();
//注意在default之后再修改屬性,否則會被覆蓋
this.age=100;
//也可以命令執(zhí)行。
Runtime.getRuntime().exec("calc");
}
重寫了上面兩個方法后,如果再對這個類的對象進行序列化相關的操作,就會使計算器被打開。這就是最原始的命令執(zhí)行。
調(diào)用鏈:基本的類嵌套&同名方法調(diào)用
(這塊涉及的內(nèi)容比較淺,可以說是我在PHP中最先學到的反序列化漏洞姿勢的 Java實現(xiàn))
前面所述的代碼屬于“入口類的readObject直接調(diào)用系統(tǒng)方法”;這種情況在真實環(huán)境中是很少出現(xiàn)的。更多的情況是“入口類參數(shù)中包含可控類對象,該類對象又調(diào)用別的類對象,別的類對象又.....幾層之后,才出現(xiàn)系統(tǒng)方法。
在類對象的調(diào)用過程中,如果讀入類對象的內(nèi)容可控,則用戶可以通過同名方法調(diào)用,將調(diào)用鏈引向開發(fā)者不曾設想的地方。
為了講述原理方便,這里只舉一個簡單的例子。
import java.io.Serializable;
import java.io.*;
/*
work類 和Person類,animal類,plant類(后面兩個沒寫代碼,就意思意思)屬于一塊邏輯,
開發(fā)者的想法是,讓用戶傳入一個屬于Person、animal、plant等類的對象,然后根據(jù)不同的類,進行不同的自我介紹。
但在每個類里都寫一個readObject方法太麻煩了,于是開發(fā)者用了個大的work類做包裹,直接調(diào)用其對象元素的toString方法。
但是work類的參數(shù)類型是Object且沒有額外過濾,所以可以干一些別的事情。
sys類是這個程序中,與上面那塊邏輯完全不相干的東西。
但是它的toString方法中有個系統(tǒng)調(diào)用。
于是,我們用sys對象作為屬性生成一個work對象(注釋的那三行)
并將其送入開發(fā)者提供的反序列化服務。
便可以成功進行syscall。
*/
class work implements Serializable{
private Object thing;
public work(Object thing) {
this.thing = thing;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException,ClassNotFoundException{
s.defaultReadObject();
System.out.println(this.thing);
}
}
class Person implements Serializable{
private String name;
private int age;
public Person(){}
public Person(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString(){
return "introduce:Person{name='"+this.name+"',age='"+this.age+"}";
}
}
class sys implements Serializable{
@Override
public String toString(){
return "This is an syscall";
}
}
public class one2022 {
public static void main(String[] args) throws Exception{
//work syscall=new work(new sys());
//ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("D://demo.txt"));
//out.writeObject(syscall);
ObjectInputStream in=new ObjectInputStream(new FileInputStream("D://demo.txt"));
in.readObject();
}
}
代碼看起來很簡單,甚至有點傻;主要是看代碼對應的邏輯。
繼續(xù)深入?
基本的調(diào)用鏈邏輯,上面那個例子就夠了。
由于我Java知識的缺乏,這里如果接著上面的思路繼續(xù)寫反序列化鏈利用的話,就變成PHP那套__call,__invoke
之類的東西了。
在PHP里,我不少很多出題人自己構造的反序列化鏈的題,也自己出過題,但主要的問題就是沒有找過框架層面的反序列化鏈,在比較真實的環(huán)境里找鏈的能力很弱。
所以在Java里,根據(jù)魔術方法構建反序列化鏈 這條老路我就不再走一遍了,而是學一些Java相關的知識和技巧后,開始嘗試在 正經(jīng)Java-web邏輯以及一些框架 里嘗試找鏈。
所以,在這里,就不繼續(xù)深入了。
Java反射
理解
與“正射”相對;不使用new來創(chuàng)建對象。
反射的作用:讓Java具有動態(tài)性。
PHP是一個動態(tài)性很強的語言;eval("字符串");
可以直接將(用戶輸入的)字符串當作代碼執(zhí)行。但正常的Java就沒有這種功能。運用反射,可以讓java實現(xiàn)類似的功能。
基礎使用
以Person類為例。
class Person implements Serializable{
public String name;
private int age;
public Person(){}
public Person(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString(){
return "Person{name='"+this.name+"',age='"+this.age+"}";
}
public void action(String s){
System.out.println(s);
}
}
反射的關鍵在于操作“類的原型”,即Class對象。
Person person=new Person();
Class c=person.getClass();//Class相當于類的原型
動態(tài)生成對象
//c.newInstance();
//可以直接調(diào)class對象的newInstance方法生成對象,但它只會調(diào)用person的無參構造方法,不能滿足我們的需求。
Constructor personcon=c.getConstructor(String.class,int.class);
//獲取以string和int作為類型的構造函數(shù);注意傳參是.class形式。
Person p=(Person) personcon.newInstance("pzc",19);
//用獲取的構造函數(shù)生成對象。
System.out.println(p);
獲取&修改對象屬性
//使用getField獲取 類原型 的公共屬性,并使用set作用于一個類對象,修改該屬性。
Field namefield0=c.getField("name");
namefield0.set(p,"hiddener");
System.out.println(p);
//使用getDeclaredfield獲取 類原型 的私有屬性,并使用setAccessible使其可修改。
//注意setAccessible沒有對象參數(shù),即,它是作用于屬性對象的(Field)
Field namefield1=c.getDeclaredField("age");
namefield1.setAccessible(true);
namefield1.set(p,20);
System.out.println(p);
//打印Person類的所有屬性(結果都是private int Person.age這種形式,和具體的實例化對象無關)
Field[] personfields=c.getDeclaredFields();
for (Field f:personfields){
System.out.println(f);
}
獲取&調(diào)用對象方法
//獲取方法與獲取屬性基本相同
//需要額外注意的是,這里的getMethod可以獲取繼承自父類的屬性,而getDeclaredMethod好像不行。
Method[] personmethods=c.getMethods();
for(Method m:personmethods){
System.out.println(m);
}
//生成Method方法對象,并通過invoke調(diào)用Person類對象的方法。也是要注意參數(shù)。
Method action=c.getMethod("action", String.class);
action.invoke(p,"wawawa");
}
漏洞利用
(在反序列化漏洞中的應用)
定制需要的對象;
通過invoke調(diào)用除了同名函數(shù)以外的函數(shù);
通過Class類創(chuàng)建對象,引入不能序列化的類。
JDK動態(tài)代理
代理模式是一種設計模式。(類似“工廠模式”這種)
其主要意圖是為其他對象提供一種代理以控制對這個對象的訪問。
靜態(tài)代理
先有一個類。
public class User0 implements IUser{
public User0(){
}
@Override
public void show(){
System.out.println("展示");
}
@Override
public void update(){
System.out.println("更新");
}
}
該類實現(xiàn)了一個IUser接口,它是代理必然需要的東西。在這個靜態(tài)代理的樣例里,它是這樣寫的:
public interface IUser {
void show();
void update();
}
我們還需要用一個代理類實現(xiàn)這個接口。
public class UserProxy implements IUser{
IUser user;
public UserProxy(IUser user){this.user=user;}
@Override
public void show(){
user.show();
System.out.println("調(diào)用了show");
}
@Override
public void update(){
user.update();
System.out.println("調(diào)用了update");
}
}
最后進行調(diào)用測試。
public class ProxyTest {
public static void main(String[] args){
IUser user=new User0();
IUser userProxy=new UserProxy(user);
userProxy.show();
//使用userProxy調(diào)用user的show方法
}
}
可以看到,我們使用userProxy調(diào)用了user的show方法,同時userProxy生成了“調(diào)用了show”調(diào)用日志。調(diào)用日志記錄這個功能是不需要show本身實現(xiàn)的,這樣會顯得邏輯很混亂。加一個代理類負責記錄各種日志,同時也達到了代理模式中“控制對這個對象的訪問”的意圖。
動態(tài)代理
但是,前面靜態(tài)代理的缺點是顯而易見的。對于接口里聲明的每一個方法,我們都要在UserProxy代理類里寫一個對應的方法來實現(xiàn)它,這樣非常麻煩,而且容易產(chǎn)生大量重復代碼。
我們的想法是,最好,無論接口聲明了多少方法,代理類都用同一個方法實現(xiàn)代理,且實現(xiàn)對需要代理的不同方法的不同處理。
然而,正常情況,在寫代理類方法時,我們無法從內(nèi)部獲知外面調(diào)用了代理接口的哪一種方法。
所以,需要使用Java自帶的動態(tài)代理科技。
還是原來的User0類和接口:
public class User0 implements IUser{
public User0(){
}
@Override
public void show(){
System.out.println("展示");
}
@Override
public void update(){
System.out.println("更新");
}
}
public interface IUser {
void show();
void update();
}
但是,代理類和之前相比,有了很大的不同:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserInvocationHandler implements InvocationHandler {
IUser user;
public UserInvocationHandler(IUser user){
this.user=user;
}
@Override
public Object invoke(Object proxy, Method method,Object[] args) throws Throwable{
String name=method.getName();
System.out.println("調(diào)用了"+name);
method.invoke(user,args);
return null;
}
}
調(diào)用測試:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args){
IUser user=new User0();
InvocationHandler userinvocationhandler=new UserInvocationHandler(user);
//classloader,要代理的接口,要做的事情
IUser userProxy=(IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(),user.getClass().getInterfaces(),userinvocationhandler);
userProxy.update();
}
}
這套東西能實現(xiàn)剛才那個需求的原因是,我們自己寫的代理管理器類(動態(tài)代理類;實現(xiàn)了InvocationHandler接口的UserInvocationHandler)有Method參數(shù)。這里面的invoke是個重寫,參數(shù)是固定的;即,能有這個參數(shù),是Java本身想好了的。
關于動態(tài)代理里涉及到的各種新類、新方法,這里就不贅述了。以后有機會的話再慢慢研究。大體的研究思路是跟進去看源碼,看傳參類型,不懂的就查資料問人,在這個過程中多學一些java相關的知識。
漏洞利用
在動態(tài)代理類存在時,前面不管調(diào)了什么,都會經(jīng)過它的invoke,而invoke后的調(diào)用和前面的調(diào)用就沒啥關系了。有時可以起到鏈拼接的效果。
動態(tài)代理類的invoke在有函數(shù)調(diào)用時自動執(zhí)行;這和前面 readObject在反序列化時自動執(zhí)行有異曲同工之妙。
類的動態(tài)加載
感覺這東西難度挺大的。
類加載流程
基礎知識
其中,加載和連接不是嚴格的先后關系,而是并列的。
Java類除了我們熟知的方法(構造方法,靜態(tài)方法等),還有“代碼塊”這種東西。其分為靜態(tài)代碼塊和構造代碼塊;
除了我們熟知的類實例化(生成對象),還有“類初始化”階段。
先擺出結論:上述內(nèi)容中,靜態(tài)代碼塊屬于初始化范疇,其他都屬于使用范疇;初始化中內(nèi)容只執(zhí)行一次,而“使用”中的內(nèi)容可以執(zhí)行多次。除了構造方法和(其他)魔術方法,一般情況下方法都需要顯式調(diào)用才會執(zhí)行,靜態(tài)方法也不例外。
基礎測試
public class Test {
public String name;
private int age;
public static int id;
static {
System.out.print("靜態(tài)代碼塊 ");
}
{
System.out.print("構造代碼塊 ");
}
public static void staticAction(){
System.out.print("靜態(tài)方法 ");
}
public Test() {System.out.print("構造方法" );}
}
以下,被注釋分割的都是一個個獨立的測試。
new Test();
//靜態(tài)代碼塊 構造代碼塊 構造方法
//用new,就一股腦全執(zhí)行了,沒啥好說的。
Class c=Test.class;
c.getConstructor();
//
//獲取類原型,以及調(diào)用類原型的大部分方法,都不進行初始化操作。
Class c=Test.class;
c.newInstance();
//靜態(tài)代碼塊 構造代碼塊 構造方法
//用反射直接實現(xiàn)類實例化,也是一股腦全調(diào)用
new Test();
Class c=Test.class;
c.newInstance();
//靜態(tài)代碼塊 構造代碼塊 構造方法 構造代碼塊 構造方法
//靜態(tài)代碼塊只執(zhí)行一次。
Class.forName("Test");
//靜態(tài)代碼塊
//調(diào)用這玩意也執(zhí)行初始化,有點神奇奧
ClassLoader cl=ClassLoader.getSystemClassLoader();
Class.forName("Test",false,cl);
//
//通過改參數(shù),讓它不初始化了。
最后兩個測試多說一句;我們跟到forName里,發(fā)現(xiàn)
打開Structure,找其他forName:
看到還有個第一個參數(shù)也是String的forName,點過去:
發(fā)現(xiàn)initialize參數(shù),設置為false;最后那個ClassLoader,先別管是啥,模仿著生成個傳進去不報錯就行了。
類加載調(diào)試
先補充一句;ClassLoader的loadClass方法不會引起類初始化。
操作
原則:loadClass和loadClassOrNull進,其余跳。
過程:
它先跳到了ClassLoader里的單參數(shù)loadClass,再到了ClassLoaders里的loadClass。在ClassLoaders.loadClass里進行一些安全檢查后,直接調(diào)用父類雙參數(shù)super.loadClass(cn, resolve)進入BuiltinClassLoader類。
BuiltinClassLoader類是重頭戲;后面基本就在這個類里來回跳了。它在里面調(diào)自己的私有l(wèi)oadClassOrNull方法。該方法檢查parent屬性,若不為空,則調(diào)它的loadClassOrNull方法。
第一輪中,該屬性是PlatFormClassLoader類。
繼續(xù),很快又回到了這里,發(fā)現(xiàn)是BootClassLoader類。
繼續(xù),發(fā)現(xiàn)在Boot這層,最后c的返回值為null;在platform這一層,c的返回值為“class Test”。
這個Test一直被回帶,最終回到測試代碼里。注意測試代碼中的ClassLoader cl是AppClassLoader類。
解釋
這種類加載過程與Java的雙親委派模型有關。
雙親委派模型其實是單親(拳師警告);它反映的是一種調(diào)用關系:當類生成時,會先找到最頂層的加載器,從它開始加載類;若它不能加載,下一層的加載器再嘗試加載,以此類推。
圖中,Extension ClassLoader對應我們調(diào)試中的 Platform ClassLoader;我們沒有寫自定義ClassLoader,剛開始就是AppClassLoader。
所以,前面的調(diào)試過程反映的流程是:我們實例化的APPClassLoader加載器通過PlatformClassLoader找到最頂層BootClassLoader,Boot不能加載那個類;再通過PlatForm加載。加載成功,返回。
一些利用
URLClassLoader加載任意類
把之前Test類生成的.class文件放在了項目根目錄。
進行復現(xiàn)操作:
URLClassLoader urlclassloader=
new URLClassLoader(new URL[]{new URL("http://localhost:9999/")});
Class<?> c=urlclassloader.loadClass("Test");
c.newInstance();
能夠執(zhí)行。
(這個過程建議也調(diào)一下,比上面稍復雜一點;它在BuiltinClassLoader里沒找到,catch了一個exception,之后再URLClassLoader類里找到的。)
先告一段落吧。
本文摘自 :https://www.cnblogs.com/