Add path traversal lesson

This commit is contained in:
Nanne Baars
2020-03-03 21:37:24 +01:00
committed by Nanne Baars
parent c4c28f544f
commit 6c25cf8e43
72 changed files with 1286 additions and 146 deletions

View File

@ -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";
}
}

View File

@ -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();
}
}

View File

@ -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)));
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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?

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -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;
});
}

View File

@ -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

View File

@ -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`

View File

@ -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/`
|===

View File

@ -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/`
|===

View File

@ -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`
|===

View File

@ -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.

View File

@ -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/`
|===

View File

@ -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)));
}
}

View File

@ -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)));
}
}

View File

@ -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));
}
}

View File

@ -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)));
}
}