Full Stack Web Attack RCEME Challenge Walkthrough

Ka Wing Ho, a Senior Consultant within Sekuro RED (our talented Offensive Security Team), shares his solution to the RCEME Preparation Challenge from the renowned Full Stack Web Attack (FSWA) Course. 

Table of Contents
    Add a header to begin generating the table of contents
    Scroll to Top

    Prework

    As mentioned on the SourceIncite website:

    Additionally, before signing up for this course students should complete the challenge to self assess if this course is right for them.

    Before proceeding, I highly HIGHLY recommend you give it a try yourself before reading the spoilers below.

    TL;DR

    As someone who has tried to do source code review in the past and failed miserably, this challenge helped me to learn:

    • How to use tools like ysoserial and JD-GUI
    • Patch diffing (and comparing codebase differences)
    • Nuances in Python3 and Java (read: pain)
    • Java Deserialization primitives and their signs/tells

    The challenge is not as straightforward as it might seem, as explained further in the blog!

    You may ask: Is this challenge a true representation of the course?

    Hard to say, because everyone will probably take different paths to solve this challenge. I had a friend who wrote their Proof-of-Concept in Java while I wrote mine in Python.

    I will follow up with a future blog post talking more about my experience taking the course!

    Running the Challenge

    As of the time of writing, the command to start the docker container is as follows:

    				
    					docker run -it --rm -p 8888:8080 registry.gitlab.com/source-incite/fswa-challenge/rceme:latest
    				
    			

    Browsing to http://localhost:8888/ reveals the following page:

    What is Apache Shiro?

    According to the source:

    Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

    After browsing around looking for functionality, the target application – an Apache Shiro sample running on an Eclipse Jetty server, gives you some dummy users which you can log in with to view a dummy account page. Just that, literally nothing else!

    So what next?

    As a seasoned penetration tester who tries harder, my first obvious instinct was to take the path of least resistance, I mean why do the work of writing an exploit when I can just go run something off the Internet that someone else wrote? (note: don’t actually do this without checking the code)

    Consulting the oracle that is the Exploit Database, I found the following:

    2020 is fairly recent, and this also results in remote code execution, could this be it?

    The associated Metasploit module for 48410

    no dice - looks like there’s no quick wins to be had here !

    Actually Understanding the Exploit

    So reading through various advisories on CVE-2016-4437, I eventually came to the conclusion that:
    • Apache Shiro 1.2.4 uses a default encryption key (which is actually visible above in Metasploit)
    • The rememberMe cookie is unsafely deserialized in the backend, which leads to RCE
    Lets have a quick look on the browser-level what the rememberMe functionality looks like:
    checking the ‘Remember Me’ box and logging in
    response in Burp Suite containing the end product

    In this case, the rememberMe cookie is issued on successful login and is sent in subsequent requests to the application.

    On a slightly lower-level, this is how Apache Shiro processes the rememberMe cookie:

    For issuing the cookie to the user:

    1. An object is serialized by the application
    2. The object is encrypted using the encryption key and stored in a cookie
    3. The cookie is base64-encoded
    4. The cookie is issued/passed to the user

    For reading the cookie passed by the user:

    1. User passes the cookie to the application
    2. The cookie is base64-decoded
    3. The cookie is then decrypted using the encryption key to retrieve the object
    4. The object is deserialized by the application
    The attack path then becomes clear:
    1. Generate a malicious Java object to be deserialized (with ysoserial perhaps?)
    2. Encrypt the ysoserial payload with the encryption key
    3. Base64-encode the encrypted payload
    4. Send it off to Apache Shiro within the rememberMe cookie
    5. Apache Shiro does the reverse of everything above
    6. The ysoserial payload is eventually deserialized leading to RCE.
    What I just said but in meme format

    Java Deserialization

    Serialization by itself is a large topic, and Java Deserialization specifically is another can of worms – so for the unfamiliar I highly recommend the following blog:

    What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.

    By @breenmachine What? The most underrated, underhyped vulnerability of 2015 has recently come to my attention, and I'm about to bring it to yours. No one gave it a fancy name, there were no press releases, nobody called Mandiant to come put out the fires. In fact, even though proof of concept code was released...

    The main takeaway for the purposes of this blog though is that serialised objects can be recognised by the signature ac ed 00 05 bytes, or by the rO0 string after passing through base64-encoding.

    However, in the current case the base64-decoded rememberMe cookie value still needs to be decrypted with the secret key to work!

    most definitely not ac ed 00 05

    Following from that, I scoured the Internet and happened upon this blog which had a nifty script to decode the rememberMe cookie and view its contents:

    				
    					# pip install pycrypto
    import sys
    import base64
    from Crypto.Cipher import AES
    def decode_rememberme_file(filename):
        with open(filename, 'rb') as fpr:
            key  =  "kPH+bIxk5D2deZiIxcaaaA=="
            mode =  AES.MODE_CBC
            IV   = b' ' * 16
            encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
            remember_bin = encryptor.decrypt(fpr.read())
        return remember_bin
    if __name__ == '__main__':
        with open("/tmp/decrypt.bin", 'wb+') as fpw:
            fpw.write(decode_rememberme_file(sys.argv[1]))
    				
    			
    The script takes a file with the rememberMe cookie and outputs the decrypted value to /tmp/decrypt.bin:

    no sign of ac ed 00 05

    Repeating this with the sample value in the blog, the serialized object can be seen:

    there it is 🙂

    Looks like there’s still some work to be done! There was definitely something different about the Apache Shiro running in the docker container…

    Reading the source

    At that point, it was then time for me to review the source to find out:

    • Was the cipher key the same in this case?
    • What version of Apache Shiro was currently running in the challenge docker container?

    I first rummaged around the docker container and found the main Web Archive (WAR) file:

    I then copied it out of the container, unpacked the contents and inspected them in Visual Studio Code.

    				
    					docker cp busy_wilson:/var/lib/jetty/webapps/root.war . && unzip root.war
    				
    			
    project folder in VS Code
    Taking a look at the shiro.ini configuration file, there was positive confirmation that the default encryption key was still in use! There was also reference to a rememberMeManager class which I then further explored…
    In this case, there were additional shiro-related dependencies located in the lib folder and referenced in the pom.xml file, so I continued my search there:

    shiro-web and shiro-core used as dependencies within the running Apache Shiro Sample

    The pom.xml file also provided the “version” of Apache Shiro core in use: 2.0.0

    Looking for the class in question, it appeared in the two JAR files as well!

    However, for the uninitiated, a Java Archive (JAR) file contains .class files which aren’t exactly the main .java source but can be decompiled using certain tools.

    JD-GUI Exploration

    I used JD-GUI in this case, as it has support for not only .class files, but entire .jar and .war files which was perfect for the current use case!
    Drilling down into the .war, then individual .jar files, and finally the CookieRememberMeManager.class file .class file

    Diff-ing Codebases

    At this point in time I had made several key observations:

    • The docker container’s version of Apache Shiro was 2.0.0
    • The latest public version of Apache Shiro on github was 1.9.1 (as of time of writing)
    • All publicly known exploits and PoCs target 1.2.4

    The encryption key was the same but the codebases were different, perhaps something different was being done in the backend? Therefore it was worth taking a look at how each version was processing the encryption/decryption, as I was still stuck at that stage from before!

    There is probably a better way to do this, but ultimately I ended up visually comparing the two codebases
    1. Top-side is VS Code showing Apache Shiro 1.2.4 code, which can be found on GitHub
    2. Bottom-side is the JD-GUI of Apache Shiro 2.0.0 running in the challenge container
    I started with the AbstractRememberMeManager.java within both shiro-core repositories, as these were the parent classes inherited by CookieRememberMeManager.java as shown above.
    As seen above, both decrypt functions and constructors operated similarly although with minor differences, I then began mucking around looking at other functions such as getCipherService(), eventually leading me to AesCipherService()
    Finally, the mystery of different behaviour is solved!
    • In Apache Shiro 1.2.4, the encryption/decryption is done in AES CBC mode.
    • In Apache Shiro 1.9.1 / 2.0.0, the encryption/decryption is done in AES GCM mode.

    yoserial

    The yoserial tool is essentially a toolkit to aid in the generation of payloads utilizing gadget chains in well-known dependencies. However, in order to use the tool, one must identify what dependencies are in use by the target application.

    The official SourceIncite solution post already mentions utilizing Common Collections, but I confirmed this by inspecting the dependencies as well:

    The list of dependencies in lib

    Applying what I learnt from the FoxGlove blog mentioned earlier, I also confirmed the vulnerable dependencies on the CLI by grepping for the InvokerTransformer function:

    ysoserial supports payloads that map to many common libraries, two of which are related to the commons-collections-3.2.2.jar as shown above:

    The CommonsBeanutils1 and CommonsCollections1 payloads are suitable types for this target

    From this point on, I simply specified the command to be run and generated the payload:
    				
    					java -jar ysoserial-all.jar CommonsBeanutils1 "touch /tmp/pwned" | base64 -w 0
    
    rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2 ...<snip>...
    				
    			

    Scripting the Exploit

    I then began crafting a Proof-of-Concept script to easily exploit the RCE. The main blocker was how to perform AES-GCM decryption properly in Python?!! After much floundering and experimenting with various solutions through StackOverflow posts, the one that stuck was using the pycryptodome library which supported the GCM mode (native Python crypto did not)
    				
    					wingz@ubuntu:/tmp/demo/$ pip install pycryptodomex
    Collecting pycryptodomex
      Downloading pycryptodomex-3.15.0-cp35-abi3-manylinux2010_x86_64.whl (2.3 MB)
         |████████████████████████████████| 2.3 MB 3.8 MB/s 
    Installing collected packages: pycryptodomex
    Successfully installed pycryptodomex-3.15.0
    				
    			

    AES GCM

    After yet another few frustrating nights of fruitless results, I finally figured out pycryptodome’s AES GCM and got the decryption process working, with help from this StackOverflow post:

    Unlike traditional CBC or EBC, GCM uses a nonce and tag feature in the first 12 and last 16 bytes of the encrypted string.

    I modified the previous decryptor script to use AES GCM and it worked! The cookie was properly decrypted:

    				
    					# for GCM encrytion in newer versions of shiro
    # pip install pycryptodomex
    
    import sys
    import base64
    #from Crypto.Cipher import AES
    from Cryptodome.Cipher import AES
    
    def decode_rememberme_file(filename):
        with open(filename, 'rb') as f:
            key  =  "kPH+bIxk5D2deZiIxcaaaA=="
            data = f.read()
            nonce, tag = data[:16], data[-16:]
            cipher = AES.new(base64.b64decode(key), AES.MODE_GCM, nonce)  # mac_len=16
            remember_bin = cipher.decrypt_and_verify(data[16:-16], tag)
        return remember_bin
    
    if __name__ == '__main__':
        with open("/tmp/cookie", 'wb+') as f:
            f.write(decode_rememberme_file(sys.argv[1]))
    
    				
    			
    The final script combines the AES GCM decryption, ysoserial payload generation and payload delivery to the target application, and can be found here: https://gist.github.com/wingzRED/bf0f91d675a10893fb11032e52818ce4

    Root Cause

    The deserialization vulnerability arose from the org.apache.shiro.lang.codec.io.DefaultSerializer class, where a readObject call can be found in the deserialize function:

    Author:

    Ka Wing Ho

    Ka Wing is a seasoned Senior Consultant within Sekuro RED, our talented Offensive Security Team. He specialises in attack surface reconnaissance, phishing simulation assessments, and web application penetration testing. At Sekuro, Ka Wing delivers projects and presents outcomes and findings to key stakeholders, ranging from C-suite executives and application owners to end developers. Some of Ka Wing's certifications include OSCP, CRTO and CRT.

    Scroll to Top