代码审计的角度分析sql注入

代码审计的角度分析sql注入

一、sql注入起源

  • SQL注⼊第⼀次为公众所知,是在1998年的著名⿊客杂志《Phrack》第54期上,⼀位名叫rfp的⿊客发表了⼀篇题为“NT Web Technology Vulnerabilities”的⽂章,在⽂章中,第⼀次向公众介绍了这种新型攻击。下⾯是当时给出的例⼦,现在看已经很古⽼了。
var ShipCity;
ShipCity = Request.from("ShipCity");
var sql = "select * from OrderTable where ShipCity = '" + ShipCity +"'";
  • 变量ShipCity为用户所提交,正常情况下,假如用户输入“glc”,那么SQL语句会执行
select * from OrderTable where ShipCity = 'glc';
  • 若用户输入一段有特别含义的SQL语句,比如:
glc' ;select version ();--
  • 那么SQL语句在实际执行时就会如下
select * from OrderTable where ShipCity = 'glc' ;select version ();--';
  • 现在变成了查询后,再执行一个查询数据库版本的操作,属于用户非法行为
  • 近⼏年,因为规范化的框架使⽤、orm的普及,基本在框架层⾯就解决了⼤部分普通sql注⼊的问题,sql注⼊越来越难,所以表现就是sql注⼊不那么受关注了。但需要注意的是,sql注⼊仍然是危害巨⼤(拿数据库、什么拿shell、拿系统权限)的攻击⼿法。

二、sql注入的基础代码样例

1.调用原生数据库操作模块
  • php

    <?php
    $db = init_db();
    $username = $_GET['username'];
    $db->query("select * from table where username = '$username'");
    ?>
    
  • .net/aspx

    string connectionstring = "xxx";
    SqlConnection con = new SqlConnection(connectionstring);
    con.Open();
    
    string username = Request.QueryString["username"];
    string sql = "select * from table where username = '" + username + "'";
    SqlDataAdapter adapter = new SqlDataAdapter(sql, con);
    DataSet dataSet = new System.Data.DataSet();
    adapter.Fill(dataSet);
    con.Close()
    
  • java

    conn = DBHerpel.getConnection();
    if (conn == null)
    return;
    String username = request.getParameter("username");
    String Sql = "select * from table where username = '" + username + "'";
    stt = conn.createStatement();
    set = stt.executeQuery(Sql);
    
  • python

    @app.route("/", methods=["GET"])
    def test():
    	username = request.args.get('username')
    	sql = "select * from table where username = '" + username + "'"
    	conn = connect(host='localhost',port=3306,user='root',password='',database='test',charset='utf8')
    	cs1 = conn.cursor()
    	count = cs1.execute(sql)
    
  • 以上全都是不同语⾔在不调⽤orm框架,直接调⽤原⽣数据库操作函数时的⽤例。其实orm框架的底层也是调⽤了原⽣数据库操作函数,只是orm帮开发者做了封装和对象映射的步骤,这种显⽽易⻅有漏洞的

  • 数据流⼊侵控制流产⽣的⻛险点,在于不同层⾯组件的交汇处,如:代码层与数据库层

  • 关于orm相关知识可以查看这里

2.调用orm的错误写法
  • php

    • php没有啥⽐较出名的orm框架,基本是各web框架或者cms⾃⾏去实现⾃⼰的orm,⼀套闭源cms的orm可能实现得并不规范,从而产⽣漏洞,即使⾃⼰写了⼀套orm,还有防注⼊,但还是被绕过了
  • java/MyBatis

    • java的持久层技术解决⽅案可以发展历程与MyBatis的详解看

    • MyBatis需要有⼀个xml配置⽂件来来绑定映射关系

      <select id="findUserByName" parameterType="java.lang.String"
      	resultType="cn.itcast.mybatis.po.User">
      	<!-- 拼接 MySQL,引起 SQL 注⼊ -->
      	SELECT * FROM table WHERE username = '${value}'
      </select>
      
      	@Test
      	public void testFindUserByName() throws Exception{
      		SqlSession sqlSession=sqlSessionFactory.openSession();
              
      		//创建UserMapper代理对象
      		UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
              
      		//调⽤userMapper的⽅法
      		List<User> list=userMapper.findUserByName("fuckdada' and 1=1#");
              
      		sqlSession.close();
      		System.out.println(list);
      	}
      
    • 简单讲就是MyBatis有两种变量绑定⽅式,分别是:#{}${}

      #是绑定变量的形式,底层会⽤#{}会被替换为?号,有参数映射,会在DefaultParameterHandler中进⾏设置占位符的操作 -->预编译
      
      $也是绑定变量的形式,{value}是直接被替换为了对应的值,没有参数映射,不会进⾏设置占位符的操作 -->拼接
      

      所以这也是很多代码审计初学者上来就去找**$符号**的原因。⼤家后续遇到MyBatis也确实可以这么搞,对于⼩cms简单有效

  • python/flask/sqlalchemy

    • sqlalchemy是flask最经常配套的orm框架,在许多django项⽬中也常常看到身影,也是⽬前python上⽤得最⽕的orm框架,详情请见这里

    • 注⼊漏洞的例⼦

      @app.route("/", methods=["GET"])
      def test():
      	username = request.args.get('username')
      	res = db.session.query(table).filter("username={}".format(username))
      

      原因跟上⾯MyBatis类似,依然是⽤户的输⼊其实是拼接后才导⼊sqlalchemy层的(不同层⾯组件的交汇处)

    • 正确的写法

      @app.route("/", methods=["GET"])
      def test():
      	username = request.args.get('username')
      	res = db.session.query(table).filter(table.username == username)
      

