Go代码审计—Gorm框架常见SQL注入场景

  • Comments Off on Go代码审计—Gorm框架常见SQL注入场景
  • 16 views
  • A+

一、关于GORM

Go有诸多的ORM框架,GORM是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。当然原生库、xorm等也是比较出名的,感兴趣的也可以看看这些,但使用场景最多的还属gorm。

Using the GORM Go module for Web APIs in Golang | JFrog GoCenter

Gorm功能概览

  • 全功能ORM(几乎)
  • 关联(包含一个,包含多个,属于,多对多,多种包含)
  • Callbacks(创建/保存/更新/删除/查找之前/之后)
  • 预加载(急加载)
  • 事务
  • 复合主键
  • SQL Builder
  • 自动迁移
  • 日志
  • 可扩展,编写基于GORM回调的插件
  • 每个功能都有测试
  • 开发人员友好

安装十分简单,只需要一条go get**命令即可。

go get -u github.com/jinzhu/gorm

下面是一个简单的demo,这里以mysql为例,方便快速了解上手。

go
 package main
 
 import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
  "log"
 )
 
 // UserInfo record model
 type UserInfo struct {
  Id int `gorm:"column:id"`
  Age int `gorm:"column:age"`
  Name string `gorm:"column:name"`
  Address string `gorm:"column:address"`
  Content string `gorm:"column:content"`
 }
 
 // TableName 设置User的表名为`userinfo`, 默认会是userinfos
 func (UserInfo) TableName() string {
  return "userinfo"
 }
 
 
 func main() {
  db, err := gorm.Open("mysql", "root:[email protected](127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
  log.Fatal(err)
  }
  defer db.Close()
  db.LogMode(true)
 
  u := new(UserInfo)
  uinfos := make([]*UserInfo, 1)
  db.Where("age = ?", 22).Find(u)
  log.Println(u)
 
  db.Where("age > ?", 10).Find(&uinfos)
  log.Println(uinfos)
 
 }

Gorm支持好多种数据库,下面是简单的连接例子:

要连接到数据库首先要导入驱动程序。例如

import _ "github.com/go-sql-driver/mysql"

为了方便记住导入路径,GORM包装了一些驱动。

import _ "github.com/jinzhu/gorm/dialects/mysql"
 // import _ "github.com/jinzhu/gorm/dialects/postgres"
 // import _ "github.com/jinzhu/gorm/dialects/sqlite"
 // import _ "github.com/jinzhu/gorm/dialects/mssql"

  • MySQL

import (
     "github.com/jinzhu/gorm"
     _ "github.com/jinzhu/gorm/dialects/mysql"
 )
 
 func main() {
   db, err := gorm.Open("mysql", "user:[email protected]/dbname?charset=utf8&parseTime=True&loc=Local")
   defer db.Close()
 }

  • PostgreSQL

import (
     "github.com/jinzhu/gorm"
     _ "github.com/jinzhu/gorm/dialects/postgres"
 )
 
 func main() {
   db, err := gorm.Open("postgres", "host=myhost user=gorm dbname=gorm sslmode=disable password=mypassword")
   defer db.Close()
 }

  • Sqlite3

import (
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
 )
 
 func main() {
  db, err := gorm.Open("sqlite3", "/tmp/gorm.db")
  defer db.Close()
 }

二、常见SQL注入场景

SQL注入主要是Web应用未对用户输入进行合法校验,最终用户输入走向了查询语句,导致攻击者能在Web应用预先写好的SQL语句中执行了额外的SQL语句,造成非授权的任意查询,进而导致数据库信息泄露,有时甚至可以反弹shell。

为了防止注入的存在,gorm框架贴心的对大部分场景都进行了预编译处理,但由于研发安全意识的缺乏以及框架本身的局限,基于gorm的Web应用中还是存在着大量的SQL注入问题。

2.1 配置/连接数据库

DB是数据库操作中最核心的结构,后续的CRUD操作基本上都基于该结构对象。

go
 type DB struct {
  *Config
  Error        error
  RowsAffected int64
  Statement    *Statement
  clone        int
 }

配置数据库

go
 func main(){
   db, err := gorm.Open("mysql", "user:[email protected]/dbname?charset=utf8&parseTime=True&loc=Local")
   if err != nil {
     panic("Connection error")
  }
   defer db.Close()
   // 后续的CRUD操作基于db
   // 比如db.Where(), db.Like(), db.Order()等                  
 }

2.2 审计

下面来介绍审计过程中常遇见的存在问题的场景,这也是我们最关心的部分。

首先明确一个问题,什么时候会存在SQL注入?当CRUD操作**直接拼接了用户输入*,并且未做有效过滤/防护时,就会存在注入。那么什么是拼接用户输入呢?—— +, fmt.Sprintf(), buf.WriteString()等字符串的连接,均可视为拼接***。

默认情况下,GORM对结构体、map结构的value在框架底层都进行了预编译,所以使用此类方式进行CRUD操作时是十分安全的,无须担心SQL注入问题,尤其结构体在增删改操作中使用的很多。

  • where查询
    该方法是很常见的条件查询,是后续许多查询的基础。

