A high severity vulnerability (CVE-2021-3177, CVSS V3 base score – 9.8 CRITICAL)  impacting all versions of Python 3 was reported privately on Jan 16, 2021 and published on Feb 19, 2021. The vulnerability has also been confirmed to affect versions of Python 2, which is currently end-of-life but still widely used. The Randori Attack Team has developed a proof of concept and been able to demonstrate code execution under extremely contrived circumstances. The vulnerability is present in Python’s built-in ctypes module, which is rarely used by enterprise applications. It is Randori’s assessment that this vulnerability poses a low real-world risk and is unlikely to affect a large number of enterprise applications.
In analyzing this vulnerability, Randori has determined the following:
- Python applications not specifically using ctypes.c_double are not affected by this bug.
- For applications that are affected, this bug is most likely to impact application availability (denial of service, application crash), rather than remote code execution.
- Even for applications that specifically call ctypes.c_double.from_value() with an untrusted argument, achieving code execution in the wild is extremely unlikely due to default mitigations (runtime address randomization and compile-time buffer overflow protections) present in most installations.
This vulnerability is present in Python’s built-in ctypes module and as a result, is highly unlikely to result in remote code execution. Most Python-based applications are not affected by this vulnerability because they do not rely on calls to ctypes.c_double.from_value(). For applications that are affected by it, impact is likely to be limited to application availability.
The cytpes module is often used for interfacing with system libraries and allows Python to interoperate with low-level data types, including IEEE 64-bit floating point types (c_double). However, many Python libraries interface with system libraries without using ctypes. To triage this bug, users should determine if their applications rely on ctypes, and, specifically, handle untrusted input with the c_double datatype.
At this time, Randori is not aware of any applications based on Python 3 where remote attackers can reach this vulnerability.
If you believe you may be impacted, upgrading to the patched versions of Python (3.6.13, 3.7.10, 3.8.8, and 3.9.2) will eliminate this vulnerability. Randori recommends any organization that believes they may be impacted to update to a patched version out of an abundance of caution, but that urgent action is not required.
If you are unable to patch, but you are able to trigger this vulnerability in your application, you can limit the reachability by eliminating the evaluation of very large values (overflowing the buffer appears to require a floating point value of at least 1e+234). If you find this vulnerability is remotely reachable in any application, reach out to us.
Organizations running Randori Recon can check their current exposure by viewing the Services page to see if any Python 2 or Python 3 targets have been detected on their perimeter.
The vulnerable line of code is in Modules/_ctypes/callproc.c:
sprintf(buffer, "<cparam '%c' (%f)>", self->tag, self->value.f);
The variable “buffer” is declared as a stack variable, char buffer, however, the format string, combined with the given variables, can exceed 300 bytes.
Python built on modern distributions will most likely have included the “–fortify-source” compile flag, which causes the compiler to call sprintf_chk() in place of sprintf() automatically. This wraps the call and detects the buffer overflow, which ultimately results in a call to abort(), terminating the process with a message that a buffer overflow was detected, prior to remote code execution being achieved.
In systems without this mitigation in place, another mitigation called a stack canary, which works in a similar fashion, would detect the overflow prior to code execution being achieved, and abort the process.
Some techniques exist to bypass this mitigation under certain circumstances, but an attacker’s bypass of either mitigation depends on many factors, such as the ability to make subsequent attempts in reasonable time periods, or whether an application is threaded or forking (the latter of which might prevent randomization of the canary between bypass attempts).
Some systems might have both of these mitigations disabled. For application binary interfaces (ABIs) that store return pointers on the stack, such as Linux on Intel architectures, it is expected that, in memory, after the buffer variable, is a return pointer, which is a memory address of the next code for the machine to execute. In most systems today, these addresses are randomized for each run of the application, but again, under some limited circumstances, an attacker may be able to bypass these mitigations if they could write an arbitrary value into this memory.
The return pointer, that is the value for the code the machine will execute, is not, however, code itself: it’s the address of some other existing code. In exploit engineering, we call this the address of a gadget, which is a useful snippet of code that will allow us to continue execution and usually performs a small useful function.
Further still, since this particular overflow can only write the values 0-9, the addresses writable to this location all look like 0x30303030 (where each 30 could be 30-39 hex in 32-bit, or a number twice that long for 64-bit processes). This is an extremely limited set of addresses compared to the size of the address space in either 32 or 64-bit cases.
If a system had Python 3, built without fortify-source or stack canaries (or was running in a way they could be bypassed), and address randomization was disabled (or again, could be somehow bypassed), and the code could somehow be predictably aligned just so that a useful gadget would appear at one of the very limited set of reachable addresses, then, in theory, this bug could result in remote code execution. In all other cases, the application will crash.
Randori constructed just such a contrived scenario as a proof of concept for the exploitability of this bug. All mentioned mitigations were disabled, and memory of the attacker’s choosing was placed at a location of the attacker’s choosing (one of the limited addresses reachable) and executed by use of this vulnerability.
from ctypes import (c_double, c_int, CDLL, memmove, create_string_buffer,
# contrived setup, map executable memory with shellcode exactly where we want
# to jump (an attacker would have to set this up somehow)
libc = CDLL(None)
syscall = libc.syscall
NR_mmap = 192
target_address = 0x34333231
# mmap, 1 page, rwx, anonymous|private, no file, no offset
syscall(NR_mmap, target_address, 0x1000, 7, 0x21, -1, 0)
shellcode = create_string_buffer(
b'h\x01\x01\x01\x01\x814$/\x0b\x01\x01hherehwas hori hRand\x89\xe1j\x01[j'
memmove(target_address, addressof(shellcode), 45)
# trigger the bug
# this will jump to address 0x34333231 (ascii '4321') where the attacker's shell code
# is waiting, and will print out "Randori was here."
# if nothing happened, this should print, however, triggering the bug
# will print an alternate message!
print("all done! no problem.")
When we run this with a special mitigations-disabled x86 build of Python 3.9.1, we get the following output:
$ ./python poc.py
Randori was here.
The following configure options were given to disable the default mitigations (including intentionally leaving out the fortify-source option usually included in distributions):
./configure OPT="-O0" CFLAGS="-m32 -fno-stack-protector -fno-pie -fno-pic" LDFLAGS="-m32"
Python runs on many architectures with many different ABIs, so these generalizations might not apply everywhere. But the bottom line is that it would be hard to imagine there are many circumstances, if any, in the real world, where this overflow will result in code execution. However, it is very reasonable that a limited number of applications could exist where the vulnerability, if triggered, would impact application availability.