关于SQL注入及防御

关系型数据库

  • 数据管理系统
  • 主要用于存放结构化数据
  • 可高效操作大量数据
  • 方便处理数据之间的关联关系
  • 常见关系型数据库
    • access: 微软出品,小型数据库,使用文件存储,容量不能太大 x.mdb
    • sqlite: 可移植,文件型存储系统,适用于嵌入设备,主要应用于手机app存储 x.db
    • mysql: 开源数据库, 提供服务, 通过协议通信, 支持大量数据和并发, x.sql
    • mssql server: 微软提供关系型数据库, 收费
    • oracle: 大型数据库, 使用率比较少, 大型企业使用,收费
  • SQL语言
    • 用于操作sql数据库
    • 查询语言:select * from table where id = 1
    • 标准化语言,但语法使用并非完全一致
    • 类似自然语言的描述性语言
    • 用于关系型数据库

SQL注入

  • 在web应用中,通过拼装用户操作参数成sql语言来查询数据库,将结果返给用户
  • 在这个过程中,如果处理不当,会出现sql注入
  • 比如:select * from table where id = ${id};
    • 如果我们把这个条件从1变成1 or 1 = 1, 即:
    • select * from table where id = 1 or 1 = 1;
    • 这时候语义完全不一样了
  • 再比如:select * from user where username = '${data.username}' and password = '${data.password}'
    • 这时候,我们通过传值,把password变更为:1' or '1' = '1
    • select * from user where username = 'johnny' and password = '1' or '1' = '1'
    • 最后的or把前面的条件给否定了,此时用户名和密码完全没用了
  • 这种SQL注入和XSS攻击非常类似,也就是混淆了程序和数据即数据变成了程序
  • 一些神奇的SQL语法
    • select * from table where id="10" and 1=0 这种查不到结果
    • select * from table where id="10" or 1=1 这种改变含义
    • select * from table where id="10" and mid(version(), 1, 1)=5 这种用于探测数据库版本,针对版本漏洞下手
    • select 1,2,3 from table 没有指定列的名字,查出来的结果是1,2,3
    • select id,1,2,3 from table 查出来的结构是这条记录的id,后面加上1,2,3
    • select * from table union select 1,2,3 from table2 联合查询, 后面再加一条1,2,3的数据,用于探测列数
    • select * from table where mid(username, 1, 1)="t" 用于探测数据库中某个值

