Web安全入门演练一:环境准备,初探XSS

本节内容将通过构建一个带有各种漏洞为Web应用,演示Web安全中较为经典的案例:跨站脚本攻击。

在此之前你需要了解这样一些内容:

  • 了解基本的HTTP知识(GET、POST请求及其参数)
  • 了解基本的Web前端知识(HTML、JavaScript)
  • 了解基本的数据库知识(SQL)

本节内容涉及的技术环境:

  • Python 2.7
  • SQLite3
  • Flask

相关文档自行百度,关键字即为上述内容。为方便读者自行研究,文末会给出本例中的一份完整的参考代码。

环境准备

安装所需软件

pip install flask

建议使用PyCharm作为编程环境,便于自动import一些需要的库。

问题背景

本文通过先构建一个最小的“社交应用”,并刻意在应用中留下一些漏洞进行演示和分析,以此向读者阐明此类安全问题的原理、引发的原因。

这个社交应用的基本功能如下:

  • 用户登录
  • 用户在time line中发帖
  • 用户删除time line中的某一项

即类似QQ空间发说说的功能,每个用户都能发表说说,用户也能看到其他人发表的说说。

应用设计

数据库

整个应用需要有两个数据库表,一个为user表,存放用户信息,一个为time_line表,存放用户发表的内容。

相应的DDL如下:

CREATE TABLE IF NOT EXISTS user(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username VARCHAR(32),
    password VARCHAR(32)
);
CREATE TABLE IF NOT EXISTS time_line(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER,
    content TEXT,
    FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
);

time_line表中通过外键user_iduser表中的用户建立关联,表明某个time_line和某个用户之间的所述关系。

controllers

这里暂且叫controller吧。整个应用对外暴露了下面这些URL:

  • /

应用首页:若用户已登录,则展示所有用户在time line下留下的记录;若用户未登录,则跳转到登录界面

  • /login

登录界面:用户在此进行登录

  • /create_time_line

创建一个time line,接受浏览器发来的POST请求,创建一个time line并存入数据库。

  • /delete/time_line/

删除一个time line,用户访问此URL并附上time lineid后即可删除对应的time line,URL中的即为time lineid

功能实现

数据库创建

在开发功能之前,先准备一些数据,便于测试。准备数据工作有两个内容:

  • 建立数据库表
  • 初始化测试数据

先准备一个文件:app.py,开始写代码:

  • 先准备连接数据库
DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'database.db')


def connect_db():
    return sqlite3.connect(DATABASE_PATH)
  • 创建数据库表
def create_tables():
    conn = connect_db()
    cur = conn.cursor()
    cur.execute('''
            CREATE TABLE IF NOT EXISTS user(
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username VARCHAR(32),
            password VARCHAR(32)
            )''')
    cur.execute('''
        CREATE TABLE IF NOT EXISTS time_line(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        content TEXT,
        FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
        )''')
    conn.commit()
    conn.close()
  • 插入一些简单的测试数据
def init_data():
    users = [
        ('user1', '123456'),
        ('user2', '123456')
    ]
    lines = [
        (1, 'Hello'),
        (1, 'World'),
        (2, 'Im 2'),
        (2, 'Hello 2')
    ]
    conn = connect_db()
    cur = conn.cursor()
    cur.executemany('INSERT INTO `user` VALUES(NULL,?,?)', users)
    cur.executemany('INSERT INTO `time_line` VALUES(NULL,?,?)', lines)
    conn.commit()
    conn.close()
  • 把这两个功能组合起来,一会儿方便用
def init():
    create_tables()
    init_data()

到此,app.py中包含了初始化数据库的功能。

需要初始化数据的时候可以这么做,在app.py所在的文件夹下执行python,进入shell调用init函数:

$ python
>>> from app import *
>>> init()
>>> exit()

这样,数据就初始化好了。测试数据集中有两个用户,每个用户分别都发了两条动态。

功能封装