三、sql注入到底在注入什么

  • 通常会把sql注入分为sql联合查询注⼊、sql堆叠注⼊、sql报错注⼊、sql时间盲注、sql布尔盲注、sql带外数据、sql注⼊执⾏命令等等,但是在这里我们要思考一个问题,那就是他们的共性是什么,也就是总结出一个sql注入的核心思维

  • 核心思维就是想法设法去执行⼀条完全的sql语句,把数据带出来或把命令传进去。归根结底sql注入就是在执⾏⼀段sql语句,那么我们的关注点就应该放在数据库类型。

  • 其他次要关注点

    • 编程语言

      不同编程语⾔最终的⽬的都是为了将我们的payload送⼊数据库层进⾏执⾏,能看到注⼊点即可,编程语⾔没那么重要。

    • 注⼊类型

      不需要特别关注分类,所有的分类都是sql语句的不同写法⽽已,⽐编程语⾔相对重要⼀些

      ⼀条sql语句,最终会被解析成⼀段控制序列

      action(动作): select
      object(对象): table
      subject(⽬标客体): *
      condition(条件):
      	key: username
      	value: $username  // ⽤户输⼊
      

      如果能够使⽤堆叠注⼊,我们就能跳出action动作,执⾏其他sql语句,肯定危害更⼤。
      ⽬前来看,堆叠注⼊越来越少。经验上来看,mssql数据库的堆叠注⼊最多;oracle数量基本差不多,稍微少⼀点;mysql不多

      爆数据的速度上:报错>联合查询>带外数据>布尔盲注>时间盲注

    • 产⽣注⼊的输⼊点

      输⼊点⽐上⾯相对⼜重要⼀些,因为输⼊点决定了我们能⽤什么样的Vector(攻击向量),以及是否需要绕过

      select $username$,password from $table$ where $username2$ = '$dada$' order by $username3$ desc limit $0$,1
      

四、特殊的sql注入

