Critical Vulnerabilities in Go Code Auditing (SQLi\CMD\SSRF)
A Go-based web vulnerability lab for reinforcing knowledge through hands-on practice, covering vulnerability code analysis and remediation for SQL injection, command execution, and SSRF in the Gin framework.
Read in Chinese→0x01 Critical Vulnerabilities in Go Code Auditing
A web vulnerability lab written in Go, created as a hands-on exercise to reinforce relevant knowledge.
https://github.com/godzeo/go-gin-vul
The entire web application uses the Gin framework and is adapted from go-gin-Example. There is no frontend framework, only Swagger, so just send requests directly.
0x02 Vulnerability Code Analysis and Remediation
0x01 SQLi
One of the most common coding issues in practice is SQL injection caused by concatenating column or table names after an ORDER BY clause.
Because table and column names cannot be used in parameterized queries, using an allowlist or escaping is recommended.
0x011 Common Incorrect Concatenation
This mainly involves joining strings using methods such as fmt.Sprintf() and buffer.WriteString().
Simply put, concatenating first and querying afterward is problematic.
db.Select(xxx).First(&user)
db.Where(fmt.Sprintf("name = '%s'", xxx)).Find(&user)
db.Raw("select name from " + xxx).First(&user)
db.Exec("select name from " + xxx).First(&user)
0x012 Common Cases Where Concatenation Is Required in Business Logic
For developers, SQL injection remediation mainly involves two scenarios:
- Concatenation of regular values;
- Concatenation of table or column names.
The reason is covered in a previous article. In short, using prepared statements for column names will cause them to become invalid.
Collapsible code and request packet
routers/api/unAuth/sql.go
db.Order(xxxx).First(&user)
For column names, the safest remediation is to use an allowlist.
validCols := map[string]bool{"col1": true, "col2":true}
if _, ok := validCols[xxxx]; !ok {
fmt.Println("illegal column")
return
}
db.Order(xxxx)
POST /sql/login HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 106
user=user&password=123456 AND EXTRACTVALUE(9509,CONCAT(0x5c,(SELECT user from blog.blog_login LIMIT 0,1)))
After allowlist remediation
POST /sql/loginSafe HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 106
user=user&password=123456 AND EXTRACTVALUE(9509,CONCAT(0x5c,(SELECT user from blog.blog_login LIMIT 0,1)))
Command Execution
When using system-call functionality such as exec,
an allowlist should be used to restrict the commands that can be executed.
Do not use bash or sh.
Collapsible code and request packet
Direct concatenation
routers/api/unAuth/cmd.go
ipaddr := c.PostForm("ip")
Command := fmt.Sprintf("ping -c 4 %s", ipaddr)
output, err := exec.Command("/bin/sh", "-c", Command).Output()
if err != nil {
fmt.Println(err)
return
}
c.JSON(200, gin.H{
"success": output,
})
POST /api/vul/cmd HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
ip=127.0.0.1 | echo zeo
// Parameter-binding concatenation
type MyMsg struct {
Domain string `json:"domain"`
Password string `json:"password"`
}
// ---> 声明结构体变量
var a MyMsg
// ---> 绑定数据
if err := c.ShouldBindJSON(&a); err != nil {
c.AbortWithStatusJSON(
http.StatusInternalServerError,
gin.H{"error": err.Error()})
return
}
output, _ := exec.Command("/bin/bash", "-c", "dig "+a.Domain).CombinedOutput() // python -c is also vulnerable
println(output)
c.JSON(200, gin.H{
"success": output,
})
POST /api/vul/cmd2 HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/json
Content-Length: 64
{
"domain":"baidu.com | whoami",
"password":"pssss"
}
Remediation:
Do not use "bash", "-c", although this only imposes restrictions and cannot fully remediate the issue.
It is recommended to hard-code the command directly or use an allowlist,
or use a pattern like this:
func safe(c *gin.Context) {
cmd := c.Query("cmd")
a := exec.Command("ls", cmd)
}
0x03 SSRF
SSRF attacks often lead to unauthorized operations or access to data within an organization.
In some cases, an SSRF vulnerability may allow an attacker to execute arbitrary commands.
Collapsible code and request packet
Common functions
http.Get(url)
http.Post(url, contentType, body)
http.Head(url)
http.PostForm(url, data)
http.NewRequest(method, url, body)
Full echo SSRF
routers/api/unAuth/ssrf.go
url := c.PostForm("q")
res, err := http.Get(url)
if err != nil {
fmt.Println(err)
fmt.Println("get image failed")
}
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
log.Fatalln(err)
}
log.Println(string(body))
c.JSON(200, gin.H{
"success": string(body),
})
POST /api/vul/ssrf HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 33
q=http://www.badiu.com/robots.txt
Simple allowlist remediation
func GetImageSafe(c *gin.Context) {
q := c.PostForm("q")
url := "https://test.image.com/path/?q="
res, err := http.Get(url + q)
if err != nil {
fmt.Println(err)
fmt.Println("get image failed")
}
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
log.Fatalln(err)
}
log.Println(string(body))
c.JSON(200, gin.H{
"success": string(body),
})
}
[GIN] 2022/10/22 - 22:44:28 | 500 | 946.54142ms | 127.0.0.1 | POST /api/safe/ssrf
Get "https://test.image.com/path/?q=@http://www.badiu.com/": x509: certificate is not valid for any names, but wanted to match test.image.com
get image failed
Of course, this remediation is overly simplistic. We will examine a more mature remediation approach later.