二进制安全|格式化字符串漏洞研究(C/C++、Python)

0x00 前言 From WIKIPEDIA:
Uncontrolled format string is a type of software vulnerability discovered around 1989 that can be used in security exploits. Originally thought harmless, format string exploits can be used to crash a program or to execute harmful code. The problem stems from the use of unchecked user input as the format string parameter in certain C functions that perform formatting, such as printf(). A malicious user may use the %s and %x format tokens, among others, to print data from the call stack or possibly other locations in memory. One may also write arbitrary data to arbitrary locations using the %n format token, which commands printf() and similar functions to write the number of bytes formatted to an address stored on the stack.
典型的漏洞利用结合了这些技术来控制进程的指令指针(IP)。然而格式化字符串的问题不仅仅出现于C中,虽然利用的原理和方式不同。
总之,当程序员通过格式化字符串函数输出包含用户提供的数据的字符串时,格式字符串错误将需要得到关注 。
本文将从格式化字符串起源的C/C++说起,最后讲解Python 中的格式化字符串问题。
0x01 C/C++格式化字符串漏洞 一、格式化字符串用法 C/C++中格式化字符串原型如下(本文以printf函数进行解析)

int printf ( const char * format, ... );

一般用法如下案例所示:
printf ("The magic number is: %d", 1911);

第一个参数为格式化字符串,将根据该参数声明的格式进行输出,格式化字符串中占位符一般以%开头,用法为:
%[flags][width][.precision][length]specifier

各部分详情可见:
1 specifier
specifier参数是最重要的主体,其定义了对应参数的类型或说明,例如上述案例表示该位置对应一个整数类型,参数为后续的第一个参数变量。其他的specifier还有:
specifier
Output
Example
d or i
Signed decimal integer
392
u
Unsigned decimal integer
7235
o
Unsigned octal
610
x
Unsigned hexadecimal integer
7fa
X
Unsigned hexadecimal integer (uppercase)
7FA
f
Decimal floating point, lowercase
392.65
F
Decimal floating point, uppercase
392.65
e
Scientific notation (mantissa/exponent), lowercase
3.9265e+2
E
Scientific notation (mantissa/exponent), uppercase
3.9265E+2
g
Use the shortest representation: %e or %f
392.65
G
Use the shortest representation: %E or %F
392.65
a
Hexadecimal floating point, lowercase
-0xc.90fep-2
A
Hexadecimal floating point, uppercase
-0XC.90FEP-2
c
Character
a
s
String of characters
sample
p
Pointer address
b8000000
n
Nothing printed.The corresponding argument must be a pointer to a signed int.The number of characters written so far is stored in the pointed location.
%
A % followed by another % character will write a single % to the stream.
%
特別地:
%n:功能上表已经说明,即将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,这是后面进行任意地址写的根本原因
%s:显示给定地址处的字符串。所以使用该specifier时,需要提供一个指针类型的参数,指针指向地址处的字符串就会被格式化到字符串中
2 flags
flags格式化字符有如下几个:
flags
description
-
Left-justify within the given field width; Right justification is the default (see width sub-specifier).
+
Forces to preceed the result with a plus or minus sign (+ or -) even for positive numbers. By default, only negative numbers are preceded with a - sign.
(space)
If no sign is going to be written, a blank space is inserted before the value.
#
Used with o, x or X specifiers the value is preceeded with 0, 0x or 0X respectively for values different than zero.
Used with a, A, e, E, f, F, g or G it forces the written output to contain a decimal point even if no more digits follow. By default, if no digits follow, no decimal point is written.
0
Left-pads the number with zeroes (0) instead of spaces when padding is specified (see width sub-specifier).
特别地,在linux下%{数字}$ specifier,表示以specifier指示的类型格式化指定位置的变量。该flags为后面实现任意地址读提供了方便。
例如:
#include void func() { printf("Hello World!"); }int main(void) { int first = 11111; int second = 22222; func(); printf ("First:%2$d; Second:%1$d; \n", first,second); return 0; }

二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

格式化字符串中参数的顺序被调换了
3 width
表示要打印数据的宽度:
width
description
(number)
Minimum number of characters to be printed. If the value to be printed is shorter than this number, the result is padded with blank spaces. The value is not truncated even if the result is larger.
*
The width is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted.
例如:
#include int main(void) { int data = https://www.it610.com/article/256; printf ("%20d", data); return 0; }

输出如下所示,因为指定了要输出数据的宽度为20,但是256宽度为3,所以%20d,宽度指示需要增加17个空格,即下述绿色部分。
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

4 precision
用于设置显示数据的精度,对于整数作用于数量级,对于浮点数用于小数点的精度
.precision
description
.number
For integer specifiers (d, i, o, u, x, X): precision specifies the minimum number of digits to be written. If the value to be written is shorter than this number, the result is padded with leading zeros. The value is not truncated even if the result is longer. A precision of 0 means that no character is written for the value 0.
For a, A, e, E, f and F specifiers: this is the number of digits to be printed after the decimal point (by default, this is 6).
For g and G specifiers: This is the maximum number of significant digits to be printed.
For s: this is the maximum number of characters to be printed. By default all characters are printed until the ending null character is encountered.
If the period is specified without an explicit value for precision, 0 is assumed.
.*
The precision is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted.
简单的例子:
#include int main(void) { double d = 123.45678; printf("%20.10f\n", d); }

输出如下:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

因为精度设置了10,所以小数点后全部包含并通过0补齐10个精度要求
5 Length
格式化字符串中Length表示解析对应变量的长度。限定符有如下,给出了对应的Length作用于各种数据类型时如何解释。例如hh作用于%d时,仅解析%d对应数据的1个字节。所有Length如下所示:
specifiers
length
d i
u o x X
f F e E g G a A
c
s
p
n
(none)
int
unsigned int
double
int
char*
void*
int*
hh
signed char
unsigned char
signed char*
h
short int
unsigned short int
short int*
l
long int
unsigned long int
wint_t
wchar_t*
long int*
ll
long long int
unsigned long long int
long long int*
j
intmax_t
uintmax_t
intmax_t*
z
size_t
size_t
size_t*
t
ptrdiff_t
ptrdiff_t
ptrdiff_t*
L
long double
例如:
#include int main(void) { int data = https://www.it610.com/article/256; printf ("%hhd\n", data); return 0; }

变量data的二进制为:0000 0001 0000 0000
格式化字符串中使用的Length为hh,所以表示仅解析%d对应变量的一个字节,所以data最终显示为0。
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

这为任意地址写的漏洞提供了更加精确写的目的。

二、C/C++格式化字符串漏洞 C/C++中的格式化字符串漏洞会造成如下两方面问题:
  1. 任意地址内存读取数据,造成内存信息泄露
  2. 修改任意地址内存的数据,如修改栈RET值从而造成恶意代码执行、修改栈条件变量值从而变更程序执行流程等等
下面以printf函数作为讲解原理的对象,其他格式化字符串函数原理类似
1 环境说明
1、操作OS类型:linux 64bit
2、为便于分析和讲解关闭ASLR地址随机化
3、编译器:g++,编译二进制时不加入其它编译选项。g++ -g xxx.cpp –o xxx使用该方式编译即可
2 printf函数执行方式
以下面一个例子进行研究:
#include int main(void){ getchar(); int data1 = 0x0100; char str2[12] = "hello world"; int data3 = 0xFFF3; int data4 = 0xFFF4; int data5 = 0xFFF5; int data6 = 0xFFF6; int data7 = 0xFFF7; int data8 = 0xFFF8; printf("output %hhd,%s,%d,%d,%d,%d,%d,%d\n",data1,str2,data3,data4,data5,data6,data7,data8); return 0; }

通过g++ -g formatPrinciple.cpp –o formatPrinciple编译上述
在调用printf处设置断点,此时程序状态如下所示:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