1.宽字节注入
  • 宽字节概念

    • 单字节字符集:所有的字符都使用一个字节来表示,比如 ASCII 编码(0-127)
    • 多字节字符集:在多字节字符集中,一部分字节用多个字节来表示,另一部分(可能没有)用单个字节来表示
  • 我们知道字节是计算机存储世界中最⼩的衡量单位,1Byte = 8bits。所以⼀个字节最⼤能够表示2^8=256个字符。

    对ascii编码⽽⾔,⼀个字符⽤⼀个字节就可以表示,所以ascii编码最多可以表示256个字符。

    对GBK编码⽽⾔,⼀个汉字字符需要⽤两个字节表示,所以gbk编码理论上最多可以表示256*256个字符。

  • 利用

    • 因为宽字符的存在,导致⼀些防注⼊的⽅式会被绕过,我们以代码的形式来讲解
    <?php
    	$db = init_db();
    	$db->query("set SET NAMES 'gbk'); //设置gbk字符集
    	$username = addslashes($_GET['username']); //input: glc' and 1=1#
    	$db->query("select * from table where username = '$username'");
    ?>
    
    • 我们来考虑下sql语句会是怎么样的呈现,输⼊glc’ and 1=1#,在这种场景下,我们没办法进⾏sql注⼊,因为我们的单引号被转义了,我们没办法侵⼊到控制流
    select * from table where username = 'glc\' and 1=1#'
    
    • 输⼊glc%df’ and 1=1#

      数据在存储的时候⼀定是以字节存储的,但是数据解读的时候,都是以字符的标准去解读的。所以,在连接数据库进⾏sql执⾏(执⾏就是⼀种对存储的解读)时,会按照字符集编码的规范去解读,所以遇到了\xDF\x5C的时候,会解读成⼀个字符,此时的数据流转过程为

      url输⼊-->php字符串变量-->addslashes-->sql语句-->数据库
      
      glc%DF' and 1=1# --> glc\xDF' and 1=1# --> fuckdada\xDF\' and 1=1# --> select * from table where username = 'glc運' and 1=1#'  //数据流成功⼊侵到控制流
      
  • 所以代码审计中审阅宽字节注⼊的⽅式,就是查看数据库连接⽂件(⼀般名字类似conn.php),检查其字符集类型是什么,如果不是utf-8(因为脚本的字符集默认是utf-8)就可能有注⼊产⽣

2.hql注入
  • Hibernate:⼀种ORM框架,⽤来映射与tables相关的类定义(代码),并包含⼀些⾼级特性,包括缓存以及继承,通常在Java与.NET中使⽤,但在Java⽣态系统中更受欢迎。近来似乎逐渐有被Mybatis取代的趋势。
  • 与Mybatis不同的是,Hibernate虽然也⽤了xml作为映射模型。但是他构成了⼀套⾃⼰的解析引擎和语法,也就是HQL
  • ⽤户的输⼊作为HQL的⼀部分,先经过Hibernate解析引擎,渲染成sql语句,然后再进⼊到数据库层进⾏查询
  • 利⽤
    • HQL注⼊的漏洞产⽣与SQL注⼊并⽆差异,也是直接通过拼接字符串导致的注⼊。即代码层的输⼊破坏了HQL层的结构,最终蔓延到SQL数据库层,改变了控制流。
    • 相对于SQL注⼊⽽⾔,HQL增加的挑战就是我们插⼊的数据,⾸先必须经过HQL渲染通过才能进⼊SQL数据库获取数据。相当于我们需要构造⼀条payload,既符合当前的HQL结构,HQL渲染后的sql⼜能正常的在数据库执⾏
    • ⼀般的利⽤⽅式是,不考虑sql能够完全执⾏成功,⽽是利⽤sql报错注⼊+框架开启报错,将有⽤的数据直接在错误回显中爆出来

五、预编译下的注入

  • 看个例子

    <?php
    	$username = $_GET['username'];
    	$db = "mysql:host=127.0.0.1;dbname=test;charset=gbk";
    	$dbname = "root";
    	$passwd = "root";
    	$conn = new PDO($dbs, $dbname, $passwd);
    	$conn->query('SET NAMES GBK');
    	$stmt = $conn->prepare("select * from table where username = :username");
    	$stmt->bindParam(":username",$username);
    	$stmt->execute();
    ?>
    

    在这种情况下,是⽆法解决宽字符注⼊的问题,因为

    $stmt = $conn->prepare("select * from table where username =:username");
    ==> $stmt->sql = "select * from table where username = '${addslashes($input)}'"
    
  • ⽆法预编译的输⼊点

    • like关键字

      我们知道sql语句的模糊查找⾥⾯⽤的关键字like,⽽like关键字默认是不会预编译的(如果使⽤Mybatis则是预编译报错)。数据库⽅给出的原因好像是like预编译会造成慢查询和DOS,所以只能⼿动去添加预编译

      <?php
      	$username = $_GET['username'];
      	$db = "mysql:host=127.0.0.1;dbname=test;";
      	$dbname = "root";
      	$passwd = "root";
      	$conn = new PDO($dbs, $dbname, $passwd);
      	$conn->query('SET NAMES GBK');
      	$stmt = $conn->prepare("select * from table where username like '%:username%'"); //不⽣效
      	$stmt = $conn->prepare("select * from table where username like concat('%',:username,'%'"); //⽣效
      	$stmt->bindParam(":username",$username);
      	$stmt->execute();
      ?>
      
    • 可能很多开发会遗漏这个点,导致存在注⼊。或者⼀些java的开发,Mybatis编译报错,然后他们⾃⼰去添加过滤(过滤没写好)或者不过滤(使⽤原⽣语句),导致GG。

    • 与之类似的还有IN关键字,该位置也默认不能预编译,需要在预编译语法中去for循环,有些程序员为了⽅便,可能也会在这边偷⼯减料

  • 不能加引号的关键字

    • 我们刚才分析了预编译模式下的宽字节注⼊,我们可以发现预编译+绑定变量的效果,有点类似于做了两个步骤

      $newInput = addslashes($input)   //内容转义
      sql = select * from table where column = '$newInput'   //强制⽤单引号包裹
      

      那么,结合我们刚才讲的,产⽣注⼊的输⼊点

      select $username$,password from $table$ where $username2$ = '$glc$' order by $username3$ $desc$ limit $0$,1
      
    • 是否有输入点是一定不能加单引号的呢,如果不能加单引号,那么就不能预编译,于是我们找到了

      $username$,$username2$,$table$,$username3$,$desc$,$0$
      
    • 这些地⽅都是不能加单引号的,总结就是表名、列名、limit子句、order by[desc/asc]

    • 跟刚才⼀样,可能很多开发会遗漏这个点,导致存在注⼊。或者⼀些java的开发,Mybatis编译报错,然后他们⾃⼰去添加过滤(过滤没写好)或者不过滤(使⽤原⽣语句),导致凉凉

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

果粒程1122

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值