最近更新项目差不多了,感觉项目的大部分问题总算得到一个总体的解决。对象分析真的太要命了
github: https://github.com/kyo-w/router-router
我经常对半自动化代码审计的工具会有个小小的问点,无论是现今所谓的静态代码分析还是自动化代码分析工具,他们的注重点永远的都是挖掘源代码的漏洞。但是很多情况下会有到这样的问题:
public class TestMain{
public void Testmain(HttpServletRequest req, HttpServletResponse resp){
String id = req.getParameter("info");
Runtime.getRuntime().exec(id);
}
}
理论上来说,这个类一定会被工具分析出来的,但是问题来了,产品的某个API节点会调用TestMain.test方法吗?这显然要打个问号?所以我觉得首先必须要把哪些类才是API节点的入口都统一整理出来。只有这样,我觉得在配合像Codeql这样的工具时,可以避免很多多余的告警或者没有意义的结果。所以我从去年的漏洞挖掘中已经在做这么一个事情了,当前做这个事情有很多的解决方案:
● 静态分析配置文件: 这个方案很糟糕,比如Filter或者Serlvet的注入是有可能在代码运行中才注入(jersey这样无配置可分析的,将是个无解的方案)
● Java Agent: 技术实在难度大, 难点在怎么增强字节码(类和路由的注册时间不一样的)、项目兼容问题
● JDWP的调试方案: 接口调用简单,难点在对象分析
以下路由都不在配置文件的范畴:
● Spring的注解路由
● Jersey的注解路由
● @Servlet
● 代码层面的路由修改(动态增加war/动态添加servlet/动态增加Spring路由等等)
可见,效果极差
Java Agent本质是字节码增强,方案比较推荐AOP某个方法:在注册路由和处理类时,加一个记录功能。但是有些坑爹的地方:
● Java Agent开发过程中如果引入了一个依赖包,这个依赖包和生产的依赖包发生冲突怎么办?
● Java Agent版本可能和目标产品的JVM是有不兼容的问题,比如Java Agent编译的版本过高了怎么办?
基于以上两点,我并没有采用。我相信世界这么多产品,你不可能都能完美避免
也许经常用IDEA做调试吧,我开始关注起JDI的相关技术了,至少在以下情况,它能做得更好:
● 独立运行,意味着它不存在和产品兼容性绑定的问题
● 独立运行,依赖完全自由控制,甚至在一个SpringBoot项目中存在
● 由于是调试技术,可以不用像静态分析那样做些反编译的工作
花小部分时间学习一些API能解决我的问题,我觉得不算什么
如果做调试的,经常会在目标产品中添加这么一个启动参数:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
那么作为调试器该如何连接调试端口5005呢?
答案是SocketAttachingConnector
SocketAttachingConnector socketAttachingConnector = new SocketAttachingConnector();
Map<String, Connector.Argument> argumentHashMap = socketAttachingConnector.defaultArguments();
argumentHashMap.get("hostname").setValue("127.0.0.1");
argumentHashMap.get("port").setValue("5005");
argumentHashMap.get("timeout").setValue("3000");
VirtualMachine attach = socketAttachingConnector.attach(argumentHashMap);
在完成上面的代码时,你将获得一个VirtualMachine的对象,这是一个目标JVM的一个引用,此时你可以对目标做以下操作:
● 添加断点
● 搜索目标加载的类/类对象
● 对目标加载的类做一些操作:比如调用Runtime.getRuntime().exec('')(出于安全考虑,工具默认IP限制127.0.0.1)
这里先从以下几个方面分析究竟哪种方式更加有利于分析
这是我最开始的想法,但很快得到了痛击,为什么?你要知道,添加断点意味着首先你要等待一个断点调试的事件,我的天啊,这意味着你要发送请求去触发断点。我举个例子:
http://127.0.0.1:8080
是一个基于tomcat的java web服务。你发送http://127.0.0.1:8080
时,你是一定能触发org.apache.catalina.mapper.Mapper的internalMap方法,因为你一定会触发tomcat寻找路由这一件事情。但是很可惜的是,SpringMvc/Jersey/struts呢?怎么确保能触发他们各自的寻找路由的请求呢?很显然,困难摆在你的面前了。也许你会说我发送一个http://127.0.0.1:8080/spring/api
请求不就行了吗?我们再看看以下的示例图吧
工具太难构造出这样的请求。如果没有正确的token,你根本到不了Spring的路由分析调试点,所以这意味着你的工具要一直处于一个连接调试的状态,把每一个功能点都走完,才算分析完成。这其实是有点失败的,因为这一点都不智能。而且还有一个很大的问题:这种基于断点的调试,在大一点的项目还很容易卡死!(原因很简单,你卡着一个断点然后花了一秒钟的时间做处理,但是你在浏览器访问时,经常会在一个时间发送大量的请求,这意味剩下的断点都卡着等你,一等你,浏览器觉得卡了也会发送尝试的请求过来,导致越来越多的请求)所以在router-router中经常会崩溃。
这是我觉得现今最稳妥的方案了。因为此时工具已经不再等待调试信息了,你可以直接通过JDI接口调用的形式获得某个类对应的所有实例对象:
VirtualMachine attach;
List<ReferenceType> refs = attach.classesByName("org.apache.catalina.mapper.Mapper");
List<ObjectReference> instances = refs.get(0).instances(0);
此时你就拿到目标JVM所有的org.apache.catalina.mapper.Mapper对象,当然有时候你会获取很多的org.apache.catalina.mapper.Mapper,因为存在对象销毁重新创建的可能。还有那些游离还未被gc处理的对象,你也会在这时刻一起拿到。虽然存在重复分析的可能,但是至少能完美达到我想要的结果。
那么Spring/Jetty/Jersey/Struts/tomcat要分析哪些对象呢?
//spring
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
org.springframework.web.servlet.mvc.support.ControllerBeanNameHandlerMapping
org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping
org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
//jetty
org.eclipse.jetty.webapp.WebAppContext
org.eclipse.jetty.servlet.ServletContextHandler
//jersey
org.glassfish.jersey.servlet.ServletContainer //1.x
com.sun.jersey.spi.container.servlet.ServletContainer //2.x
//struts
org.apache.struts.config.impl.ModuleConfigImpl //1.x
com.opensymphony.xwork2.config.impl.DefaultConfiguration //2.x
//tomcat
org.apache.catalina.mapper.Mapper // Tomcat8/9
org.apache.tomcat.util.http.mapper.Mapper // Tomcat 6/7
目标搜索的对象,你将失去方法调用的权利,因为JDI在调用方法时是一定要在一个线程环境中运行的,然而通过内存搜索的方式,你是没有设置断点,没有断点事件就意味着你得不到一个线程环境,所以你只能分析对象的结构,无法调用对象的任何方法,这也是为什么Router4.x版本中只有tomcat有version(Tomcat可以不用调用方法获得版本号), 所以在使用IDEA做调试时,如果没有到断点的时候,你压根没法执行表达式的原因。
基本的思路已经确定,我们是不是可以继续延申: