shiro 1.2.4 (shiro)

Posted by trammel at 2020-02-27

In the "strong net cup" competition held last weekend, there was a topic "colored eggs" that was particularly interesting. The questions are as follows:

(don't ask me why the system font is so strange, I don't know...)

I finally used the UDF of unexpected PostgreSQL solution to solve this problem, but the solution was not found. Just in time, the analysis article of orange God let me find a positive solution: So write this article. This article does not talk about the correct solution, but my personal research notes.

Now the original author has fixed the vulnerability, so we start from the branch before he fixed the vulnerability: Considering the complexity of setting up the environment, you can use this docker warehouse to build the local environment: It should be noted that some URLs in the docker image may be invalid and need to be modified. For the dockerfile I modified, see

At the beginning of this article, there are several valuable reference articles, and they will be mentioned many times.

Let's get down to business.

When I got this topic, I directly located Shiro 1.2.4 in pom.xml. As mentioned in Article 3 at the beginning, we know that the vulnerability lies in the rememberme cookie passed to the website. By constructing a malicious cookie, Shiro can deserialize the malicious code to realize rce. The format of this cookie is "AES (serialized data)".


But we can't deal with it if we use payload in this article. There are two reasons.

The first reason is the problem of encryption key. The original text takes note of this line of code and thinks that the encryption key is hard coded.

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

This key is defined in the org.apache.shiro.mgt.abstractremembermemanager class, which is inherited by the org.apache.shiro.web.mgt.cookieremembermemanager we are going to use. As mentioned in the variable name, this is "default_key". The key he actually uses is the following two variables, and the hard coded key is initialized in class constructor through setcipherkey.

org.apache.shiro.mgt.AbstractRememberMeManager org.apache.shiro.web.mgt.CookieRememberMeManager setCipherKey private byte[] encryptionCipherKey; private byte[] decryptionCipherKey; public void setCipherKey(byte[] cipherKey) { //Since this method should only be used in symmetric ciphers //(where the enc and dec keys are the same), set it on both: setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }

We may find that no function calls the setcipherkey function. But don't forget that Java world is a world full of configuration files. Let's go back to the configuration file of that project:

setCipherKey $ cat src/main/resources/spring-shiro.xml | grep CookieRememberMe -A5 -n 37: <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> 38- <!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)--> 39- <property name="cipherKey" 40- value="#{T(org.apache.shiro.codec.Base64).decode('cGhyYWNrY3RmREUhfiMkZA==')}"/> <!-- Cookie加密密钥:phrackctfDE!~#$d --> 41- <property name="cookie" ref="rememberMeCookie"/> 42- </bean>

Java's reflection is always magical, isn't it:)

The second reason is the problem of dependence.

This article adds a dependency on commons-collections4, which is not available in our environment this time. Our environment is only commons-collections-3.2.1.

commons-collections4 commons-collections-3.2.1