上图通过在windows下远程调在64位linux环境下的上述程序,可以看出该环境下函数调用时参数的传递方式:
a、参数1-6分别使用寄存器保存:rdi,rsi,rdx,rcx,r8,r9。(本文不分析float情形,不影响原理分析)。特别的rdx保存的是str2字符串的地址,rdx值指向真实存储字符串的地址
b、超过6个以外的参数,从最后的参数压入程序栈指导第七个参数,和__cdecl 的入栈方式相同。(注:栈向低地址方向生长,因为data8先入栈,所以data8的地址在高处)
上述程序最终输出为:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

具体的对应图为:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

可以理解printf根据format字符串中的specifier占位符,依次获取从第二个开始参数的值。format字符串中前6个要显示的参数直接从对应的寄存器中取,后续的参数依次从栈中获取,每个参数相隔8个字节
上述一切似乎很正常,因为如果有确定的format字符串,一般开发时后续的可变参数列表就会提供一致数量的变量。但是如果format字符串变得可控,例如format字符串中的specifier个数超过了程序可变参数列表变量数量,将会怎样?
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

3 读取内存-信息泄露
  • 原理
我们考虑如下情形:
#include int main(void) { getchar(); int data1 = 0x0100; char str2[12] = "hello world"; int data3 = 0xFFF3; int data4 = 0xFFF4; int data5 = 0xFFF5; int data6 = 0xFFF6; printf("output %hhd,%s,%d,%d,%d,%d,%x,%x,%x\n",data1,str2,data3,data4,data5,data6); return 0; }

上述format中需要9个变量来格式化字符串,而实际可变参数列表仅提供了6个参数,其中:
data1,str2,data3,data4,data5的值分别存储在寄存器rsi,rdx,rcx,r8,r9中
data6存储在栈中。
最后三个%x无变量提供数据,我们运行该程序输出如下所示:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

发现程序运行正常,并且正常输出了值。那么输出的值来自何处?我们对该程序进行调试查看。
汇编运行到调用printf处:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

查看此时的程序栈信息:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

从栈信息可以看出:
  1. 栈顶位置保存的就是第七个参数,printf打印时该出的值被使用
  2. 同时发现上述多出三个格式化specifier对应的值就是栈顶第七个参数保存位置后面连续出现的数据。因为64bits环境中以8字节为单位,而%x显示的是int,所以程序的结果
输出的分别就是0和上述绿色框
结论:
从上述可以看出若格式化函数中多出需要格式化的specifier时,会直接解析printf栈中保存参数的后续内容,如上所述。
所以可以知道如果用户可控format参数,拼接的format中恶意增加超过预期数量的specifier时,则内存数据将被泄露。
  • 简单利用-读取敏感信息
所以如果程序想要使用格式化字符串函数例如printf函数输出用户的输入,如果类似下述代码就会产生上节的问题:
#include int main(void) { char str[50]; scanf("%s\n",str); printf(str); return 0; }

例如用户输入如下以及显示:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

如上所示,格式化字符串要显示第六个参数,此时直接解析到了用户输入的字符串的栈区地址,如果直接往下进行输出,若栈中有敏感数据则敏感数据被获取,同时也能得到程序的函数返回地址,从而进一步可以绕过ASLR的措施。
既然可以通过可控的格式化字符串format参数进行内存数据泄露,下述给出查看任意地址的内存数据的format构造方法。
例如像如下代码所示,main函数中模拟通过recv获取网络数据,程序意图将获取的数据直接使用printf打印,其中存在敏感数据secret。代码如下所示:
#include #define SECRETLENTH 10 #define MSGLENTH 50char secret[SECRETLENTH] = "password"; void recv(char *format,unsigned int lenth ) { FILE *fp = NULL; fp = fopen("formatArbiReadMsg.bin","rb"); int pos = 0; while(!feof(fp)) { format[pos] = getc(fp); pos++; if(pos >= lenth) { break; } } }int main(void) { getchar(); char str[MSGLENTH] = {0}; recv(str,MSGLENTH); printf(str); printf("\n"); secret[0] = 'w'; return 0; }

上述代码printf直接打印format格式化字符串,format参数可控,可以泄露内存数据。利用下述的payload可以泄露上述代码中的敏感数据secret
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