SQL注入演示

  • 比如url上是这么一个地址:http://localhost/post/10
  • 程序处理上是这样的
    exports.post = async function(ctx, next){
        try{
            const id = ctx.params.id;
            const connection = connectionModel.getConnection();
            const query = bluebird.promisify(connection.query.bind(connection));
            const posts = await query(
                `select * from post where id = "${id}"`
            );
            let post = posts[0];
            const comments = await query(
                `select comment.*,user.username from comment left join user on comment.userId = user.id where postId = "${post.id}" order by comment.createdAt desc`
            );
            if(post){
                ctx.render('post', {post, comments});
            }else{
                ctx.status = 404;
            }
            connection.end();
        }catch(e){
            console.log('[/site/post] error:', e.message, e.stack);
            ctx.body = {
                status: e.code || -1,
                body: e.message
            };
        }
    };
    
  • 一般攻击者会假设网站有sql注入的漏洞,比如这个查询语句:select * from post where id = 10
  • 攻击者假设这个10是文章的id, 攻击者就会猜测可能是上面的sql语句
  • 页面上输入的参数是10,这时候,就可以拼凑测试,比如在url上拼接
    • 1 ) 判断是否可以注入, 一般而言,如果可以的话页面正常,不报错,但是也有其他情况,如下
    • http://localhost/post/10 and 1=1
    • http://localhost/post/10 and 1=0
    • 这两个都没什么反应,不报错,页面正常,说明后面的and没有起作用
    • 攻击者会猜测可能是字符串存在引号的问题, 继续尝试
    • http://localhost/post/10" and 1=0 and ""=" 这样如果页面出错,那么继续修改
    • http://localhost/post/10" and 1=1 and ""=" 这时候页面不报错
    • 这种情况(恒等正常,恒不等报错)就说明,一般存在sql注入的问题,这时候,我们继续
    • 2 ) 判断是否存在信息泄露的问题
    • http://localhost/post/10" or 1=1 and ""=" 此时,and变成了or
    • 如果发现内容变化,就说明存在信息泄露, 很多时候在一些场景中,用户登录后查看信息是有权限控制的
    • 只有和自己有关的,或者说自己产生的可以查看,如果是上面这种情况,那就说明权限存在漏洞,可以查看别人的数据
    • 3 ) 探测服务器的版本
    • http://localhost/post/10" and mid(version(),1,1)=5 and ""=" 如果是5,正常
    • http://localhost/post/10" and mid(version(),1,1)=6 and ""=" 如果是6或其他,报错
    • 这就意味着mysql的主版本号是5,同样,继续
    • http://localhost/post/10" and mid(version(),2,1)="." and ""=" 看第二位是否是点"."
    • http://localhost/post/10" and mid(version(),2,1)="2" and ""=" 换成其他的
    • 如果是点,页面正常,换成其他的数据,报错,则意味着,版本号第二位是点(“.”), 我们继续看第三位
    • http://localhost/post/10" and mid(version(),3,1)=5 and ""=" 如果是5,正常
    • http://localhost/post/10" and mid(version(),3,1)=6 and ""=" 如果是6或其他,报错
    • 这时候,我们就可以猜测数据库的版本号是5.5,由此,我们可以继续各种探测
    • http://localhost/post/10" union select 1,2,3,4,5,6 from user where ""=" 假设有6列,正常
    • http://localhost/post/10" union select 1,2,3,4,5 from user where ""=" 假设有5列,报错
    • 这时候,我们就知道post表有6个字段,我们继续
    • http://localhost/post/10" union select id,username,password,4,5 from user where ""="
    • 如果正常,那么我们就可以猜测user表中的一些关键字段信息了,这时候,我们继续
    • 将post后的id换成一个不存在的,假设99999999
    • http://localhost/post/99999999" union select id,username,password,4,5 from user where ""="
    • 这时候前面的sql不生效,我们直接可以通过后面的sql及更多的变换,来获取用户信息了
    • http://localhost/post/99999999" union select id,username,password,4,5 from user where id = 2 and ""="
    • 这是一个非常严重的问题,在环境和版本漏洞中还会存在各种各样的安全问题
    • sql注入是一门非常大的学问,是很复杂的领域,有很多人在研究,有各种各样的写法,防不胜防,要对此问题足够重视

SQL注入的危害

  • 猜解密码(有工具会快速完成)
  • 获取数据(批量拿走数据)
  • 删库删表(union后面接上drop table等语句)
  • 拖库(本来是展示一部分数据,后来所有数据都被拿走了)

相关SQL注入案例

  • 腾讯校招,按号码和手机号查询面试结果的页面,可随机找到记录
  • 淘宝分站的漏洞,可猜解管理员账号
  • 微博的movieapp产品,自动注入工具
  • 支付宝某分站,自动化注入工具检测
  • 由此可见, sql注入是非常普遍的问题,不止于小公司,大公司都有

SQL注入的防御

1 ) 关闭页面错误输出

  • 也就是如果错误,则不会让页面报错显示,避免帮助攻击者判断
  • 比如将之前的代码,如下
    ctx.body = {
        status: e.code || -1,
        body: e.message
    };
    
  • 修改成现在的代码
    ctx.body = {
        status: -1,
        body: '出错啦'
    };
    

2 ) 检查参数类型

  • 比如原先是数值型的,就保证它必然是数值型的,否则报错
  • 如之前的代码,如下
    const id = ctx.params.id;
    
  • 修改成现在这样
    let id = ctx.params.id;
    id = parseInt(id, 10)
    

