去年的WebShell引擎检测绕过思路分享中,主要介绍了当下主流引擎对WebShell检测引擎的几种检测方法,再针对各个检测方法,逐一的利用Java语法的trick去进行绕过。重心放在了检测引擎的行为上,依赖对Java语法和trick的先验知识进行绕过。在今年的比赛中,去年文中列出的绕过方法基本上已经被引擎修复完成。结合今年阿里举办的恶意代码检测挑战赛的比赛经历,分享一下在已有的trick都被ban,如何从0研究出新的绕过思路,把重心转移到WebShell本身上,通过分析jsp的解析过程,挖掘绕过方法。
Tomcat处理jsp的核心的逻辑是它实现了一个处理jsp的Servlet:org.apache.jasper.servlet.JspServlet,这个Servlet处理所有以jsp为后缀的请求。
当我们上传一个jsp文件后,在这个service处下断点,然后请求这个jsp,跟进代码,程序在org.apache.jasper.servlet.JspServletWrapper中调用如下代码进行编译
这是一个JspCompilationContext对象,它在JspServletWrapper的构造方法中被生成,其中jspuri是文件名,options保存了jsp文件的参数信息。
整个从jsp到生成class的编译过程都是发生在JspCompilationContext的compile方法中
这里的JspCompilationContext类和compile方法都是public属性,所以其实可以直接在jsp中用如下写法去编译任意jsp文件,而且参数都是可控的。
那么这里就存在着一个理论上可行的绕过点:我们是否可以上传一个jsp文件,这个文件在被上传和执行时,不存在恶意特征,然后我们通过控制参数,从而使再次编译时触发恶意命令,形成二次编译。接下来就可以带着这个目标去寻找可以被利用的特征。
继续往下看,编译jsp文件的最终实现类是org.apache.jasper.compiler.Compiler
这个类的compile方法对jsp进行了编译。
代码中的方法名非常明显,generateJava方法代表着生成java文件。跟进该方法。
前面的部分主要是通过调用如下代码:
生成一个pageInfo对象,接着获取jsp文件中的属性,后续根据属性的不同进行不同的配置。通过一连串的if进行选项配置。
这些配置在jsp可控,那就可以通过控制一些特定的配置,使样本看起来有一点点的反常,如果引擎未能识别到这种参数,那就存在绕过的可能。一个典型的例子是p牛分享过的,利用trimDirectiveWhitespaces属性忽略jsp不同块之间的空白字符。
样本如下:
对应到代码上是:
不加该属性,生成的java文件会是这样:
添加属性后,输出的java文件不会再被添加换行。
generateJava接着会调用下面的代码:
调试可以发现这段代码会返回jsp编译的java文件的路径
接着又是一些处理jsp的标签和控制信息的操作,创建了一个ParserController对象,并调用其parseDirectives和parse方法对jsp文件进行读取和解析,保存到directives和pageNodes对象中。
跟进其解析jsp文件的函数。在parserCtl.parseDirectives中,会用其doParser方法
其中的determineSyntaxAndEncoding会调用getPageEncodingForJspSyntax方法,内容如下:
也就是通过jsp的pageEncoding和contentType等配置去设置读取jsp文件的编码。根据代码的含义,支持如下写法:
利用编码绕过也已经是的老生常谈的话题了,一个常见的例子:
如果引擎不能识别这种编码,则会形成绕过。
接着设置一个ServletWriter对象。这里的ServletWriter实际上是一个指向生成java文件的PrintWriter对象的封装。生成前,会从Options中获取JavaEncoding属性的值,作为文件写入的编码。
接着调用Generator.generate。这个方法实际上就是对jsp到java代码写入的实现类。函数先通过generateCommentHeader、generatePreamble以及generateXmlProlog等方法写入一些注释、xml的控制信息,接着通过观察者模式创建了一个GenerateVisitor对象,根据传入的代码类型不同,调用不同的Node进行处理。
jsp中输入的<%…%>块代码代码被Scriptlet类捕获,并调用其visit方法
可以看到n.getText()被直接out.printMultiLn到了文件中。没有进行任何过滤的直接拼接到了文件里。
以一个WebShell为例:
最后生成的文件如下,被<% %>包裹的代码直接被打印到了_jspService函数中:
那么很容易想到,是否可以在jsp中注入代码,闭合掉其他上面的_jspService方法,再闭合掉后续的代码,新建一个函数或者代码块进行恶意操作执行。这样生成的jsp会看起来是个不正常的jsp文件。
实现如下:
再次访问WebShell,生成的java文件如下,成功的新生成了一个名为foo的函数,并在原本的_jspService逻辑中进行了调用。
如果引擎只是对jsp文件本身进行分析,面对这种畸形的jsp文件,可能数据流分析之类的分析方法就无法正常运行。
除了jsp中直接输入的<%…%>代码,在Generator类中还可以看到程序对于很多jsp的参数属性信息也是直接使用print输出到java文件中,因此都存在注入的问题。
比如在jsp文件中,在处理jsp代码前通过generateXmlProlog函数处理xml信息,代码如下:
在这个函数中,如果包含了DoctypeName、Doctypepublic及DoctypeSystem等属性,那么就会将这部分内容打印到java文件中。
这部分内容的本意是在在java文件中加入一句out.write(“<!DOCTYPE NAME SYSTEM DOCTYPESYSTEM “); 但是我们可以在doctypeName等参数中加入”);以及在doctypeSystem添加out.print(” 对原本的代码进行闭合,从而注入新的恶意代码。样本如下:
访问jsp文件,生成的java文件如下:
原样本中被引号包裹的字符串,最终被当作了代码进行执行,并且对象的传递放在了不同的字符串中,同样引擎如果没能正确编译jsp文件后再检测,就会存在漏报的可能。
到这里从jsp到java的过程就结束了,现在回过头来思考文章之前提到的那个问题,是否有什么控制参数,可以让我们的jsp文件被编译成功,并且我们通过设置这个参数,让jsp再次编译时产生不同的效果?要解决这个问题需要先回顾哪里存在控制参数。前文提到的第一处是在pageInfo生成时一连串的if。就像p牛的那个例子,但同样以那个例子为例,如果没有 trimDirectiveWhitespaces='true',则jsp文件在第一次被上传时就无法正确解析,也就无法被利用。
前文提到的第二处控制参数配置在那段代码之后。程序通过ParserController对象解析jsp中的编码设置,对jsp文件使用特定编码进行读取,在那以后会从Options对象中,获取javaEncoding属性,对输出的java文件进行输出。这里的Options是代码中可控的,所以我们可以控制代码在被运行时,再用一个指定的编码控制输出文件的编码。综合这些思路,提出一种理论上可行的绕过方法:
1. 寻找两种编码,这两种编码对换行符之类的控制字符存在解析差异。即第一种编码不会将解析成换行符而是一个普通的字符,第二种编码会解析为换行符。并且这两种编码在解析其他文本时存在尽量少的区别;
2. jsp文件在pageEncoding中标注为第一种编码,此时上传上去的文件会以这种编码解析文件。其中以注释符//开头,并在其后写恶意代码。此时由于编码会解析为普通字符,因此这段代码只是一段注释,不会被保存到java文件中,更不会被解析;
3. 在jsp的代码中包含如下代码:
当jsp运行时,就会使用编码二设置Java文件的PrintWriter编码,编码二会把识别成换行,恶意的代码也就逃逸了出来,从而后续被编译和执行。
思路清楚,问题就是如何找到这样的两种编码。使用下面的代码fuzz一下:
可以找到不少满足条件的编码,但是大部分的编码存在其他的解析差异,导致无法使用,因此还需要手动测试一下。
最后找到两种满足条件的编码x-IBM1097和IBM1026,制作jsp文件的代码如下:
最终效果如下,在tomcat的jsp编译过程中,生成的java文件长这样:
恶意代码被注释掉了。
但是随着程序运行,jsp中的其他代码被执行,此java文件会被重新写入,此时java文件长这样:
从而被成功执行。
本文没有分享太多的绕过样本,而是着重分享了如何从0开始挖掘绕过的思路。本文中分享的Tomcat解析jsp代码只是冰山一角,Tomcat源码中还有许多对于jsp中各种标签、语法的解析代码,以及后续的从java到class文件的编译过程,都存在着未被发现的可以用来绕过的可能。
[1] 浅谈JspWebshell之编码
[2] 知识星球
【版权说明】
本作品著作权归YYHY所有
未经作者同意,不得转载
天工实验室安全研究员
专注于代码审计、Web安全
原文始发于微信公众号(破壳平台):阿里云WebShell伏魔挑战赛新思路挖掘
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论