go
 func (s *DB) Where(query interface{}, args ...interface{}) *DB

wKg0C2GBQBaAJYnTAACbHCeSXtQ365.png

以下类型的代码存在注入:

go
 // name可控
 db.Where("name = '" + name + "'")
 
 sql = fmt.Sprintf("name = '%s'", name)
 db.Where(sql)

以下类型的代码则是安全的:

go
 // name可控,但使用了预编译,不存在注入问题
 db.Where("name = ?", name)

* map的key
GORM对map的value进行了预编译,但却没对key进行预编译,此时极其容易审到注入。如下代码就存在SQL注入:

wKg0C2GBQCADgqDAABrtEvSUL4053.png

go
 // name_key可控,key框架未做预编译,代码也未作处理,存在注入
 m1 := map[string]interface{} {
     name_key: "tom",
     age: 6,
 }
 
 db.Where(m1)
 
 // name_value可控,框架底层对value做了预编译,不存在注入
 m2 := map[string]interface{} {
     "name": name_value,
     "age": 6,
 }
 
 db.Where(m2)

那么GORM为何没有对key做处理呢?原因在于map的key等价于sql语句中的列名,众所周知列名是无法执行预编译操作的,审计时可以重点关注这部分。
* Raw查询

go
 // 使用原始的sql作为查询条件
 func (s *DB) Raw(sql string, values ...interface{}) *DB

Raw是GORM提议提供的中较为宽松的写法,能让研发自由的写原生SQL语句,例如类似desc tables的语句,显然无法通过使用Where()Order()等函数执行。由于其较高的自由度,该函数颇受欢迎,此外,许多开发者对该函数存在一定误解,并不知道该函数也能使用预编译,由此引入了诸多注入问题。

go
 // req用户可控,此类写法存在注入
 sql := fmt.Sprintf("select id, %s.age from users where name = %s order by 1 desc", req.u, req.name)
 db.Raw(sql)

* Exec查询

go
 func (s *DB) Exec(sql string, values ...interface{}) *DB

Exec和Raw函数情况类似,这里不再赘述。
* orderby查询
采用预编译执行SQL语句传入的参数不能作为SQL语句的一部分,那么OrderBy所代表的的列名、或者是后面跟随的ASC/DESC也无法进行预编译处理。由于框架缺乏对此种类型的安全处理,加之许多开发者并不清楚SQL注入,Order()就成了SQL注入的高发地带。

go
 func (s *DB) Order(value interface{}, reorder ...bool) *DB

req.loc用户可控,代码中未对loc做任何处理,存在注入风险

go
 db.Order(req.loc)

req.loc用户可控,代码中虽做了检查,但很简单就可以绕过,仍存在注入问题

go
 if strings.Contains(req.loc, "shanghai") {
     db.Order(req.loc)
 }

req.loc用户可控,代码中通过map做了白名单校验,无法绕过,不存在注入问题

go
 validateCols := map[string]bool{"col1": true, "col2":true}
 
 if _, ok := validateCols[req.loc]; !ok {
     fmt.Println("列名不合法")
     return
 }
 db.Order(req.loc)

* groupby查询
groupby和orderby的问题类似,这里不再赘述。
* like模糊查询
like一般是基于where的查询,许多开发者对此处存在误区,很喜欢下面这类写法。
req.music用户可控,直接拼接了用户输入,存在注入问题。

go
 cond := "music like %"+req.music+"%"
 db.Where(cond)

req.music用户可控,使用了预编译,不存在注入问题。

go
 db.Where("musick like %?%", req.music)

* in范围查询
in是基于where的查询,存在的问题和like类似。
req.name用户可控,直接拼接了用户输入,存在注入问题。

go
 cond := "name in (" + req.name1 + req.name2 + ")"
 db.Where(cond)

req.name用户可控,使用了预编译,不存在注入问题。

go
 db.Where("name in (?)", []string{req.name1, req.name2})

总而言之,会导致注入问题的,只有字符串类型,map结构的key;而结构体、map结构的value、数字类型等,均不会存在注入问题

三、防御建议

一个大的原则是,优先使用预编译,无法使用预编译的,使用白名单/转义操作进行防御。

  • 常规值的拼接
    对于除表/列名的常规值,使用参数化查询对语句进行预编译,可以完全防御SQL注入。
  • 表/列名的拼接
    对于表/列名,无法使用预编译的,则可使用白名单机制,或者对特殊字符进行转义来防御注入。

四、参考资料

相关推荐: PDF生成漏洞:从XSS到服务端任意文件读取

一、 前言 XSS是最为常见的Web漏洞之一,多年来连续入选OWASP TOP 10,相信大家都耳熟能详。 它是一种代码注入类的攻击,是一种客户端侧的攻击,攻击者通过在Web应用中注入恶意JavaScript代码,通过点击URL,最终在受害者浏览器端执行的一种…