在开始编写各个controller之前,先将整个应用的基础功能进行一次封装,方便稍后使用:

  • 根据用户名、密码查询用户(若没有找到匹配的用户则返回None
def get_user_from_username_and_password(username, password):
    conn = connect_db()
    cur = conn.cursor()
    cur.execute('SELECT id, username FROM `user` WHERE username=\'%s\' AND password=\'%s\'' % (username, password))
    row = cur.fetchone()
    conn.commit()
    conn.close()

    return {'id': row[0], 'username': row[1]} if row is not None else None
  • 根据用户的id查询对应的用户
def get_user_from_id(uid):
    conn = connect_db()
    cur = conn.cursor()
    cur.execute('SELECT id, username FROM `user` WHERE id=%d' % uid)
    row = cur.fetchone()
    conn.commit()
    conn.close()

    return {'id': row[0], 'username': row[1]}
  • 根据用户的idtime line的内容创建一条动态
def create_time_line(uid, content):
    conn = connect_db()
    cur = conn.cursor()
    cur.execute('INSERT INTO `time_line` VALUES (NULL, %d, \'%s\')' % (uid, content))
    row = cur.fetchone()
    conn.commit()
    conn.close()

    return row
  • 获取所有用户的动态信息(返回的数据格式为字典数组)
def get_time_lines():
    conn = connect_db()
    cur = conn.cursor()
    cur.execute('SELECT id, user_id, content FROM `time_line` ORDER BY id DESC')
    rows = cur.fetchall()
    conn.commit()
    conn.close()

    return map(lambda row: {'id': row[0], 'user_id': row[1], 'content': row[2]}, rows)
  • 根据用户的id和动态的id删除一个动态
def user_delete_time_line_of_id(uid, tid):
    conn = connect_db()
    cur = conn.cursor()
    cur.execute('DELETE FROM `time_line` WHERE  user_id=%s AND id=%s' % (uid, tid))
    conn.commit()
    conn.close()

有了这样一些基本功能的封装之后,后面需要使用的时候就可以直接在controller函数里调用了。

视图

整个Web应用有两个界面:登录界面、动态列表界面。为了简化程序,这里的这两个界面都是直接通过返回符合html语法的字符串直接产生,而不再去设置单独的html作为模板。

两个视图如下:

  • 登录界面
def render_login_page():
    return '''

'''
  • 动态列表界面
def render_home_page(uid):
    user = get_user_from_id(uid)
    time_lines = get_time_lines()
    template = Template('''

I am: {{ user['username'] }}

Add time line:
    {% for line in time_lines %}
  • {{ line['content'] }}

    {% if line['user_id'] == user['id'] %} Delete {% endif %}
  • {% endfor %}
''') return template.render(user=user, time_lines=time_lines)

这两个函数目前仅仅负责根据传入的参数,查询数据库并通过字符串模板渲染成html字符串,然后返回渲染后的html

render_home_page用于渲染整个应用的首页,也就是所有用户的动态列表,当某个动态的所有者为当前用户的时候,还会在动态中渲染出一个对应的删除链接,方便用户删除动态。

controllers

在对应的controller中编写应用的业务逻辑,前面先拉出一长串代码,就是为了进行足够的封装,让整个应用的controller部分足够简洁清晰,整个应用的逻辑一目了然。

以下便是整个应用的主体框架:

@app.route('/')
def index():
    if 'uid' in session:
        return render_home_page(session['uid'])
    return redirect('/login')


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_login_page()
    elif request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = get_user_from_username_and_password(username, password)
        if user is not None:
            session['uid'] = user['id']
            return redirect('/')
        else:
            return redirect('/login')


@app.route('/create_time_line', methods=['POST'])
def time_line():
    if 'uid' in session:
        uid = session['uid']
        create_time_line(uid, request.form['content'])
    return redirect('/')


@app.route('/delete/time_line/')
def delete_time_line(tid):
    if 'uid' in session:
        user_delete_time_line_of_id(session['uid'], tid)
    return redirect('/')


@app.route('/logout')
def logout():
    if 'uid' in session:
        session.pop('uid')
    return redirect('/login')


if __name__ == '__main__':
    app.run(debug=True)

这部分代码就比较简单了。稍微说明一下,用户登录成功后,会在session中保存用户的id,以此标识当前登录的用户。

应用首页会判断用户是否登录,若用户未登录,则会跳转到登录界面要求用户登录。若用户已登录,则渲染首页,首页中的内容为所有用户发布的动态。

好了,整个应用到这里基本就搭建完了。

测试

启动程序:

$ python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 233-701-647

如果你运行python app.py的时候看到上述结果,说明整个应用基本构建好了。接下来可以开始尝试了。

按照提示,访问http://127.0.0.1:5000/,按照预期,浏览器会自动跳转到登录界面http://127.0.0.1:5000/login,还记得前面创建的测试数据吗?随便选择一个用户登录进去,比如这里使用user1:123456进行登录。

不出意外的话,登录之后便能看到测试数据中的内容了。在界面上方的文本框中输入一些内容,点击Submit后即可创建新的动态(相当于在空间中发了一条新的说说)。

你还可以点击自己发的动态下方的Delete删除对应的动态。

到这里,应用环境的搭建才算完工。

来一发

现在要开始搞事情了,验证下环境有没有设置好。在创建动态的文本框中输入如下内容作为动态:

XSSed

之后提交,创建动态,看看页面刷新后会发生什么。

如果你发现了诡异的现象,说明环境搭建成功了。

跨站脚本攻击

上述实例演示了一个简单的跨站脚本攻击,其实也就是当用户输入的内容会渲染到HTML页面中的时候,就有可能存在这样的问题。

即我在文本框中输入一段含有script标签的html代码,之后再次访问页面时,页面中包含了刚才构造出的特殊内容,这段内容会被浏览器傻乎乎地当作脚本也执行了。

当然,上述示例只是弹出了一个框而已,可能感觉仅仅是影响使用,构不成大威胁。那可难说了,笔者后续会引出一些危害更大的代码,到时候就可以感受到这个问题时多么的可怕了。比如这样恶搞一下:


在小白用户面前,这个应用就已经瘫掉了没法用了,这对一个互联网产品来说危害也相当大。

Summary

后续内容将会给出更多更恶劣的问题,以及给出一些对应的解决方法。

在此就可以开始Web安全的了解了,并非说必须得是安全从业者才需要了解安全相关的内容,应用开发者也同样需要学习,保证开发过程中不会犯下低级错误,最终可能会导致产品发布后受到比较恶劣的影响。

上述示例的代码可以在这里找到:

https://gist.github.com/hackeris/fa2bfd20e6bec08c8d5240efe87d4687

文中若有疏漏或不当之处,欢迎指正。

你可能感兴趣的:(Web安全入门演练一:环境准备,初探XSS)