Add path traversal lesson
11
webgoat-lessons/path-traversal/pom.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>path-traversal</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<parent>
|
||||
<groupId>org.owasp.webgoat.lesson</groupId>
|
||||
<artifactId>webgoat-lessons-parent</artifactId>
|
||||
<version>v8.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
</project>
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is part of WebGoat, an Open Web Application Security Project utility. For details, please see http://www.owasp.org/
|
||||
*
|
||||
* Copyright (c) 2002 - 2019 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.
|
||||
*/
|
||||
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.owasp.webgoat.lessons.Category;
|
||||
import org.owasp.webgoat.lessons.Lesson;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PathTraversal extends Lesson {
|
||||
|
||||
@Override
|
||||
public Category getDefaultCategory() {
|
||||
return Category.INJECTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return "path-traversal-title";
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.owasp.webgoat.assignments.AssignmentHints;
|
||||
import org.owasp.webgoat.assignments.AttackResult;
|
||||
import org.owasp.webgoat.session.WebSession;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.springframework.http.MediaType.ALL_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
@RestController
|
||||
@AssignmentHints({"path-traversal-profile.hint1", "path-traversal-profile.hint2", "path-traversal-profile.hint3"})
|
||||
public class ProfileUpload extends ProfileUploadBase {
|
||||
|
||||
public ProfileUpload(@Value("${webgoat.server.directory}") String webGoatHomeDirectory, WebSession webSession) {
|
||||
super(webGoatHomeDirectory, webSession);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/PathTraversal/profile-upload", consumes = ALL_VALUE, produces = APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public AttackResult uploadFileHandler(@RequestParam("uploadedFile") MultipartFile file, @RequestParam(value = "fullName", required = false) String fullName) {
|
||||
return super.execute(file, fullName);
|
||||
}
|
||||
|
||||
@GetMapping("/PathTraversal/profile-picture")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> getProfilePicture() {
|
||||
return super.getProfilePicture();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.owasp.webgoat.assignments.AssignmentEndpoint;
|
||||
import org.owasp.webgoat.assignments.AttackResult;
|
||||
import org.owasp.webgoat.session.WebSession;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class ProfileUploadBase extends AssignmentEndpoint {
|
||||
|
||||
private String webGoatHomeDirectory;
|
||||
private WebSession webSession;
|
||||
|
||||
protected AttackResult execute(MultipartFile file, String fullName) {
|
||||
if (file.isEmpty()) {
|
||||
return failed(this).feedback("path-traversal-profile-empty-file").build();
|
||||
}
|
||||
if (StringUtils.isEmpty(fullName)) {
|
||||
return failed(this).feedback("path-traversal-profile-empty-name").build();
|
||||
}
|
||||
|
||||
var uploadDirectory = new File(this.webGoatHomeDirectory, "/PathTraversal/" + webSession.getUserName());
|
||||
if (uploadDirectory.exists()) {
|
||||
FileSystemUtils.deleteRecursively(uploadDirectory);
|
||||
}
|
||||
|
||||
try {
|
||||
uploadDirectory.mkdirs();
|
||||
var uploadedFile = new File(uploadDirectory, fullName);
|
||||
uploadedFile.createNewFile();
|
||||
FileCopyUtils.copy(file.getBytes(), uploadedFile);
|
||||
if (userAttemptedToSolveLesson(uploadDirectory, uploadedFile)) {
|
||||
return solvedIt(uploadedFile);
|
||||
}
|
||||
return informationMessage(this).feedback("path-traversal-profile-updated").feedbackArgs(uploadedFile.getAbsoluteFile()).build();
|
||||
|
||||
} catch (IOException e) {
|
||||
return failed(this).output(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean userAttemptedToSolveLesson(File expectedUploadDirectory, File uploadedFile) throws IOException {
|
||||
return !expectedUploadDirectory.getCanonicalPath().equals(uploadedFile.getParentFile().getCanonicalPath());
|
||||
}
|
||||
|
||||
private AttackResult solvedIt(File uploadedFile) throws IOException {
|
||||
if (uploadedFile.getCanonicalFile().getParentFile().getName().endsWith("PathTraversal")) {
|
||||
return success(this).build();
|
||||
}
|
||||
return failed(this).build();
|
||||
}
|
||||
|
||||
public ResponseEntity<?> getProfilePicture() {
|
||||
var profilePictureDirectory = new File(this.webGoatHomeDirectory, "/PathTraversal/" + webSession.getUserName());
|
||||
var profileDirectoryFiles = profilePictureDirectory.listFiles();
|
||||
|
||||
if (profileDirectoryFiles != null && profileDirectoryFiles.length > 0) {
|
||||
try (var inputStream = new FileInputStream(profileDirectoryFiles[0])) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
|
||||
.body(Base64.getEncoder().encode(FileCopyUtils.copyToByteArray(inputStream)));
|
||||
} catch (IOException e) {
|
||||
return defaultImage();
|
||||
}
|
||||
} else {
|
||||
return defaultImage();
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private ResponseEntity<?> defaultImage() {
|
||||
var inputStream = getClass().getResourceAsStream("/images/account.png");
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
|
||||
.body(Base64.getEncoder().encode(FileCopyUtils.copyToByteArray(inputStream)));
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.owasp.webgoat.assignments.AssignmentHints;
|
||||
import org.owasp.webgoat.assignments.AttackResult;
|
||||
import org.owasp.webgoat.session.WebSession;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import static org.springframework.http.MediaType.ALL_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
@RestController
|
||||
@AssignmentHints({"path-traversal-profile-fix.hint1", "path-traversal-profile-fix.hint2", "path-traversal-profile-fix.hint3"})
|
||||
public class ProfileUploadFix extends ProfileUploadBase {
|
||||
|
||||
public ProfileUploadFix(@Value("${webgoat.server.directory}") String webGoatHomeDirectory, WebSession webSession) {
|
||||
super(webGoatHomeDirectory, webSession);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/PathTraversal/profile-upload-fix", consumes = ALL_VALUE, produces = APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public AttackResult uploadFileHandler(
|
||||
@RequestParam("uploadedFileFix") MultipartFile file,
|
||||
@RequestParam(value = "fullNameFix", required = false) String fullName) {
|
||||
return super.execute(file, fullName != null ? fullName.replaceAll("../", "") : "");
|
||||
}
|
||||
|
||||
@GetMapping("/PathTraversal/profile-picture-fix")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> getProfilePicture() {
|
||||
return super.getProfilePicture();
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.owasp.webgoat.assignments.AssignmentHints;
|
||||
import org.owasp.webgoat.assignments.AttackResult;
|
||||
import org.owasp.webgoat.session.WebSession;
|
||||
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.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import static org.springframework.http.MediaType.ALL_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
@RestController
|
||||
@AssignmentHints({"path-traversal-profile-remove-user-input.hint1", "path-traversal-profile-remove-user-input.hint2", "path-traversal-profile-remove-user-input.hint3"})
|
||||
public class ProfileUploadRemoveUserInput extends ProfileUploadBase {
|
||||
|
||||
public ProfileUploadRemoveUserInput(@Value("${webgoat.server.directory}") String webGoatHomeDirectory, WebSession webSession) {
|
||||
super(webGoatHomeDirectory, webSession);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/PathTraversal/profile-upload-remove-user-input", consumes = ALL_VALUE, produces = APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public AttackResult uploadFileHandler(@RequestParam("uploadedFileRetrieval") MultipartFile file) {
|
||||
return super.execute(file, file.getOriginalFilename());
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.owasp.webgoat.assignments.AssignmentEndpoint;
|
||||
import org.owasp.webgoat.assignments.AssignmentHints;
|
||||
import org.owasp.webgoat.assignments.AttackResult;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.token.Sha512DigestUtils;
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.springframework.util.FileCopyUtils.copy;
|
||||
import static org.springframework.util.ResourceUtils.getFile;
|
||||
|
||||
@RestController
|
||||
@AssignmentHints({"path-traversal-profile-retrieve.hint1", "path-traversal-profile-retrieve.hint2", "path-traversal-profile-retrieve.hint3, path-traversal-profile-retrieve.hint4", "path-traversal-profile-retrieve.hint5"})
|
||||
@Slf4j
|
||||
public class ProfileUploadRetrieval extends AssignmentEndpoint {
|
||||
|
||||
private final File catPicturesDirectory;
|
||||
|
||||
public ProfileUploadRetrieval(@Value("${webgoat.server.directory}") String webGoatHomeDirectory) {
|
||||
this.catPicturesDirectory = new File(webGoatHomeDirectory, "/PathTraversal/" + "/cats");
|
||||
this.catPicturesDirectory.mkdirs();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initAssignment() {
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
try {
|
||||
copy(getFile(getClass().getResource("/images/cats/" + i + ".jpg")), new File(catPicturesDirectory, i + ".jpg"));
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to copy pictures", e);
|
||||
}
|
||||
}
|
||||
var secretDirectory = this.catPicturesDirectory.getParentFile().getParentFile();
|
||||
try {
|
||||
Files.writeString(secretDirectory.toPath().resolve("path-traversal-secret.jpg"), "You found it submit the SHA-512 hash of your username as answer");
|
||||
} catch (IOException e) {
|
||||
log.error("Unable to write secret in: {}", secretDirectory, e);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/PathTraversal/random")
|
||||
protected AttackResult execute(@RequestParam(value = "secret", required = false) String secret) {
|
||||
if (Sha512DigestUtils.shaHex(getWebSession().getUserName()).equalsIgnoreCase(secret)) {
|
||||
return success(this).build();
|
||||
}
|
||||
return failed(this).build();
|
||||
}
|
||||
|
||||
@GetMapping("/PathTraversal/random")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> getProfilePicture(@RequestParam(value = "id", required = false) String id) {
|
||||
var catPicture = new File(catPicturesDirectory, (id == null ? RandomUtils.nextInt(1, 11) : id) + ".jpg");
|
||||
|
||||
try {
|
||||
if (catPicture.getName().toLowerCase().contains("path-traversal-secret.jpg")) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
|
||||
.body(FileCopyUtils.copyToByteArray(catPicture));
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
|
||||
.location(new URI("/PathTraversal/random?id=" + catPicture.getName()))
|
||||
.body(Base64.getEncoder().encode(FileCopyUtils.copyToByteArray(catPicture)));
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
log.error("Unable to download picture", e);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
|
||||
.body(StringUtils.arrayToCommaDelimitedString(catPicture.getParentFile().listFiles()).getBytes());
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
.upload-container
|
||||
{
|
||||
width: 500px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.preview
|
||||
{
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview i
|
||||
{
|
||||
color: white;
|
||||
font-size: 35px;
|
||||
transform: translate(50px,130px);
|
||||
}
|
||||
|
||||
.preview-img
|
||||
{
|
||||
border-radius: 100%;
|
||||
box-shadow: 0px 0px 5px 2px rgba(0,0,0,0.7);
|
||||
}
|
||||
|
||||
.browse-button
|
||||
{
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 100%;
|
||||
position: absolute; /* Tweak the position property if the element seems to be unfit */
|
||||
top: 10px;
|
||||
left: 150px;
|
||||
background: linear-gradient(180deg, transparent, black);
|
||||
opacity: 0;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.browse-button:hover
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.browse-input
|
||||
{
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 100%;
|
||||
transform: translate(-1px,-26px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.Error
|
||||
{
|
||||
color: crimson;
|
||||
font-size: 13px;
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<script th:src="@{/lesson_js/path_traversal.js}" language="JavaScript"></script>
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/lesson_css/path_traversal.css}"/>
|
||||
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_intro.adoc"></div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_upload.adoc"></div>
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<div class="upload-container">
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST" name="form"
|
||||
onsubmit='return false'
|
||||
contentType="false"
|
||||
successCallback="profileUploadCallback"
|
||||
failureCallback="profileUploadCallback"
|
||||
informationalCallback="profileUploadCallback"
|
||||
prepareData="profileUpload"
|
||||
enctype="multipart/form-data"
|
||||
action="/WebGoat/PathTraversal/profile-upload">
|
||||
<div class="preview text-center">
|
||||
<img class="preview-img" th:src="@{/images/account.png}" alt="Preview Image" width="200"
|
||||
height="200" id="preview"/>
|
||||
<div class="browse-button">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<input class="browse-input" type="file" required name="uploadedFile" id="uploadedFile"/>
|
||||
</div>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Full Name:</label>
|
||||
<input class="form-control" type="text" id="fullName" name="fullName" required value="test"
|
||||
placeholder="Enter Your Full Name"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<input class="form-control" type="email" id="email" name="email" required
|
||||
placeholder="Enter Your Email" value="test@test.com"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input class="form-control" type="password" id="password" name="password" required
|
||||
placeholder="Enter Password" value="test"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary btn-block" value="Submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_upload_fix.adoc"></div>
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<div class="upload-container">
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST" name="form"
|
||||
onsubmit='return false'
|
||||
contentType="false"
|
||||
successCallback="profileUploadCallbackFix"
|
||||
failureCallback="profileUploadCallbackFix"
|
||||
informationalCallback="profileUploadCallbackFix"
|
||||
prepareData="profileUploadFix"
|
||||
enctype="multipart/form-data"
|
||||
action="/WebGoat/PathTraversal/profile-upload-fix">
|
||||
<div class="preview text-center">
|
||||
<img class="preview-img-fix" th:src="@{/images/account.png}" alt="Preview Image" width="200"
|
||||
height="200" id="previewFix"/>
|
||||
<div class="browse-button">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<input class="browse-input" type="file" required name="uploadedFile" id="uploadedFileFix"/>
|
||||
</div>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Full Name:</label>
|
||||
<input class="form-control" type="text" id="fullNameFix" name="fullName" required value="test"
|
||||
placeholder="Enter Your Full Name"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<input class="form-control" type="email" id="emailFix" name="email" required
|
||||
placeholder="Enter Your Email" value="test@test.com"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input class="form-control" type="password" id="passwordFix" name="password" required
|
||||
placeholder="Enter Password" value="test"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary btn-block" value="Submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_upload_remove_user_input.adoc"></div>
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<div class="upload-container">
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST" name="form"
|
||||
onsubmit='return false'
|
||||
contentType="false"
|
||||
successCallback="profileUploadCallbackRemoveUserInput"
|
||||
failureCallback="profileUploadCallbackRemoveUserInput"
|
||||
informationalCallback="profileUploadCallbackRemoveUserInput"
|
||||
prepareData="profileUploadFix"
|
||||
enctype="multipart/form-data"
|
||||
action="/WebGoat/PathTraversal/profile-upload-remove-user-input">
|
||||
<div class="preview text-center">
|
||||
<img class="preview-img-fix" th:src="@{/images/account.png}" alt="Preview Image" width="200"
|
||||
height="200" id="previewRemoveUserInput"/>
|
||||
<div class="browse-button">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<input class="browse-input" type="file" required name="uploadedFile"
|
||||
id="uploadedFileRemoveUserInput"/>
|
||||
</div>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Full Name:</label>
|
||||
<input class="form-control" type="text" id="fullNameRemoveUserInput" name="fullName" required
|
||||
value="test"
|
||||
placeholder="Enter Your Full Name"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<input class="form-control" type="email" id="emailRemoveUserInput" name="email" required
|
||||
placeholder="Enter Your Email" value="test@test.com"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input class="form-control" type="password" id="passwordRemoveUserInput" name="password" required
|
||||
placeholder="Enter Password" value="test"/>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary btn-block" value="Submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_retrieval.adoc"></div>
|
||||
<div class="attack-container">
|
||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||
<div class="upload-container">
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary btn-block" onclick="newRandomPicture()">Show random cat picture</button>
|
||||
</div>
|
||||
<div>
|
||||
<img id="randomCatPicture" th:src="@{/images/cats/1.jpg}"/>
|
||||
</div>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
<form class="attack-form" method="POST" name="form" action="/WebGoat/PathTraversal/random">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon"><i class="fa fa-flag-checkered" aria-hidden="true"
|
||||
style="font-size:20px"></i></div>
|
||||
<input type="text" class="form-control" id="pathTraversalSecret" name="secret"/>
|
||||
</div>
|
||||
<div class="input-group" style="margin-top: 10px">
|
||||
<button type="submit" class="btn btn-primary">Submit secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="attack-feedback"></div>
|
||||
<div class="attack-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</html>
|
@ -0,0 +1,46 @@
|
||||
#
|
||||
# This file is part of WebGoat, an Open Web Application Security Project utility. For details,
|
||||
# please see http://www.owasp.org/
|
||||
# <p>
|
||||
# Copyright (c) 2002 - 2017 Bruce Mayhew
|
||||
# <p>
|
||||
# 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.
|
||||
# <p>
|
||||
# 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.
|
||||
# <p>
|
||||
# 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.
|
||||
# <p>
|
||||
# Getting Source ==============
|
||||
# <p>
|
||||
# Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository for free software
|
||||
# projects.
|
||||
# <p>
|
||||
#
|
||||
path-traversal-title=Path traversal
|
||||
path-traversal-profile-updated=Profile has been updated, your image is available at: {0}"
|
||||
path-traversal-profile-empty-file=File appears to be empty please upload a non empty file
|
||||
path-traversal-profile-empty-name=Name is empty
|
||||
path-traversal-profile.hint1=Try updating the profile WebGoat will display the location
|
||||
path-traversal-profile.hint2=Look at the displayed location how is the file name on the server constructed?
|
||||
path-traversal-profile.hint3=Does the server validate any input given in the full name field?
|
||||
|
||||
path-traversal-profile-fix.hint1=Take a look what happens compared to the previous assignment
|
||||
path-traversal-profile-fix.hint2=The new and improved version removes `../` from the input, can you bypass this?
|
||||
path-traversal-profile-fix.hint3=Try to construct a full name which after cleaning still has `../` in the full name
|
||||
|
||||
path-traversal-profile-remove-user-input.hint1=Take a look what happened to the file name
|
||||
path-traversal-profile-remove-user-input.hint2=Can we still manipulate the request?
|
||||
path-traversal-profile-remove-user-input.hint3=You can try to use a proxy to intercept the POST request
|
||||
|
||||
|
||||
path-traversal-profile-retrieve.hint1=Can you specify the image to be fetched?
|
||||
path-traversal-profile-retrieve.hint2=Look at the location header...
|
||||
path-traversal-profile-retrieve.hint3=Use /random?id=1 for example to fetch a specific image
|
||||
path-traversal-profile-retrieve.hint4=Use /random/?id=../../1.jpg to navigate to a different directory
|
||||
path-traversal-profile-retrieve.hint5=Do you see a difference when retrieving a file or a directory?
|
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,62 @@
|
||||
webgoat.customjs.profileUpload = function () {
|
||||
|
||||
var picture = document.getElementById("uploadedFile").files[0];
|
||||
var formData = new FormData();
|
||||
formData.append("uploadedFile", picture);
|
||||
formData.append("fullName", $("#fullName").val());
|
||||
formData.append("email", $("#email").val());
|
||||
formData.append("password", $("#password").val());
|
||||
return formData;
|
||||
}
|
||||
|
||||
webgoat.customjs.profileUploadCallback = function () {
|
||||
$.get("PathTraversal/profile-picture", function (result, status) {
|
||||
document.getElementById("preview").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
||||
|
||||
webgoat.customjs.profileUploadFix = function () {
|
||||
var picture = document.getElementById("uploadedFileFix").files[0];
|
||||
var formData = new FormData();
|
||||
formData.append("uploadedFileFix", picture);
|
||||
formData.append("fullNameFix", $("#fullNameFix").val());
|
||||
formData.append("emailFix", $("#emailFix").val());
|
||||
formData.append("passwordFix", $("#passwordFix").val());
|
||||
return formData;
|
||||
}
|
||||
|
||||
webgoat.customjs.profileUploadCallbackFix = function () {
|
||||
$.get("PathTraversal/profile-picture", function (result, status) {
|
||||
document.getElementById("previewFix").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
webgoat.customjs.profileUploadRemoveUserInput = function () {
|
||||
var picture = document.getElementById("uploadedFileRemoveUserInput").files[0];
|
||||
var formData = new FormData();
|
||||
formData.append("uploadedFile", picture);
|
||||
formData.append("fullName", $("#fullNameRemoveUserInput").val());
|
||||
formData.append("email", $("#emailRemoveUserInput").val());
|
||||
formData.append("password", $("#passwordRemoveUserInput").val());
|
||||
return formData;
|
||||
}
|
||||
|
||||
webgoat.customjs.profileUploadCallbackRemoveUserInput = function () {
|
||||
$.get("PathTraversal/profile-picture", function (result, status) {
|
||||
document.getElementById("previewRemoveUserInput").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
webgoat.customjs.profileUploadCallbackRetrieval = function () {
|
||||
$.get("PathTraversal/profile-picture", function (result, status) {
|
||||
document.getElementById("previewRetrieval").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
||||
|
||||
function newRandomPicture() {
|
||||
$.get("PathTraversal/random", function (result, status) {
|
||||
document.getElementById("randomCatPicture").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
=== Path traversal
|
||||
|
||||
A path(directory) traversal is a vulnerability where an attacker is able to access or store files and directories outside
|
||||
the location where the application is running. This may lead to reading files from other directories and in case of a file
|
||||
upload overwriting critical system files.
|
||||
|
||||
=== How does it work?
|
||||
|
||||
For example let's assume we have an application which hosts some files and they can be requested in the following
|
||||
format: `http://example.com/file=report.pdf` now as an attacker you are interested in other files of course so
|
||||
you try `http://example.com/../../../../../etc/passwd`. In this case you try walk up to the root of the filesystem
|
||||
and then go into `/etc/passwd` to gain access to this file. The `../` is called dot-dot-slash which is another name
|
||||
for this attack.
|
||||
|
||||
Of course this is a very simple example and in most cases this will not work as frameworks implemented controls for
|
||||
this, so we need to get a little more creative and start encoding `../` before the request is sent to the server.
|
||||
For example if we URL encode `../` you will get `%2e%2e%2f` and the web server receiving this request will decode
|
||||
it again to `../`.
|
||||
|
||||
Also note that avoiding applications filtering those encodings double encoding might work as well. Double encoding
|
||||
might be necessary in the case where you have a system A which calls system B. System A will only decode once and
|
||||
will call B with the still encoded URL.
|
||||
|
||||
Now let's try some examples
|
||||
|
||||
1. Example with upload where you have to overwrite one of the files
|
||||
1a Example upload where developer implemented a fix by removing ../ from the path
|
||||
1b No longer using name of user to store file so you need proxy to intercept
|
||||
2. Example with download for example the idea is to download a pdf but there is also a txt file
|
||||
3. Same as 2 with blacklisting implemented (see example from Workpress plugin)
|
||||
4. Zip path traversal where you have to create a zip file which overwrites a file in another directory
|
||||
|
||||
|
||||
- Run app with least priviledge
|
||||
- Before storing the file calculate the location
|
||||
- Same for reading is it actually reading from the directory you allow
|
||||
|
@ -0,0 +1,6 @@
|
||||
=== Retrieving other files with a path traversal
|
||||
|
||||
Path traversals are not limited to file uploads also when retrieving files it can be the case that a path traversal
|
||||
is possible to retrieve other files from the system. In this assignment try to find a file called `secret.txt`
|
||||
|
||||
|
@ -0,0 +1,12 @@
|
||||
=== Path traversal while uploading files
|
||||
|
||||
In this assignment the goal is to overwrite a specific file on the file system. Of course WebGoat cares about the users
|
||||
so you need to upload your file to the following location which is outside the normal upload location.
|
||||
|
||||
|===
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/`
|
||||
|
||||
|===
|
@ -0,0 +1,11 @@
|
||||
=== Path traversal while uploading files
|
||||
|
||||
Again the same assignment but can you bypass the implemented fix?
|
||||
|
||||
|===
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/`
|
||||
|
||||
|===
|
@ -0,0 +1,12 @@
|
||||
=== Path traversal while retrieving files
|
||||
|
||||
Finally the upload is no longer vulnerable at least help us to verify :-)
|
||||
In this assignment you need to get the contents of the following file:
|
||||
|
||||
|===
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/secret.txt`
|
||||
|
||||
|===
|
@ -0,0 +1,42 @@
|
||||
=== Path traversal mitigation
|
||||
|
||||
As we saw in the previous assignments protecting a file upload can be a daunting task. The thing comes down to trusting
|
||||
input without validating it.
|
||||
In the examples shown before a solution might be to not trust user input and create a random file name on the
|
||||
server side.
|
||||
|
||||
If you really need to save it based on user input the best way to keep you save is to check the canonical path the
|
||||
file will be saved. For example in Java:
|
||||
|
||||
[source]
|
||||
----
|
||||
var multiPartFile = ...
|
||||
var targetFile = new File("/tmp", multiPartFile.getOriginalName());
|
||||
var canonicalPath = targetFile.getCanonicalPath();
|
||||
|
||||
if (!canonicalPath.startWith("/tmp") {
|
||||
throw new IllegalArgumentException("Invalid filename");
|
||||
}
|
||||
|
||||
IOUtils.copy(multiPartFile.getBytes(), targetFile);
|
||||
----
|
||||
|
||||
The canonical path function will resolve to a absolute path, removing `.` and `..` etc. By checking whether the canonical
|
||||
path is inside the expected directory the path traversal will be avoided.
|
||||
|
||||
|
||||
For path traversals while retrieving one can apply the same technique described above but as a defence in depth you
|
||||
can also implement a mitigation by running the application under a specific not privileged user which is not allowed to read and write
|
||||
in any other directory.
|
||||
|
||||
Make sure that in any case you build detection for catching these cases but be careful with returning explicit information
|
||||
to the user. Every small detail might give the attacker knowledge about your system.
|
||||
|
||||
==== Spring Boot protection
|
||||
|
||||
By default Spring Boot has protection for usage of for example `../` in a path. The implementation can be found in
|
||||
`StrictHttpFirewall` class. This will protect endpoint where the user input is part of the `path` like `/test/1.jpg`
|
||||
if you replace `1.jpg` with `../../secret.txt` it will block the request. With query parameters that protection
|
||||
will not be there.
|
||||
|
||||
In the lesson about "File uploads" more examples of vulnerabilities are shown.
|
@ -0,0 +1,14 @@
|
||||
=== Path traversal while uploading files
|
||||
|
||||
The developer became aware of the vulnerability by not validating the input of the `full name` input field.
|
||||
A fix was made in an attempt to solve this vulnerability.
|
||||
|
||||
Again the same assignment but can you bypass the implemented fix?
|
||||
|
||||
|===
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/`
|
||||
|
||||
|===
|
@ -0,0 +1,58 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.owasp.webgoat.plugins.LessonTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
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 ProfileUploadFixTest extends LessonTest {
|
||||
|
||||
@Autowired
|
||||
private PathTraversal pathTraversal;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Mockito.when(webSession.getCurrentLesson()).thenReturn(pathTraversal);
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
|
||||
Mockito.when(webSession.getUserName()).thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void solve() throws Exception {
|
||||
var profilePicture = new MockMultipartFile("uploadedFileFix", "../picture.jpg", "text/plain", "an image".getBytes());
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.multipart("/PathTraversal/profile-upload-fix")
|
||||
.file(profilePicture)
|
||||
.param("fullNameFix", "..././John Doe"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.assignment", CoreMatchers.equalTo("ProfileUploadFix")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalUpdate() throws Exception {
|
||||
var profilePicture = new MockMultipartFile("uploadedFileFix", "picture.jpg", "text/plain", "an image".getBytes());
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.multipart("/PathTraversal/profile-upload-fix")
|
||||
.file(profilePicture)
|
||||
.param("fullNameFix", "John Doe"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.feedback", CoreMatchers.containsString("/unit-test\\/John Doe\\\"")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(false)));
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.owasp.webgoat.plugins.LessonTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
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 ProfileUploadRemoveUserInputTest extends LessonTest {
|
||||
|
||||
@Autowired
|
||||
private PathTraversal pathTraversal;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Mockito.when(webSession.getCurrentLesson()).thenReturn(pathTraversal);
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
|
||||
Mockito.when(webSession.getUserName()).thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void solve() throws Exception {
|
||||
var profilePicture = new MockMultipartFile("uploadedFileRetrieval", "../picture.jpg", "text/plain", "an image".getBytes());
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.multipart("/PathTraversal/profile-upload-remove-user-input")
|
||||
.file(profilePicture)
|
||||
.param("fullNameFix", "John Doe"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.assignment", CoreMatchers.equalTo("ProfileUploadRemoveUserInput")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalUpdate() throws Exception {
|
||||
var profilePicture = new MockMultipartFile("uploadedFileRetrieval", "picture.jpg", "text/plain", "an image".getBytes());
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.multipart("/PathTraversal/profile-upload-remove-user-input")
|
||||
.file(profilePicture)
|
||||
.param("fullNameFix", "John Doe"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.feedback", CoreMatchers.containsString("/unit-test\\/picture.jpg\\\"")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(false)));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.owasp.webgoat.plugins.LessonTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.token.Sha512DigestUtils;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class ProfileUploadRetrievalTest extends LessonTest {
|
||||
|
||||
@Autowired
|
||||
private PathTraversal pathTraversal;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Mockito.when(webSession.getCurrentLesson()).thenReturn(pathTraversal);
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
|
||||
Mockito.when(webSession.getUserName()).thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void solve() throws Exception {
|
||||
//Look at the response
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/PathTraversal/random"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(header().exists("Location"))
|
||||
.andExpect(header().string("Location", containsString("?id=")))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.IMAGE_JPEG));
|
||||
|
||||
//Browse the directories
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/PathTraversal/random?id=../../"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(content().string(containsString("/path-traversal-secret.jpg")))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.IMAGE_JPEG));
|
||||
|
||||
//Retrieve the secret file (note: .jpg is added by the server)
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/PathTraversal/random?id=../../path-traversal-secret"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(content().string("You found it submit the SHA-512 hash of your username as answer"))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.IMAGE_JPEG));
|
||||
|
||||
//Post flag
|
||||
mockMvc.perform(MockMvcRequestBuilders.post("/PathTraversal/random").param("secret", Sha512DigestUtils.shaHex("unit-test")))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.assignment", equalTo("ProfileUploadRetrieval")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", is(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReceiveRandomPicture() throws Exception {
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/PathTraversal/random"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(header().exists("Location"))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.IMAGE_JPEG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownFileShouldGiveDirectoryContents() throws Exception {
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/PathTraversal/random?id=test"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(content().string(containsString("cats/8.jpg")))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.IMAGE_JPEG));
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.owasp.webgoat.plugins.LessonTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
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 static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class ProfileUploadTest extends LessonTest {
|
||||
|
||||
@Autowired
|
||||
private PathTraversal pathTraversal;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Mockito.when(webSession.getCurrentLesson()).thenReturn(pathTraversal);
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
|
||||
Mockito.when(webSession.getUserName()).thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void solve() throws Exception {
|
||||
var profilePicture = new MockMultipartFile("uploadedFile", "../picture.jpg", "text/plain", "an image".getBytes());
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.multipart("/PathTraversal/profile-upload")
|
||||
.file(profilePicture)
|
||||
.param("fullName", "../John Doe"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.assignment", CoreMatchers.equalTo("ProfileUpload")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalUpdate() throws Exception {
|
||||
var profilePicture = new MockMultipartFile("uploadedFile", "picture.jpg", "text/plain", "an image".getBytes());
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.multipart("/PathTraversal/profile-upload")
|
||||
.file(profilePicture)
|
||||
.param("fullName", "John Doe"))
|
||||
.andExpect(status().is(200))
|
||||
.andExpect(jsonPath("$.feedback", CoreMatchers.containsString("/PathTraversal\\/unit-test\\/John Doe\\\"")))
|
||||
.andExpect(jsonPath("$.lessonCompleted", CoreMatchers.is(false)));
|
||||
}
|
||||
|
||||
}
|