从传统到 AI 探讨 Webshell 检测攻防对抗
一、前言 近些年,各个厂商常常举办WebShell绕过挑战赛,用以检测其WebShell检测引擎的稳定性与检出能力。结合一些比赛我的参赛经历以及之前对公司终端产品的WebShell检测引擎的攻防对抗经历,聊一聊WebShell检测的绕过思路。 二、引擎行为 在思考如何绕过前,首先需要明确的是,检测引擎究竟会拦截什么行为。一个WebShell检测引擎,往往会结合多种检测方法进行检测,因此我们需要拆分检测方法,再基于每个引擎的拦截方法思考对应的绕过策略,最后将各引擎的绕过方法进行整合,从而实现完整的绕过。对于拦截的内容,由于引擎对于我们来说是黑盒,因此只能通过反复测试的方法去确认引擎的拦截方式。 根据测试可以发现,检测引擎常常会使用以下几类方法进行行为检测。 静态检测。这里主要指的是源码规则的静态匹配手段,通过对已知的WebShell特征和模式(比如特定的字符串或者代码结构),例如Runtime.getRuntime().exec()、ProcessBuilder().start()等进行匹配,从而达到检测的目的。这种方法的优点是检测速度快,实现成本低,但是缺点是极其依赖文本特征的提取,容易误报也容易被绕过。 动态沙箱。将样本放在模拟的环境中执行,模拟攻击者的输入,通过hook危险函数进行检测。这种方法优点是不强依赖知识和规则,可以检测出一些未知的WebShell样本,但最大的缺点是它无法准确模拟出攻击者的外部输入,从而导致模拟执行的输入和真实攻击的输入不同,代码走向不同,形成绕过。 模拟污点执行。将样本进行词法和语法分析形成AST,通过对用户可控的数据标记为污点Source,结合对节点进行静态的遍历分析以及动态的模拟执行代码,判断污点是否可以传递到危险函数Sink,从而进行WebShell的检测。这种方法的优点是可以结合了静态和动态的分析技术,误报率相对较低,但缺点是实现相对复杂,污点传播可能由于编程语言的trick、特性等问题导致规则覆盖不全,形成绕过。 三、绕过方法 (一)静态检测绕过 针对引擎的静态检测,应对方法就是尽量去寻找一些不常见的命令/代码执行方法,这些方法最终调用了了危险的代码执行/命令执行sink,如果这些方法没有在目标引擎的匹配规则里,就可以实现绕过。在Java中最常见的命令执行方法是如下两种: Runtime.getRuntime().exec() new ProcessBuilder().start() 在Tabby中分别查找JDK11中调用了这两个方法的方法: 可以发现链不是很多,逐一手动分析。由于反射的代码特征相对明显,因此尽量减少对非公共方法或者类的依赖。整理各个关键类的特性如下 com.sun.tools.jdi.AbstractLauncher 的两个实现类:公共类,执行命令的方法为公共方法 sun.security.krb5.internal.ccache.FileCredentialsCache$2:内部匿名类,无公共调用方法 sun.net.www.MimeLauncher: 非公共类 jdk.internal 内的多个类:属于jdk.internal模块,Tomcat的WebShell默认情况下访问不到该模块,需要使用反射等方法进行类加载,动静比较大。如果目标引擎不会拦截反射可以考虑使用 com.sun.tools.jdi.AbstractLauncher的两个实现类无疑是最符合要求的。两个类差不多,这里以com.sun.tools.jdi.SunCommandLineLauncher举例分析。其存在一个public的launch方法,通过对参数的一系列赋值,对传入的命令进行字符串拼接,调用其父类的launch方法。 public VirtualMachine launch(Map<String, ? extends Connector.Argument> arguments) throws IOException, IllegalConnectorArgumentsException, VMStartException { VirtualMachine vm; String home = argument(ARG_HOME, arguments).value(); String exe = argument(ARG_VM_EXEC, arguments).value(); ... try { if (home.length() > 0) { exePath = home + File.separator + "bin" + File.separator + exe; } else { exePath = exe; } ....