浅谈被动式IAST产品与技术实现
笔者目前就职于墨云科技,欢迎大家一起来交流、学习,因为之前有过参与RASP研究&研发的一些经验,而且看近两年IAST看着发展势头挺足,所以一直有时间想研究下IAST相关的技术,但是苦于国内外对于IAST具体实现的细节的一些文章并不是很多,唯一开源的IAST也就是洞态了,所以想自己对IAST原理进行简单的技术实现。正好最近有时候有点时间,所以抽空研究了下IAST相关的技术实现,笔者因为工作原因,已经将近2年多没有碰Java了,对于本文章中的一些疏漏点,且本文仅代表个人的一些观点,还望大家多多包涵。
0x00 什么是IAST
IAST是AST其中的一个类别,AST是Application Security Testing的简称,翻译过来就是应用安全测试,在他之下衍生出来以下几种类型:
- SAST(Static Application Security Testing)静态应用安全测试
- DSAT(Dynamic Application Security Testing)动态应用安全测试
- MAST(Mobile Application Security Testing)移动应用安全测试
- IAST (Interactive Application Security Testing)交互式应用安全测试
对于IAST的定义我并没有在Gartner找到其相关的术语表,但是在Gartner推荐的服务商中找到了一些关于IAST的定义,我总结了下,核心如下:
IAST使用运行时代理方法在测试阶段分析&监控应用程序的行为。这种类型的测试也不测试整个应用程序或代码,而只测试执行功能的部分。
比较有意思的一点是,大家好像都在说IAST是Gartner2012年提出来的术语,但是我查了Gartner的术语表,并没有找到IAST相关的定义,但是在Gartner推荐的服务商找到了IAST相关的标签和简单的介绍(可能由于Gartner之前改版,导致这个术语丢失?)
好了,回到正题,IAST呢,又细分为好几种,大家可以看下 https://www.freebuf.com/sectool/290671.html 这篇文章,对IAST的分类有比较清晰的描述。
0x01 国内外IAST产品
笔者对国内外的IAST相关的产品公司进行了一些规整,大概如下(该数据不代表所有的IAST厂商,仅是笔者搜索到的部分厂商)
厂商 | 产品名 | 产品介绍 | 相关网址 |
---|---|---|---|
Checkmarx | CxIAST | Checkmarx CxIAST解决方案提供了一个应用程序安全自测试模型,其中安全测试由自动或手动执行的任何应用程序功能测试(通常是QA)驱动。它还以零时间(即时检测)和零运营开销交付结果,这使其非常适合CI/CD环境。 | https://checkmarx.atlassian.net/wiki/spaces/CCD/pages/253657926/Checkmarx+CxIAST |
Contrast | Contrast ASSESS | Contrast Assess是一种革命性的交互式应用程序安全测试(IAST)解决方案,它将安全专业知识融入应用程序本身。Contrast agent使用智能传感器检测应用程序,以便从应用程序内部实时分析代码。Contrast Assess然后使用代理收集的情报来识别和确认代码中的漏洞。这包括已知(CVE)和未知漏洞。 | https://www.contrastsecurity.com/interactive-application-security-testing-iast |
synopsys | Seeker IAST | Seeker 易于在 CI/CD 开发工作流程中进行部署和扩展。本机集成、Web API 和插件能够无缝集成到用于本地、基于云、基于微服务和基于容器的开发的工具。无需大量配置、自定义服务或调整,即可获得直接可用的准确结果。Seeker 在正常测试期间监视后台的 Web 应用交互,并能快速处理数十万个 HTTP 请求,在几秒钟内为您提供结果,误报率几乎为零,而且无需运行手动安全扫描。 | https://www.synopsys.com/software-integrity/security-testing/interactive-application-security-testing.html |
HCL Software | AppScan IAST | 一个可扩展的应用程序安全测试工具,提供SAST、DAST、IAST和风险管理功能,帮助企业在整个应用程序开发生命周期中管理风险和合规性。 | https://help.hcltechsw.com/appscan/Enterprise/zh_CN/10.0.2/topics/c_ase_iast_scanning.html |
北京酷德啄木鸟信息技术有限公司 | CodePecker Finder | Finder 是 北京酷德啄木鸟信息技术有限公司提供的一款基于敏感数据追踪分析的交互式应用程序安全测试(IAST)软件,通过 Finder 可深入观察应用系统的的安全状况并发现基于各种合规性标准(例如 OWASP Top 10、CWE/SANS、Cert)的缺陷定义,并能够提供可视化的视图。 | http://www.codepecker.com.cn/ |
北京安普诺信息技术有限公司 | 悬镜灵脉IAST | 悬镜灵脉IAST灰盒安全测试平台作为一款次世代智慧交互式应用安全测试产品,采用前沿的深度学习技术,融合领先的IAST产品架构,使安全能力左移前置,将精准化的应用安全测试高效无感地应用于从开发到测试的DevSecOps全流程之中。 | https://iast.xmirror.cn/ |
OpenRasp | IAST 灰盒扫描工具 | 基于 OpenRASP 的一款灰盒扫描工具。 | https://github.com/baidu-security/openrasp-iast |
默安科技 | 雳鉴IAST | 默安科技雳鉴交互式应用安全检测系统(以下简称“雳鉴IAST”),专注解决软件安全开发流程(SDL)中测试阶段的应用安全问题。雳鉴IAST使用基于请求和基于代码数据流两种技术的融合架构,采用被 Gartner 评为十大信息安全技术之一的IAST技术,结合SAST和DAST的优点,做到检出率极高且误报率极低,同时可定位到API接口和代码片段,在测试阶段无缝集成,可高准确性的检测应用自身安全风险,帮助梳理软件成分及其漏洞,为客户系统上线前做强有力的安全保障。 | https://www.moresec.cn/product/sdl-iast |
北京安全共识科技有限公司 | 洞态IAST | 洞态IAST是全球首家开源的IAST,支持SaaS访问及本地部署,助力企业在上线前解决应用的安全风险 | https://dongtai.io/ |
杭州孝道科技有限公司 | 安全玻璃盒 | 安全玻璃盒自主研发了国内第一款运行时非执行态的交互式应用安全测试系统,通过安全与软件高度耦合的安全检测技术,对应用系统漏洞及所引用的三方组件,实现在线无风险、高效自动化、全面精确可视化的漏洞检测和问题定位。 | https://www.tcsec.com.cn/product/iast |
0x02 国内外IAST技术实现现状
洞态
洞态是目前国内&业界开源的一款被动式IAST产品,对于洞态的一些技术细节,网上已经有一些文章对其进行了分析,在这里笔者简单的用自己的理解对于洞态实现的IAST进行一个总结阐述:
- 依靠JVM-Sandbox的AOP能力对关键类进行埋点处理
- 依靠预定义好的规则,对上下文请求、埋点数据进行跟踪反馈
洞态对于IAST规则的定义分了以下几个类别:
- Http(这个类别没有在规则中体现出来,代码中有这部分的实现,主要是对Servlet数据进行克隆存储)
- Source (如getParameter、getParameterValues等获取http请求包中数据的一些方法)
- Propagator(污点传播,一堆复杂的逻辑对上下文进行判断,根据判断结果去决定是否保存该传播点的信息)
- Sink(最终漏洞触发点)
我本地搭建了个洞态的服务,在后台看到了不少规则信息,
可以看到预定义的各种规则覆盖了不少,因此可以对于在展示漏洞的时候,将其相关的调用传播堆栈信息展示出来。
但是洞态对于整条链路中所涉及到的souce的传播以及到最后危险函数到达的部分,是没直观的看到其在传播中变量的整个传播变化结果,仅有一个source获取攻击参数的展示,这样可能对后续报告中的体现,以及推动研发修改这个漏洞有一些暗坑。
经洞态的研发确认我知道了如果是通过私有化部署的方式下载的 Agent,可升级至 洞态 1.1.3 级以上版本或增加 JVM 参数:-Diast.server.mode=local ,即可收集到链路上的具体数据,这部分就需要大家自己去研究看看了。
Contrast
Contrast提供免费的使用,因为对agent的代码进行了混淆,所以我没有对其到底如何实现进行深入的了解,有兴趣的朋友可以了解看看。
通过对agent的使用以及控制台的展示内容来看,我个人感觉Contrast的IAST更像RASP(Contrast也提供RASP功能,可能我没玩明白..),所以到底是IAST,还是RASP换一种方式去展现,这个就需要大家自己去深入了解了。
Checkmarx
Checkmarx是基于AspectJ对关键的类进行切片埋点,因为我拿到Checkmarx的Agent是没办法直接运行起来的,只有jar,所以只是简单的看了下逻辑,在Checkmarx的代码中,可以看到其埋点的数据都在com.checkmarx.iast.agent.aspects.original
包里面,
而且是完全依赖AspectJ对关键的类以及方法进行处理,从而达到埋点的效果。对于真正运行起来的效果,笔者这边环境有限,暂未深入研究。
安全玻璃盒
机缘巧合的情况下,某匿名好心人听说我在研究IAST,所以将他们购买的一套IAST让我远程看了下。
通过对系统的查看,我发现这个系统和我上面说到的Contrast有点类似,并没有对于中间的传播点进行覆盖,仅仅是对于source以及sink点进行了埋点。
由于安全玻璃盒也对agent进行了混淆处理,所以没有办法直观的看到其内部运行的逻辑。
抛开其IAST的技术实现逻辑,在整体界面上,以及使用情况下,安全玻璃盒可能更符合国人的习惯。
0x03 IAST实现总结
本篇笔者就个人对被动式IAST的理解进行了阐述,也对市场上的部分IAST进行了收集整理,并且提取了其中部分可以拿到agent的IAST进行了原理性总结,可以看到这些Agent都是对字节码进行编辑增强,从而达到一种被动式IAST的效果。
看到这里我有点潜意识的认为,被动式IAST要想实现,那么其实和RASP差不太多,可能多出来的点就是多了一些中间的埋点检测,从而达到对调用链的精准跟踪,在这一细小部分,我个人的理解是,就是对所有有可能导致source获取到的参数进行改变的方法进行埋点,包括但不限于类似以下几种情况(下面仅是伪代码,并不代表真实逻辑中的代码,仅用于更方面的向大家传达我个人的一些理解)
new String(....)
"aa".replace(...)
StringBuilder sb = new StringBuilder();
Base64.decode(...)
等等等等,这个链路是需要根据自己的实际业务情况进行完善的,比如自己实现了个加解密的类等等,又或者是加入对souce进行安全过滤处理的方法,然后将所有经过预埋点的堆栈信息进行拼接,在这个过程中,可以去判断这条链路经过了安全过滤处理方法,那么或许可以粗暴的不上报这条调用链信息,认为这是一个安全的请求(当然这种情况还是要谨慎,毕竟研发中难免会犯一些错误,所以在情况允许的环境下,还是全部上报,交给人工进行复验、排除是更为妥当的解决方式),然后将数据上报到服务端,到此完成一个IAST的技术理念逻辑。
那么其实是不是可以使用一些APM的开源技术,对它进行一些改造,从而实现IAST的部分功能。如果想深度控制IAST的流程,更好的方式就是自己实现一套IAST埋点、检测逻辑。
0x04 被动式IAST实现Demo
通过上一章节,大家应该已经了解到了当前IAST的一些基础知识,如果想要从零实现一个被动式的IAST,我们至少需要掌握关于字节码操作的技术,比如ASM、Javassist等,如果不想从零实现或从那么底层的方式去实现,可以试试看使用AspectJ技术,或者结合使用开源的APM框架进行改造,让其成为一个简单的被动IAST。
本次所涉及的Demo源码已经公开,地址为JAVA_IAST_EXAMPLE。
实验环境搭建:
这次IAST相关的环境其实和之前的RASP环境基本差不多。大家可以参照之前的浅谈RASP技术攻防之实战[环境配置篇]文章内容去搭建一个本地的实验环境,唯一变的,可能就是包名了。
0x05 demo整体逻辑
这次实验的整体逻辑如果相比真正的IAST,肯定会有很多缺少的细节部分完善,所以仅仅适合用来学习了解被动IAST实现的大致流程,整体逻辑图如下:
从上图可以看到,其实在这次demo实现的过程中,逻辑也并不是很复杂,大致文字版说明如下:
http->enterHttp->enterSource->leaveSource
enterPropagator->leavePropagator(…………此过程重复n次…………)
enterSink->leaveSink(可省略)->leaveHttp
以上大致完成了整个污点跟踪链路流程,在初始化HTTP的时候,将新建一个LinkedList<CallChain>
类型的对象,用来存储线程链路调用的数据。
为了方便对不同类型的点进行适配,抽象了一个Handler
出来,然后在根据不同的类型实现具体的ClassVisitorHandler
内容,Handler.java
具体代码如下:
package cn.org.javaweb.iast.visitor;
import org.objectweb.asm.MethodVisitor;
/**
* @author iiusky - 03sec.com
*/
public interface Handler {
MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access, String name, String desc, String signature, String[] exceptions);
}
0x06 实现Http埋点
在Java EE中通过劫持javax.servlet.Servlet
的service
方法和javax.servlet.Filter
类的doFilter
方法不但可以获取到原始的HttpServletRequest
和HttpServletResponse
对象,还可以控制Servlet和Filter的程序执行逻辑。
可以将所有参数描述符为(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V
的方法进行插入埋点,并缓存request、response对象。
实现的代码如下(示例代码为了便于理解未考虑异常处理):
package cn.org.javaweb.iast.visitor.handler;
import cn.org.javaweb.iast.visitor.Handler;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import java.lang.reflect.Modifier;
/**
* @author iiusky - 03sec.com
*/
public class HttpClassVisitorHandler implements Handler {
private static final String METHOD_DESC = "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V";
public MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access,
String name, String desc, String signature, String[] exceptions) {
if ("service".equals(name) && METHOD_DESC.equals(desc)) {
final boolean isStatic = Modifier.isStatic(access);
final Type argsType = Type.getType(Object[].class);
System.out.println(
"HTTP Process 类名是: " + className + ",方法名是: " + name + "方法的描述符是:" + desc + ",签名是:"
+ signature + ",exceptions:" + exceptions);
return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
protected void onMethodEnter() {
loadArgArray();
int argsIndex = newLocal(argsType);
storeLocal(argsIndex, argsType);
loadLocal(argsIndex);
if (isStatic) {
push((Type) null);
} else {
loadThis();
}
loadLocal(argsIndex);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Http", "enterHttp",
"([Ljava/lang/Object;)V", false);
}
@Override
protected void onMethodExit(int i) {
super.onMethodExit(i);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Http", "leaveHttp", "()V",
false);
}
};
}
return mv;
}
}
上面的代码将对所有实现javax.servlet.Servlet#service的方法进行了埋点处理(接口、抽象类除外),真正编译到jvm中的类如下:
可以看到,在对进入方法的时候调用了IAST中的方法cn.org.javaweb.iast.core.Http#enterHttp
,在离开方法的时候,调用了cn.org.javaweb.iast.core.Http#leaveHttp
其中enterHttp
具体代码如下:
public static void enterHttp(Object[] objects) {
if (!haveEnterHttp()) {
IASTServletRequest request = new IASTServletRequest(objects[0]);
IASTServletResponse response = new IASTServletResponse(objects[1]);
RequestContext.setHttpRequestContextThreadLocal(request, response, null);
}
}
从上文中可以看到,传入的HttpServletRequest
和HttpServletResponse
对象存到了当前线程的上下文中,方便后续对数据的调取使用。
leaveHttp
具体代码如下:
public static void leaveHttp() {
IASTServletRequest request = RequestContext.getHttpRequestContextThreadLocal()
.getServletRequest();
System.out.printf("URL : %s \n", request.getRequestURL().toString());
System.out.printf("URI : %s \n", request.getRequestURI().toString());
System.out.printf("QueryString : %s \n", request.getQueryString().toString());
System.out.printf("HTTP Method : %s \n", request.getMethod());
RequestContext.getHttpRequestContextThreadLocal().getCallChain().forEach(item -> {
if (item.getChainType().contains("leave")) {
String returnData = null;
if (item.getReturnObject().getClass().equals(byte[].class)) {
returnData = new String((byte[]) item.getReturnObject());
} else if (item.getReturnObject().getClass().equals(char[].class)) {
returnData = new String((char[]) item.getReturnObject());
} else {
returnData = item.getReturnObject().toString();
}
System.out
.printf("Type: %s CALL Method Name: %s CALL Method Return: %s \n", item.getChainType(),
item.getJavaClassName() + item.getJavaMethodName(), returnData);
} else {
System.out
.printf("Type: %s CALL Method Name: %s CALL Method Args: %s \n", item.getChainType(),
item.getJavaClassName() + item.getJavaMethodName(),
Arrays.asList(item.getArgumentArray()));
}
});
}
其实就是从当前线程中获取到在调用enterHttp
时候存的数据,然后对其中的数据进行可视化的输出打印。
0x07 实现Source埋点
在Java EE中通过可以劫持获取输入源的所有方法,比如常用的getParameter
、getHeader
等类似的方法,在这里将对调用的方法、以及返回的参数进行跟踪,这里为真正污点跟踪的起点。可以简单的理解为就是http各个get方法即为来源,但这一结论不保证完全适配所有情况。
对于Source相关的点处理的代码如下(示例代码为了便于理解未考虑异常处理):
package cn.org.javaweb.iast.visitor.handler;
import cn.org.javaweb.iast.visitor.Handler;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import java.lang.reflect.Modifier;
/**
* @author iiusky - 03sec.com
*/
public class SourceClassVisitorHandler implements Handler {
private static final String METHOD_DESC = "(Ljava/lang/String;)Ljava/lang/String;";
public MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access, final String name,
final String desc, String signature, String[] exceptions) {
if (METHOD_DESC.equals(desc) && "getParameter".equals(name)) {
final boolean isStatic = Modifier.isStatic(access);
System.out.println("Source Process 类名是: " + className + ",方法名是: " + name + "方法的描述符是:" + desc + ",签名是:" + signature + ",exceptions:" + exceptions);
return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
protected void onMethodEnter() {
loadArgArray();
int argsIndex = newLocal(Type.getType(Object[].class));
storeLocal(argsIndex, Type.getType(Object[].class));
loadLocal(argsIndex);
push(className);
push(name);
push(desc);
push(isStatic);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Source", "enterSource", "([Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V", false);
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
Type returnType = Type.getReturnType(desc);
if (returnType == null || Type.VOID_TYPE.equals(returnType)) {
push((Type) null);
} else {
mv.visitInsn(Opcodes.DUP);
}
push(className);
push(name);
push(desc);
push(isStatic);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Source", "leaveSource", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V", false);
super.onMethodExit(opcode);
}
};
}
return mv;
}
}
其实以上代码的逻辑,只是简单的对于getParameter
进行了埋点处理,让其调用IAST的处理逻辑,编译到JVM的Class内容如下:
可以看到,在进入方法后调用了cn.org.javaweb.iast.core.Source#enterSource
,具体内容如下
public static void enterSource(Object[] argumentArray,
String javaClassName,
String javaMethodName,
String javaMethodDesc,
boolean isStatic) {
if (haveEnterHttp()) {
CallChain callChain = new CallChain();
callChain.setChainType("enterSource");
callChain.setArgumentArray(argumentArray);
callChain.setJavaClassName(javaClassName);
callChain.setJavaMethodName(javaMethodName);
callChain.setJavaMethodDesc(javaMethodDesc);
callChain.setStatic(isStatic);
RequestContext.getHttpRequestContextThreadLocal().addCallChain(callChain);
}
}
其实就是对参数、类名、方法名、描述符等信息添加到了callChain中.
在方法结束前获取了返回值,并且调用了cn.org.javaweb.iast.core.Source#leaveSource
方法,将返回值传入了进去,那么在处理的时候,就将其结果放到了callChain.returnObject
。
0x08 实现Propagator埋点
传播点的选择是非常关键的,传播点规则覆盖的越广得到的传播链路就会更清晰。比如简单粗暴的对String
、Byte
等类进行埋点,因为中间调用这些类的太多了,所以可能导致一个就是结果堆栈太长,不好对调用链进行分析,但是对于传播点的选择,可以更精细化一些去做选择,比如Base64
的decode
、encode
也可以作为传播点进行埋点,以及执行命令的java.lang.Runtime#exec
也是可以作为传播点的,因为最终执行命令是最底层在不同系统封装的调用执行命令JNI方法的类,如java.lang.UNIXProcess
等,所以将java.lang.Runtime#exec
作为传播点也是一个选择。为了方便演示污点传播的效果,对Base64
的decode
以及encode
和java.lang.Runtime
进行了埋点处理,具体实现代码如下(示例代码为了便于理解未考虑异常处理):
package cn.org.javaweb.iast.visitor.handler;
import cn.org.javaweb.iast.visitor.Handler;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import java.lang.reflect.Modifier;
/**
* @author iiusky - 03sec.com
*/
public class PropagatorClassVisitorHandler implements Handler {
private static final String METHOD_DESC = "(Ljava/lang/String;)[B";
private static final String CLASS_NAME = "java.lang.Runtime";
@Override
public MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access,
final String name, final String desc, String signature, String[] exceptions) {
if ((name.contains("decode") && METHOD_DESC.equals(desc)) || CLASS_NAME.equals(className)) {
final boolean isStatic = Modifier.isStatic(access);
final Type argsType = Type.getType(Object[].class);
if (((access & Opcodes.ACC_NATIVE) == Opcodes.ACC_NATIVE) || className
.contains("cn.org.javaweb.iast")) {
System.out.println(
"Propagator Process Skip 类名:" + className + ",方法名: " + name + "方法的描述符是:" + desc);
} else {
System.out
.println("Propagator Process 类名:" + className + ",方法名: " + name + "方法的描述符是:" + desc);
return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
protected void onMethodEnter() {
loadArgArray();
int argsIndex = newLocal(argsType);
storeLocal(argsIndex, argsType);
loadLocal(argsIndex);
push(className);
push(name);
push(desc);
push(isStatic);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Propagator",
"enterPropagator",
"([Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V",
false);
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
Type returnType = Type.getReturnType(desc);
if (returnType == null || Type.VOID_TYPE.equals(returnType)) {
push((Type) null);
} else {
mv.visitInsn(Opcodes.DUP);
}
push(className);
push(name);
push(desc);
push(isStatic);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Propagator",
"leavePropagator",
"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V",
false);
super.onMethodExit(opcode);
}
};
}
}
return mv;
}
}
真正运行在JVM中的类如下:
java.util.Base64$Decoder#decode
java.lang.Runtime
可以看到其实也是在方法进入后和方法离开前插入了IAST的代码逻辑,以便可以直观的观察到入参值以及返回值发生的变化。
0x09 实现Sink埋点
对于Sink点的选择,其实和找RASP最终危险方法的思路一致,只限找到危险操作真正触发的方法进行埋点即可,比如java.lang.UNIXProcess#forkAndExec
方法,这种给java.lang.UNIXProcess#forkAndExec
下点的方式太底层,如果不想这么底层,也可以仅对java.lang.ProcessBuilder#start
方法或者java.lang.ProcessImpl#start
进行埋点处理。本次实验选择了对java.lang.ProcessBuilder#start
进行埋点处理,具体实现代码如下(示例代码为了便于理解未考虑异常处理):
package cn.org.javaweb.iast.visitor.handler;
import cn.org.javaweb.iast.visitor.Handler;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import java.lang.reflect.Modifier;
/**
* @author iiusky - 03sec.com
*/
public class SinkClassVisitorHandler implements Handler {
private static final String METHOD_DESC = "()Ljava/lang/Process;";
@Override
public MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access,
final String name, final String desc, String signature, String[] exceptions) {
if (("start".equals(name) && METHOD_DESC.equals(desc))) {
final boolean isStatic = Modifier.isStatic(access);
final Type argsType = Type.getType(Object[].class);
System.out.println("Sink Process 类名:" + className + ",方法名: " + name + "方法的描述符是:" + desc);
return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
protected void onMethodEnter() {
loadArgArray();
int argsIndex = newLocal(argsType);
storeLocal(argsIndex, argsType);
loadThis();
loadLocal(argsIndex);
push(className);
push(name);
push(desc);
push(isStatic);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/iast/core/Sink", "enterSink",
"([Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V",
false);
super.onMethodEnter();
}
};
}
return mv;
}
}
在这次实验中,选择了对所有方法名为start
且方法描述为()Ljava/lang/Process;
的类进行埋点,其实也就是对java.lang.ProcessBuilder#start
进行埋点处理。
最终运行在JVM中的class如下:
可以看到在方法进去后调用了IAST的cn.org.javaweb.iast.core.Sink#enterSink
方法,以此来确定一个调用链是否已经到达危险函数执行点。对于Sink,除了整体处理逻辑与Propagator
以及Source
相似,多了一个setStackTraceElement
的操作,目的是将在触发Sink
点的堆栈将其保存下来,方便后面使用分析。
具体代码如下
public static void enterSink(Object[] argumentArray,
String javaClassName,
String javaMethodName,
String javaMethodDesc,
boolean isStatic) {
if (haveEnterHttp()) {
CallChain callChain = new CallChain();
callChain.setChainType("enterSink");
callChain.setArgumentArray(argumentArray);
callChain.setJavaClassName(javaClassName);
callChain.setJavaMethodName(javaMethodName);
callChain.setJavaMethodDesc(javaMethodDesc);
callChain.setStatic(isStatic);
callChain.setStackTraceElement(Thread.currentThread().getStackTrace());
RequestContext.getHttpRequestContextThreadLocal().addCallChain(callChain);
}
}
0x10 结果验证
全部实现完成后,写一个jsp来执行命令试试看,代码如下:
该JSP接收一个参数,然后对该参数进行base64解码后传入Runtime.exec中来执行命令,最后输出执行结果
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Base64" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<pre>
<%
String sb = request.getParameter("cmd");
byte[] decode = Base64.getDecoder().decode(sb);
Process process = Runtime.getRuntime().exec(new String(decode));
InputStream in = process.getInputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) {
out.println(new String(b, 0, a));
}
in.close();
%>
</pre>
接着编译agent,将其加入到tomcat的启动命令中,部署jsp页面,访问看看结果
可以看到,首先触发了getParameter
方法中的Source埋点,传入的参数为cmd
,获取到的结果为CHdK
,接着连续触发了5次Propagator点。
第一次触发的Propagator点位Base64
类中decode
方法,传入的参数是CHdK
,返回值为pwd
(原始返回为[]byte,为了方便展示,对其转为了字符串),这时候已经可以初步看到了参数的获取到base64解码,也就是原始source点已经发生了变化。
第二次触发的埋点信息为获取一个Runtime
对象,调用的是java.lang.Runtime#getRuntime
,传入的参数为空,返回的结果为一个Runtime的对象信息,其实就是实例化了一个java.lang.Runtime
对象,这次可以观察到一个小细节,就是这个返回对象发生了变化,但是并没有传入任何参数。
第三次触发的埋点信息为调用java.lang.Runtime#exec
方法(接收参数类型为:String
),传入的值是pwn
,在这次调用中可以看到,第一次Propagator点的返回值作为了入参传入了这次调用,但是紧接着并触发没有想象中的leavePropagator
方法,而是调用了另一个exec
方法。
第四次触发的埋点信息为调用java.lang.Runtime#exec
方法(接收参数类型为:String、String[]、File
),其中第一个参数的值为pwn
,而其它参数为null
(本文不讨论如何确定第几个参数是污染点的问题,这个可以通过加规则去逐步完善)。在这次调用中可以看到,第三次中传递过来的pwn
没有发生变化,然而也没有触发leavePropagator
方法,由此可以推测出来这个方法内部继续调用了在规则里面预先匹配到的方法。
第五次触发的埋点信息为调用java.lang.Runtime#exec
方法(接收参数类型为:String[]、String[]、File
),传入的值是[[Ljava.lang.String;@58ed07d8, null, null]
,这时候就看到了在传入的值由pwn
变为了一个String
数组类型的对象,返回到第四次触发的埋点看,其实就可以看到var
6其实是最开始是由var1
,也就是入参值pwn
转换得到的。然后可以看到在当前调用的方法里面,又调用了规则中的Sink点(java.lang.ProcessBuilder#start
)方法。
以上就是大概从Srouce点(getParameter
),经过中间的Propagator点(java.util.Base64$Decoder#decode、java.lang.Runtime#getRuntime、java.lang.Runtime#exec
)到最终Sink点(java.lang.ProcessBuilder#start
)的整体大概流程了。
0x11 总结
在本次实验中,将java.lang.Runtime
作为了传播点,其实在整体流程访问结束后,这个传播点才会有返回值返回回来,他是在传播的过程中调用到了Sink点。
那么对于这种情况,是否应该摒弃将java.lang.Runtime
作为传播点呢?这其实应该就是仁者见仁智者见智了,对于整体IAST的流程,其实和RASP流程差不多,但是对于传播点的选择,目前大家更多的是基于规则(正则or继承类)判断去覆盖其中的传播链,或者更简单粗暴的对String
、Byte
进行埋点,但是需要处理的细节也就更多了,以及对于在整条链路中的无用调用也需要处理。是否有一种一劳永逸的办法可以完整的拿到整条污点传播链路,从而抛弃基于规则的对传播点进行人为覆盖,这个可能就需要进行更加深入的研究了。
在这次实现的demo中,并没有结合真正业务去实现,以及IAST的其它功能点去展开研究,比如流量重放、SCA、污点在方法中的参数位置等功能。如果仅仅是想融入DevSecOps中,可以基于开源的APM项目实现一个简易的IAST,根据具体的一些公司开发规范,去定制一些规则点,来减少因为某些问题导致的误报情况。
参考链接
- https://www.freebuf.com/sectool/290671.html
- https://www.gartner.com/reviews/market/application-security-testing
- https://doc.dongtai.io/
- http://rui0.cn/archives/1175
- https://dzone.com/refcardz/introduction-to-iast
- https://blog.secodis.com/2015/11/26/the-emerge-of-iast/
- https://www.03sec.com/Ideas/qian-tanrasp-ji-shu-gong-fang-zhi-shi-zhan-huan-ji.html