前言

好久没写文章了,最近一直都是在看文章。
近期写了好几个PythonWeb项目,在实现需求的过程中,一直在思考PythonWeb开发过程中会遇到哪些常见的Web安全问题呢?这些问题又在什么情况下会被利用者GetShell呢?下面就分类来讨论这些问题:(这也是重新启用博客的第一篇文章,顺便投给90Sec也不知道能不能过QAQ。)

常见漏洞分析

SQLI

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建模型类,并使用模型类查询用户数据的例子:

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': <sqlalchemy.orm.state.InstanceState object at 0x03F96530>, 'user_id': 3, 'createtime': datetime.datetime(2016, 7, 6, 6, 50, 16)}

在数据库执行的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'),
('[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注入的情况。

  1. 获取get、post请求参数没有限定type或指定type为str
  2. 同样的,定义url参数没有限定参数类型或指定参数回调为str
  3. 使用用户可控的参数进行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-<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)

注入测试:

总结与思考

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

最后再说点啥

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

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

下一章XSS…

本文URL: https://blog.neargle.com/2016/07/22/pythonweb-framework-dev-vulnerable/index.html