上述红色框为specifier,指定取第七个参数解析字符串:第七个参数为地址并显示该地址处的字符串。而根据栈数据布局分析,第七个数据未上述绿色框部分(64bit os下内存单位为8 bytes),所以在此处填入该进程可访问的任意地址均可以访问(因为%s可能因为0截断)。上述案例中输入了secret的地址,运行后可以获取secret密码:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片




4 写内存
  • 原理
能实现地址写的问题要使用到上述1.1.1 specifier章节所示的“%n” :将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置。看如下例子:
#include int main(void) { int c = 0; printf("Hello World%n", &c); printf("%d\n", c); return 0; }

%n前面有11个字符,最终c被写入了11这个数字,所以运行结果如下所示:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


如果想要指定c的值例如100,当然可以在%n前面注入100个字符,输入将是复杂的:
“aaaaaaaaaaaa…….aaaaaaaaaa%n”(其中有100个a)

但是有一种更好的方式,可以使用1.1中讲述的[width]标识:自定义打印字符串的宽度,不足该宽度使用空格替换。例如下述程序和输出:
#include int main(void) { int data = https://www.it610.com/article/12; printf("%101d%n", data,&data); printf("\nthe value of data: %d\n", data); return 0; }

输出为:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


这样,一方面输入较方便;另一方便也减少了输入的字符串量,如果程序提供保存format字符串的空间有限的情况,该方法就突显优势。
也可以如下仅精确在某个地址写入一个字节数据:
#include int main(void) { char c = 'A'; int n = 0; printf("%257c%hhn\n",c,&n); printf("%d\n", n); return 0; }

输出:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


输出1是因为仅写入一个自己的数据到变量n中,由于%n前输出257个字节(二进制1 0000 0001),则低位字节就是1。
根据前文1.2.2所述的格式化字符串的解析原理可知,如果在1.2.3节中如数带有含%n的format则可以达到向某地址写入的功能。 具体的:
1、%n指向的栈区中保存的数据会被解析为一个地址A
2、计算printf的%n前面输出的字符数量C
3、将数据C写入A地址指向的内存、
所以重点就是如何构造format中的%n指向栈的第几块以及给出想要写入的值。可以看下节利用过程帮助理解。
  • 简单利用
依然使用1.2.3节的代码,稍作删改:
#include #include #define MSGLENTH 100int recv(char *format,unsigned int lenth ) { FILE *fp = NULL; fp = fopen("formatArbiWriteMsg.bin","rb"); int pos = 0; while(!feof(fp)) { format[pos] = getc(fp); pos++; if(pos >= lenth) { break; } } return pos; }void printMsg() { char msg[MSGLENTH] = {0}; char end[]= "%c"; int recvCount = recv(msg,MSGLENTH); memcpy(&msg[recvCount-1],end,strlen(end)); char c = '!'; printf(msg,c); // #1 return; // #2 }int main(void) { getchar(); printMsg(); printf("done!\n"); // #3 return 0; // #4 }

对该程序使用GDB调试,模拟程序运行并方便我们查看汇编和内存数据。
formatArbiWriteMsg.bin内容如下所示:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


先不用理解其中数据的含义,后续将揭开神秘面纱。
运行到#1处代码:
查看当前rbp和rsp,并显示当前栈帧内容:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片



可以看出栈顶开始就是formatArbiWriteMsg.bin内容,所以字符数组msg的起始位置就是栈顶开始位置。0x00007fffffffe050为rbp位置,所以当前函数返回地址被保存在0x00007fffffffe058,即红色框部分。
运行到#2处代码,查看此时的栈帧内容:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


发现返回地址低字节被修改b6-》C0。0x4008c0其实就是main函数的return 0语句。
所以看到此时栈的变化,就可以知道函数返回时将绕过#3处的语句。而实际运行结果也是。此处对特定地址内存进行了修改改变了程序的执行流程。
下面解释下上述的payload:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