3 ) 对数据进行转义

  • 如果是数字,解析一下成数值型即可,如果是字符串需要对其进行转义
  • 也就是将涉及到可能变成程序的字符转义成纯字符
  • 我们可以自己写replace进行转义, 也可以利用现成的方法
  • 在npm上安装的mysql库是自带有一些转义的方法,如escape可以使用
  • escapme方法暴露在mysql模块,connection连接,connection pool连接池三个对象上
  • 我们选择在connection对象上来处理,如下
  • 原来的代码
    const id = ctx.params.id;
    const posts = await query(
        `select * from post where id = "${id}"`
    );
    
  • 转义后的代码
    const id = ctx.params.id;
    const posts = await query(
        `select * from post where id = ${connection.escape(id)}`
    );
    
  • 使用escape函数就可以防御sql注入的攻击了,但有可能输入错误参数会报错,但不能够改变本来的意图
  • mysql模块还提供了别的语法,如
    const id = ctx.params.id;
    const posts = await query(
        `select * from post where id = ?`, [id]
    );
    
  • 这里把id作为参数传进去,但是在传进去之前,会做转义
  • 其实转义是一个比较麻烦的方法,sql不可能用白名单来处理
  • 转义只能解决大部分问题,sql有无数种变种,让你防不胜防

4 ) 参数化查询方案

  • 数据库提供商,比如mysql提供的解决方案
  • 它会把sql和参数分开,sql表示查询意图,参数仅仅是特定参数不会造成别的影响
  • 也即是一个sql语句,分成2部分来执行
  • 这个是数据库标准,和语言无关,只要语言能按照其规则即可
  • npm中的mysql库暂不支持参数化查询的方式,我们需要升级一下成mysql2
    • $ cnpm i -S mysql2
  • mysql2提供了向下兼容的API,也就是说代码不用变,增添了新的的函数
  • 我们将原来项目代码引用的mysql, 换成mysql2, 其他都基本不用变动
  • 其参数化查询,用的是execute方法
  • 原代码
    const id = ctx.params.id;
    const connection = connectionModel.getConnection();
    const query = bluebird.promisify(connection.query.bind(connection));
    const posts = await query(
        `select * from post where id = "${id}"`
    );
    
  • 修改后的代码
    const id = ctx.params.id;
    const connection = connectionModel.getConnection();
    const query = bluebird.promisify(connection.execute.bind(connection)); // 修改这里,将query修改成execute
    const posts = await query(
        `select * from post where id = ?`, [id]
    );
    
  • 修改之后,可以将一个sql分成两步来执行,我们可以通过Wireshark这个工具来查看sql语句验证
  • 进入该工具后,可以看到自己本机电脑上的网卡信息及其流量信息
  • 如果是外网通信,选择Wi-Fi:en0这个网卡,如果是本机自己通信,选择Loopback:lo0这个网卡
  • 进去之后可以看到所有通信的包,为了去除干扰,我们可以在最上面过滤输入mysql
  • 当我们再次刷新我们的项目网站, 就可以看到Wireshark上的包信息
  • 我们可以看到有一个Request Prepare Statement的包
    • 点击进入,可以看到 select * from post where id = ?的信息
  • 还有一个Request Execute Statement的包
    • 点击进入,可以看到相关参数信息
  • 当然,可以还原代码,将mysql2还原成mysql,并将代码中的execute修改成query
  • 再次进行同样的刷新操作,进而只能看到一条Request Query的信息
  • 使用参数化查询,用户就无法改变sql语句的意图,这样就没法产生注入
  • 这是最彻底,最根本的解决方案

