转自: http://blog.neargle.com/2016/07/22/pythonweb-framework-dev-vulnerable/
SQL注入所产生的条件是用户输入可构造sql语句并带入数据库执行。在Web应用中,容易产生SQL注入的输入一般是GET或POST请求参数。在PythonWeb开发中,以Flask框架为例,Flask里获取GET或POST请求数据的方式分别是request.args.get('id', 0, type=int)
和request.form.get('id', 0, type=int)
两种方式,另外Flask还支持在URL路由里带入变量:@app.route('/news/
,当程序员定义了这样的URL,则id这个变量在该视图里就是可以调用的。两种方法获取都是可以限定参数的类型,前者如果程序指定type为int,当用户传入无法转换成整形的字符串时,就返回None(若指定了默认值则为默认值,例子的默认值为0),后者出现这种情况则直接返回404.
PythonWeb开发中,在处理数据库的过程中经常使用orm库进行数据库处理,orm库是防SQL注入的好手。Flask和Tornado经常使用Sqlalchemy,而Django有自己自带的orm引擎。举一个用Sqlalchemy建模型类,并使用模型类查询用户数据的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from datetime import datetime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, DateTime engine = create_engine('mysql+pymysql://user:[email protected]/test') DBSession = sessionmaker(bind=engine) session = DBSession() Base = declarative_base() class user_t(Base): __tablename__ = 'user_t' user_id = Column(Integer, primary_key=True) username = Column(String) userpassword = Column(String) createtime = Column(DateTime, default=datetime.utcnow) |
正常的查询与数据展示:
1 2 3 |
>>> user = session.query(user_t).filter(user_t.username=='test').first() >>> user.__dict__ {'username': 'test', 'userpassword': '098f6bcd4621d373cade4e832627b4f6', '_sa_instance_state': |
在数据库执行的sql语句为:
1 2 3 4 |
SELECT user_t.user_id AS user_t_user_id, user_t.username AS user_t_username, user_t.userpassword AS user_t_userpassword, user_t.createtime AS user_t_createtime FROM user_t WHERE user_t.username = 'test' LIMIT 1 |
如果我们构造sql注入测试语句,并传入Sqlalchemy的查询语句中,看一下返回。
1 2 3 |
>>> user = session.query(user_t).filter(user_t.username=="test'").first() >>> print user None |
那么在数据库中执行的sql语句是什么呢?
1 2 3 4 |
SELECT user_t.user_id AS user_t_user_id, user_t.username AS user_t_username, user_t.userpassword AS user_t_userpassword, user_t.createtime AS user_t_createtime FROM user_t WHERE user_t.username = 'test\'' LIMIT 1 |
由此可见在当Sqlalchemy接收到字符串进行查询时,在构造SQL语句的时候,会默认使用单引号包裹字符串,如果字符串内含有单引号的话,会使用\
进行转义。从而达到过滤单引号的效果。
我们知道原生的sql语句在进行字符串拼接的情况下,容易产生sql注入,那Sqlalchemy是否支持执行sql语句呢?答案是肯定的,下面是Sqlalchemy执行sql语句的一个例子。
1 2 3 4 5 |
In [18]: from sqlalchemy import text In [19]: sql = text('SELECT * from user_t WHERE username = :username;') In [20]: data = session.execute(sql, {'username':'test'}).fetchall() In [21]: data Out[21]: [(3, 'test', '098f6bcd4621d373cade4e832627b4f6', datetime.datetime(2016, 7, 6, 6, 50, 16))] |
那么这种情况下,会造成sql注入吗?同样我们传入test'
字符串,看看是否会进行对其进行过滤。
1 |
In [22]: data = session.execute(sql, {'username':"test'"}).fetchall() |
在数据库执行的sql语句为SELECT * from user_t WHERE username = 'test\''
,可见Sqlalchemy对其进行了相同的处理。那么是不是使用Sqlalchemy的情况下就不用产生sql注入了呢?显然,如果正确使用Sqlchemy的话,出现sql注入的情况会大大的降低,但是愚蠢的sql语句处理方法,同样会导致sql注入。如果不使用execute传入参数,而是使用python格式化字符串或拼接字符串的话,出现sql注入的概率会大大增加。示例代码:
1 2 3 4 5 |
>>> sqli_payload = "test'" >>> sql = text("SELECT * from user_t WHERE username = '%s'" %sqli_payload) >>> data = session.execute(sql).fetchall() ProgrammingError: (pymysql.err.ProgrammingError) (1064, u"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''test''' at line 1") [SQL: u"SELECT * from user_t WHERE username = 'test''"] |
报错,从错误信息上或查看数据库记录可见,单引号被成功带进了sql语句中。因此我们就可以构造payload获取数据。例如:sqli_payload = "test' union select user(),1,2,3#"
或sqli_payload = "test' union SELECT host,user,1,2 FROM mysql.user LIMIT 1 OFFSET 1#"
。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
In [22]: sqli_payload = "test' union select user(),1,2,3#" In [23]: sql = text("SELECT * from user_t WHERE username = '%s'" %sqli_payload) In [24]: data = session.execute(sql).fetchall() In [25]: data Out[25]: [('3', 'test', '098f6bcd4621d373cade4e832627b4f6', '2016-07-06 06:50:16'), ('root@localhost', '1', '2', '3')] In [33]: sqli_payload = "test' union SELECT host,user,1,2 FROM mysql.user LIMIT 1 OFFSET 1#" In [34]: sql = text("SELECT * from user_t WHERE username = '%s'" %sqli_payload) In [35]: data = session.execute(sql).fetchall() In [36]: data Out[36]: [('%', 'root', '1', '2')] |
结合上面提到的Flask传入参数的方法,我们可以整理在Flask+Sqlalchemy
的情况下,比较容易产生sql注入的情况。
综合以上几点,我们写一个基于flask的单文件web小程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
from flask import Flask, request, render_template_string from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from datetime import datetime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, DateTime, text app = Flask(__name__) engine = create_engine('mysql+pymysql://root:[email protected]/test', echo=True) DBSession = sessionmaker(bind=engine) session = DBSession() Base = declarative_base() @app.route('/id- |
注入测试:
\
过滤字符串中的单引号。那么宽字符注入在使用本文的环境中是否可行呢?答案是否定的,Flask默认会将所有传入的字符串转为unicode,但不排除使用别的PythonWeb框架结合Sqlalchemy会产生宽字符注入的情况。Web应用包含sql注入的情况,通常的想法会使用sql注入写文件拿webshell。但是写webshell的情况,在多数的PythonWeb框架或PythonWeb生产环境中并不管用。当然这并不代表,sql注入的危害性在PythonWeb环境中会降低,你依旧可以使用它来进行很多危险的行为。PythonWeb框架会产生的安全问题也有很多有趣的地方值得我们思考,我会继续分析其他的诸如XSS,SSRF等漏洞在PythonWeb上面所表现的特点,也会分析诸如Pickle反序列化,Flask强大的Debug模式等Python特性可能产生的安全问题。