红色框代表显示192宽度的字符
绿色框指定找到栈中第8个参数,并把前面输出的宽度值即192(Hex:C0)写入到该参数表示的地址位置,且仅写一个字节(hh)
黄色框就是第八个参数的位置,该位置填入了rip存储的位置
绿色线是为了让黄色框内容位于第八个参数而增加的填充位。
所以程序运行时该数据就会让rip的值修改到return 0语句地址,从而改变执行流程。

5 安全编码建议
1、调用格式化函数时,禁止format参数由外部可控。调用格式化函数时,如果format参数由外部可控,会造成字符串格式化漏洞。这些格式化函数有:
  1. 格式化输出函数:xxxprintf
  2. 格式化输入函数:xxxscanf
  3. 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line();
  4. 格式化日志函数:syslog(),vsyslog()。
2、若必须format参数用户可控,必须对用户输入的数据进行校验,过滤或者编码其中的所有“%”,禁止用户输入使用specifier格式化功能。注:可能开发人员会存在删除其中的%d类似这种,但是恶意用户可通过%%dd绕过。建议所有出现的%编码或者转义成其它字符
3、若用户可使用specifier功能,则必须确保format参数中specifier的个数和可变参数列表的变量个数保持一致,并且检查类型符合预期。

6 安全测试建议
该问题的发现通过源码审计更佳:
  1. 找到所有格式化字符串函数使用的地方:pintf、sprintf、snprintf、scanf等格式化字符串的函数
  2. 查看format 参数是否外部可控
  3. 若可控可按上述原理进行合适的利用
0x02 Python格式化字符串漏洞 一、实验环境 Python :2.7.18
OS:windows10 64bits
Django:1.10

二、格式化字符串用法 Python 2.6之前格式化format字符串的写法和C中的类型,都是使用%进行表示specifier,例如:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