The author did not elaborate on the role of this dependence. Searching for other articles, I found that the analysis of this loophole in the Chinese Internet is only based on that article, and it is no value at all. First of all, it enlightens me to cherish life and read less second-hand articles(((

This dependency is used to use the commonscollections2 payload of ysoserial. It seems that the readme of this project has payloads of Commons Collections: 3.1. Maybe it can be used. Try it.

CommonsCollections2 commons-collections:3.1

——Of course not. Check catalina.out and find the following error prompt:

catalina.out [02:26:34:462] [WARN] - org.apache.shiro.mgt.DefaultSecurityManager.getRememberedIdentity( - Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals(). Unable to deserialze argument byte array.

This error is thrown at the action point of readObject, which is unserialize.


Let's see the next breakpoint. After shutting down the service through / opt / Tomcat / bin / stop, jpda_address = / opt / Tomcat / bin / JPDA start can restart the service to let the debugger attach.

/opt/tomcat/bin/ stop JPDA_ADDRESS= /opt/tomcat/bin/ jpda start

Tracing the stack, we will find that at this time it throws an error:

// resolveClass: 53, ClassResolvingObjectInputStream ( return ClassUtils.forName(osc.getName()); // forName: 127, ClassUtils (org.apache.shiro.util) // fqcn = [Lorg.apache.commons.collections.Transformer; Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); // loadClass: 228, ClassUtils$ExceptionIgnoringAccessor (org.apache.shiro.util) clazz = cl.loadClass(fqcn);

Finally, no such class was found in JRE.

Notice that the name of fqcn is [lorg. Apache. Commons. Collections. Transformer;. [l is a JVM tag, indicating that it is actually an array, namely transformer []. Article 2 says,

[Lorg.apache.commons.collections.Transformer; [L Transformer[]

ChainedTransformer - the chain of transformers inside this object is an array, thus we cannot use ChainedTransformer at all

Let's analyze it by ourselves and see what serialize has produced.

00000190 6c 65 63 74 69 6f 6e 73 2e 66 75 6e 63 74 6f 72 |lections.functor|000001a0 73 2e 43 68 61 69 6e 65 64 54 72 61 6e 73 66 6f |s.ChainedTransfo|000001b0 72 6d 65 72 30 c7 97 ec 28 7a 97 04 02 00 01 5b |rmer0… (Z... ..[|000001c0 00 0d 69 54 72 61 6e 73 66 6f 72 6d 65 72 73 74 |..iTransformerst|000001d0 00 2d 5b 4c 6f 72 67 2f 61 70 61 63 68 65 2f 63 |.-[Lorg/apache/c|000001e0 6f 6d 6d 6f 6e 73 2f 63 6f 6c 6c 65 63 74 69 6f |ommons/collectio|000001f0 6e 73 2f 54 72 61 6e 73 66 6f 72 6d 65 72 3b 78 |ns/Transformer;x|

Compare this class: here, at 00000 1C, according to the Java serialization specification, [00 0d] is the length of field name, [0x74]: tc_string corresponds to a new string. Because this type is an object, the type needs JVM signature, which is the origin of the "[L" tag.

000001c0 [00 0d] [0x74]: TC_STRING

So why on earth can't the JVM find this class? Look at the following code

package; public class ClassResolvingObjectInputStream extends ObjectInputStream { @Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } } }

Compared with the original objectinputstream:

ObjectInputStream protected Class<?> resolveClass(ObjectStreamClass var1) throws IOException, ClassNotFoundException { String var2 = var1.getName(); try { return Class.forName(var2, false, latestUserDefinedLoader()); } catch (ClassNotFoundException var5) { Class var4 = (Class)primClasses.get(var2); if (var4 != null) { return var4; } else { throw var5; } } }

We can notice that Shiro rewrites the implementation of resolveclass and changes the search method. As an anonymous friend pointed out in the comments of orange's article:

Shiro resolleclass uses classloader. Loadclass() instead of class. Forname(), while classloader. Loadclass does not support loading classes of array type.

Uh huh? What's this? Check it out. Classloader.loadclass will call class.forname later with the following parameters. After that, it's the implementation of the so-called parent delegation that Java interviewers like most.

ClassLoader.loadClass Class.forName

I noticed that if I execute class. Forname in different process, result is different. To exclude the influence of array, I only left the class name. Pictured here.


Why? Note that the call stack here is different from the classloader sent to forname. When I passed the parallel webappclassloader of Tomcat to forname, everything was OK.

But in fact, when you actually call here, you use urlclassloader

Debug, go in and see what's going on.

According to the process, this parallelwebappclassloader will first find the internal cache, and if it cannot be found, it will be handed to urlclassloader. Let's see the path in this picture This, can find, on the contrary just strange. So that's why array type Shiro can't be found. After testing, the array is removed, and it is initialized normally.

I don't know how you feel when you see here. Shiro, due to a buggy implementation of tomcat, is crooked and solves the rce caused by using it with commons Collections: 3.1.


Finally, how can we ensure the security of readObject? Refer to Article 76: writing readObject methods protectively in efficient Java (Second Edition).

In the end, it's a pity to play GG ~