5 ) 使用ORM(对象关系映射)

  • 之所以存在sql注入,是因为我们要拼接sql语句
  • 参数化查询是通过意图+数据的形式避免拼接过程存在的可能攻击
  • 如果不去管sql相关的问题,我们就需要用到ORM
  • 就是通过代码中的对象映射数据库中的一条数据
  • 我们首先要进行orm工具的安装,最常用的mysql的ORM工具是sequelize
  • $ cnpm i -S sequelize
  • 新建sequelize实例
    // sequelize.js
    var Sequelize = reuqire('sequelize');
    var sequelize = new Sequelize({
        host: '',
        // port: 3306, // 端口默认不用写
        database: '', // 数据库名
        username: 'root', // 用户名
        password: '', // 数据库密码
        define: {
            freezeTableName: true // seq 根据对象名称会寻找对应的数据库表名,不用默认规则,定义模型时要同时提供表名
        }
    })
    module.exports = sequelize;
    
  • 定义对象模型, 每个表都映射一个对象
    // post.js
    var sequelize = require('path/to/sequelize') // 引入上面我们刚写的
    var Sequelize = require('sequelize'); // 引入安装的模块
    var Post = sequelize.define('post', {
        id: {
            type: Sequelize.INTERGER,
            primaryKey: true
        },
        title: Sequelize.STRING(256), // 这里直接用一个属性,不用指定对象的写法了
        imgUrl: Sequelize.STRING(256),
        content: Sequelize.TEXT
    }, {
        tableName: 'post' // 指定表名
    })
    
    module.exports = Post;
    
  • 使用Post模型
    const Post = require('path/to/post') // 引入上面的模型
    let id = ctx.params.id;
    let posts = await Post.findById(id);
    console.log(posts);
    
  • 上面代码仅提供示例,由此可见ORM会让代码更加简洁,不需要管理和关闭连接的问题,不用拼接sql
  • 这样对我们的开发效率和安全保证非常有好处

PHP的SQL注入

  • 原理和上面共通,也有上面几种,这里提供一下简单的实现

  • 1 ) 检查数据类型

    // 获取传递参数id并转义
    $id = intval($_GET['id']);
    // 建立数据库连接
    $conn = mysql_connect("127.0.0.1", "root", "123456");
    // 选择DB
    mysql_select_db("safety", $conn);
    // 准备SQL语句
    $sql = "SELECT * FROM post where id ='".$id."'";
    // 查询DB
    $result = mysql_query($sql);
    // 获取结果,如果结果是多行数据,需要用一个循环来获取
    $row = mysql_fetch_array($result);
    
  • 2 ) 对数据进行转义

    // 建立数据库连接
    $conn = mysql_connect("127.0.0.1", "root", "123456");
    // 选择DB
    mysql_select_db("safety", $conn);
    // 获取传递参数title并转义
    // $title = addslashes($_GET['title']); // 将字符引号变成\转义,但并不是完全安全
    $title = mysql_real_escape_string($_GET['title']); // 除了引号转义,还包含其他额外字符更安全, 连接创建好之后再去调用
    // 准备SQL语句
    $sql = "SELECT * FROM post where title like '%".$title."%'";
    // 查询DB
    $result = mysql_query($sql);
    // 获取结果,如果结果是多行数据,需要用一个循环来获取
    $row = mysql_fetch_array($result);
    
  • 3 ) 使用参数化查询

    • mysql_connect是基于早期的一个php扩展叫做php-mysql, 使用非常广泛但不支持参数化查询
    • 在现代php的开发中已被摒弃了,推荐使用pdo-mysql, 提供参数化的查询方式
    • 具体的使用示例如下
    // 建立数据库连接
    $conn = new PDO(
        'mysql:host=127.0.0.1;dbname=safety',
        'root',
        '',
        []
    )
    // sql意图
    $stmt = $conn->prepare("SELECT * FROM post where title like :title");
    // sql参数
    $stmt->execute([
        'title' => '%'.$title.'%'
    ]);
    $rows = $stmt->fetchAll();
    $row = $rows[0];
    
  • 4 ) 关于ORM

    • 在实际的php开发中不会单独的使用orm组件
    • 因为php和html代码是混编的,但是在现代php的开发中是见不到的,维护性太差
    • 现代php中都会用到框架, 框架会负责路由,模板,数据库连接等,这里就会有ORM的封装
    • 一般选择框架的时候,使用其自身自带的ORM工具即可,会有详细的文档说明
    • 其底层一般都是使用PDO封装,但不排除一些老的框架使用php-mysql来封装
    • 推荐使用框架自带ORM来实现防SQL注入

你可能感兴趣的:(Web,sql,数据库,sqlite)