%10.2s——10位占位符,截取两位字符串。
Python 2.6后使用{}取代了%,但依然仍然也可以使用,效果是一致的。详细的python格式化字符串用法见:https://docs.python.org/release/2.6/library/string.html#formatstrings
format_spec ::=[[fill]align][sign][#][0][width][.precision][type]

下面通过简单的例子帮助读者理解用法,例子均使用了新用法:
#!/usr/bin/python # -*- coding: UTF-8 -*-print "{} {}".format("hello", "world") print "{0} {1}".format("hello", "world") print "{1} {0} {1}".format("hello", "world")print "姓:{firstname}, 名: {secondname}".format(firstname="张", secondname="三")# 通过字典设置参数 site = {"firstname": "张", "secondname": "三"} print("姓:{firstname}, 名: {secondname}".format(**site))# 通过列表索引设置参数 my_list = ['张三', '李四'] print("{0[0]}, {0[1]}".format(my_list))# 也可以传入对象 class AssignValue(object): def __init__(self, value): self.value = https://www.it610.com/article/valuemy_value = AssignValue(6) print('value 为: {AV.value}'.format(AV=my_value))

输出:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

上述能够在format模板串中访问对象的属性将是后文介绍Python格式化字符串漏洞的起因

三、格式化字符串漏洞 1 类C/C++格式化字符串漏洞研究
C/C++格式化字符串中漏洞包含:
  • 内存数据泄露:通过超过可变参数列表范围读取数据。
在python中试探发现如下结果:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

从现象可以发现,python严格检查了format中的占位specifier数量必须不能超过可以索引的范围,实际这也是前文对C/C++安全编码的建议。Python提供的该格式化函数已经为用户做了此层防御,无类似C/C++通过过多的或超出可变参数范围的specifier造成内存信息泄露的现象。
  • 内存写数据:通过%n向指定内存写数据
通过阅读Python 的官方开发手册,没有发现任何类似C/C++同通过%n的specifier 可以对指定地址写入的操作,仅仅作为输出到格式化字符串的功能。所以对于内存写的风险是没有的。


2 Python格式化字符串问题
根据上节分析,Python格式化字符串的过程中是没有类似C/C++相似原理的漏洞。然而由于Python2.6以后的版本给出了功能更强大的format方法,而该方法允许访问内嵌的数据结构,format可用访问Python对象的属性,这给Python在格式化字符串中产生了另一种信息泄露的风险。
  • 2.1 未最小化开放的范围导致的信息泄露
示例代码如下所示:
def formatInfoDisclosureView(request): request.user.username = 'admin' request.user.password = 'mima' template = 'Hello {user.username}, This is your email: ' + request.GET.get('email')return HttpResponse(template.format(user=request.user))

代码开始设置了当前请求用户的账户名和密码,模拟当前登录的用户。然后使用格式化字符串显示用户的信息,正常情况如下
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

从代码看出email字段被直接拼接如format模板格式字符串,由于format参数中带入了整体的request.user对象,所以url构造如下:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


获取到敏感信息的password信息,而本应该信息设计意图不能让用户知道。
  1. 开发者错误的放大了对象的范围
  2. 并且使得用户可控format字符串
所以防御方式显而易见:
  1. 用户不可控format字符串
  2. Format参数仅提供需要的范围,精细化参数。例如template.format(user=request.user)变为template.format(user=request.user.username)
  • 2.2 通过对象内建属性造成信息泄露
Python中的对象(python中函数也是对象)一般都包含着一些隐藏属性和方法,并能够通过这些属性和方法访问到敏感信息。
  • 2.2.1前提知识储备
内建对象和方法
Python中每个函数和类对象都包含内建的魔法属性和方法,通过dir函数查看函数和类的内建属性:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


类和其实例内建属性和方法是一样的。
整理其中的内建属性:
a、类的内建属性有
'__class__', '__dict__', '__doc__', '__module__', '__weakref__'

这边简单介绍几个:
__class__:返回当前对象实例的类:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


b、函数的内建属性有
'__annotations__', '__class__', '__closure__', '__code__', '__defaults__', '__dict__','__doc__', '__kwdefaults__', '__module__', '__name__','__qualname__',__globals__

这边简单介绍几个:
__globals__:函数对象的属性。返回一个由当前函数可以访问到的变量,方法,模块组成的字典,不包含该函数内声明的局部变量。例如:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


最明显的因为globalVar为全局变量,函数fun能够访问,所以globalVar变量位于fun函数的全局dict中
内建属性和方法含义讲解详情见: https://www.jianshu.com/p/cfda0b76501c。或者咨询到python官方文档查看功能作用。
Python函数、类理解
在 Python 中万物皆为对象,所以类中的成员函数也是对象,所以你可以看到使用{class}.__init__获取构造函数这个对象。
class EventBase(object): def __init__(self, id): self.id = idclass Event(EventBase): def __init__(self, id, level, message): self.level = level self.message = message super(Event,self).__init__(id)print dir(Event) print Event.__init__.__class__

二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

  • 2.2.2、简单的案例如下所示:
Password = 'i am mima'class Event(object): def __init__(self, id, level, message): self.id = id self.level = level self.message = messagedef format_event(format_string, event1): return str(format_string).format(event=event1)event = Event(1, 1, "2012 lab") _user_input = raw_input('Please input:') #{event.__init__.__globals__[Password]} privacy_data = https://www.it610.com/article/format_event(_user_input, event) print(privacy_data)

正常输入可以显示Event各对象属性值,但是用户构造如下Payload将泄露出Password全局变量的值:
#{event.__init__.__globals__[Password]}


二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

因为__init__构造含有内建属性__globals__,而该属性表示函数能够访问到的变量和方法,所以也就能访问到全局变量。
防御的思想也很简单通过某种黑白名单限制用户可以访问对象的属性,下面的方案重新封装了一个格式化函数:
from string import Formatter from collections import Mappingclass MagicFormatMapping(Mapping): # This class implements a dummy wrapper to fix a bug in the Python # standard library for string formatting. def __init__(self, args, kwargs): self._args = args self._kwargs = kwargs self._last_index = 0def __getitem__(self, key): if key == '': idx = self._last_index self._last_index += 1 try: return self._args[idx] except LookupError: pass key = str(idx) return self._kwargs[key]def __iter__(self): return iter(self._kwargs)def __len__(self): return len(self._kwargs)# This is a necessary API but it's undocumented and moved around # between Python releases try: from _string import formatter_field_name_split except ImportError: formatter_field_name_split = lambda x: x._formatter_field_name_split()class SafeFormatter(Formatter): def get_field(self, field_name, args, kwargs): first, rest = formatter_field_name_split(field_name) obj = self.get_value(first, args, kwargs) for is_attr, i in rest: if is_attr: obj = safe_getattr(obj, i)# #1 else: obj = obj[i] return obj, firstdef safe_getattr(obj, attr): # Expand the logic here. For instance on 2.x you will also need # to disallow func_globals, on 3.x you will also need to hide # things like cr_frame and others. So ideally have a list of # objects that are entirely unsafe to access. if attr[:1] == '_':# #2 raise AttributeError(attr) return getattr(obj, attr)def safe_format(_string, *args, **kwargs): formatter = SafeFormatter() kwargs = MagicFormatMapping(args, kwargs) return formatter.vformat(_string, args, kwargs)Password = 'i am mima'class Event(object): def __init__(self, id, level, message): self.id = id self.level = level self.message = messageevent = Event(1, 1, "2012 lab") print(safe_format('{0.message}', event)) print(safe_format('{0.__init__.__globals__[Password]}', event))

输出结果:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片


上述代码继承了Formatter,并且重写了get_field函数,刚方法仅有一处变动:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

而在变更的该函数中,正如上述代码#2位置处,程序判断了如果属性开头包含”_”则抛出异常,这样就防止了内建属性的调用。所以在此处也可以增加其他类型的不可访问字段。


三、案例展示
1、 利用格式化字符串漏洞泄露Django配置信息
使用前文的代码:
def formatInfoDisclosureView(request): request.user.username = 'admin' request.user.password = 'mima' template = 'Hello {user.username}, This is your email: ' + request.GET.get('email')return HttpResponse(template.format(user=request.user))

前文通过可控的格式化字符串获取了request.user其它的敏感属性,但是一般情况开发人员不会把敏感对象直接使用,所以也就不能从本身对象包含的属性进行敏感信息泄露。
考虑到上节可通过内置对象获取全局的一些信息,可通过下述payload获取Django的
http://10.164.146.143:8000/info/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

获取到Django配置settings文件里面的密钥:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

2、Jinja 2.8.1 模板沙盒绕过
Jinja2是一个在Python web框架中使用广泛的模板引擎,可以直接被被Flask/Django等框架引用。Jinja2在防御SSTI(模板注入漏洞)时引入了沙盒机制,也就是说即使模板引擎被用户所控制,其也无法绕过沙盒执行代码或者获取敏感信息。
Jinja2沙箱对于模板中访问不安全的属性和方法是被禁止的。
但由于format带来的字符串格式化漏洞,导致在Jinja2.8.1以前的沙盒可以被绕过,进而读取到配置文件等敏感信息
安装jinja有问题的版本2.8:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

编写如下代码:
from jinja2.sandbox import SandboxedEnvironmentclass Account(object): def __init__(self, name): self.name = nameenv = SandboxedEnvironment() passwd = 'mima' t = env.from_string('{{ "{0.__class__.__init__.__globals__}".format(user) }}') ret = t.render(user=Account('joe')) print ret

其中:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片

沙箱的该方法不允许访问不安全的属性和方法。
但是上述代码通过格式化字符串绕过了沙箱的机制,访问到了全局属性和方法,包括其中敏感信息:
二进制安全|格式化字符串漏洞研究(C/C++、Python)
文章图片



四、安全测试建议
  1. 全文搜索format函数,并确认是实现格式化字符串的功能
  2. 查看format字符串是否可被外部可控或部分可控
  3. 若外部可控,查看是否存在校验和过滤



五、 安全编码建议
【二进制安全|格式化字符串漏洞研究(C/C++、Python)】上述在讲解漏洞时已经给出了详细的解决方案,此处不再赘述

    推荐阅读