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