Vulnerability-goapp-Go语言漏洞平台审计过程
February 16th 2020, 12:00:00 pm
平台的漏洞是比较偏基础的,很多内容都是简单傻瓜式的漏洞。但尽管如此,这个平台用来了解go语言的web流程还是可以的。
项目地址:Vulnerability-goapp
Ps:项目中的docker环境我搭不起来,总是报错。所以是直接把源码下载到本机windows环境下自己改了源码搭的。
熟悉架构
文件结构
一些比较重要的文件夹与文件:
- pkg 平台各功能的源码都在这个目录
- views html模板目录
- main.go 主程序
/login
页面:
一个页面的渲染过程
主程序先从pkg中引入各功能模块
在main函数中定义路由,可以从这里通过功能定位函数
以/login
页面为例,对应的函数是login.Login。跟踪到pkg/login/login.go
。然后来看下这个函数的整个过程是怎么样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| func Login(w http.ResponseWriter, r *http.Request) { fmt.Println("method ", r.Method) if r.Method == "GET" { if cookie.CheckSessionID(r) { http.Redirect(w, r, "/top", 302) } else { t, _ := template.ParseFiles("./views/public/login.gtpl") t.Execute(w, nil) } } else if r.Method == "POST" { r.ParseForm() if isZeroString(r.FormValue("mail")) && isZeroString(r.FormValue("passwd")) { fmt.Println("passwd", r.Form["passwd"]) fmt.Println("mail", r.Form["mail"]) mail := r.FormValue("mail") id := SearchID(mail) if id != 0 { passwd := r.FormValue("passwd") name := CheckPasswd(id, passwd) if name != "" { fmt.Println(name) t, _ := template.ParseFiles("./views/public/logined.gtpl") encodeMail := base64.StdEncoding.EncodeToString([]byte(mail)) fmt.Println(encodeMail) cookieSID := &http.Cookie{ Name: "SessionID", Value: encodeMail, } cookieUserName := &http.Cookie{ Name: "UserName", Value: name, } StoreSID(id, encodeMail) http.SetCookie(w, cookieUserName) http.SetCookie(w, cookieSID) p := Person{UserName: name} t.Execute(w, p) } else { fmt.Println(name) t, _ := template.ParseFiles("./views/public/error.gtpl") t.Execute(w, nil) } } else { t, _ := template.ParseFiles("./views/public/error.gtpl") t.Execute(w, nil) }
} else { fmt.Println("username or passwd are empty") outErrorPage(w) } } else { http.NotFound(w, nil) } }
|
如果登录成功,p := Person{UserName: name}
p传递到了模板中,再来看下/views/public/logined.gtpl
模板是怎么解析的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <title>Login successful!</title> </head> <link rel="stylesheet" href="./assets/css/style.css" type="text/css"> <body> <div class="center"> <p class="display-1 text-center">Login successful !!!!</p> <p class="display-1 text-center">Welcome , {{.UserName}} !!</p> <h2><a href="/top">Top Page</a></h2> </div> </body> </html>
|
可以看到,这里使用了模板来读取p中的UserName的值并将其替换。最终作为返回数据返回。所以在传递到模板之后只会进行替换,不会进行转义或其他过滤操作。
XSS
首页反射型XSS
漏洞点源码:main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| func sayYourName(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Println(r.Form) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println("r.Form", r.Form) fmt.Println("r.Form[name]", r.Form["name"]) var Name string for k, v := range r.Form { fmt.Println("key:", k) Name = strings.Join(v, ",") } fmt.Println(Name) fmt.Fprintf(w, Name) }
|
访问主页就是调用的sayYourName
,可以看到最后返回的是Name的内容,Name是在for循环当中,将最后一个参数赋值得到的。(如果参数有多个定义,则会使用”,”连接) 传递期间并没有进行过滤,所以造成xss漏洞。
POC:http://127.0.0.1/?test=%3Cscript%3Ealert(%22Threezh1%22)%3C/script%3E
注册处储存型XSS
注册处源码:pkg/register/register.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func RegisterUser(r *http.Request) bool { db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/vulnapp") if err != nil { log.Fatal(err) } age, err := strconv.Atoi(r.FormValue("age")) if err != nil { fmt.Println(err) return false } _, err = db.Exec("insert into user (name,mail,age,passwd) value(?,?,?,?)", r.FormValue("name"), r.FormValue("mail"), age, r.FormValue("passwd")) if err != nil { fmt.Println(err) return false } return true }
|
从源码中可以知道,插入到数据库的数据是直接从表单提交的数据中获取的。期间并没有经过过滤。虽然经过了一个换位符的处理,但是对xss的payload起不到过滤的效果。
注册时使用用户名:test<script>alert(1)</script>
登录后即可弹窗
后台Profile处多个储存型XSS
后台Profile处可以修改个人信息,Name、Address、Favorite Animal、Word三处内容都可以造成储存型XSS。
pkg/user/usermanager.go:
1 2 3 4 5 6 7 8 9 10
| func UpdateUserDetails(w http.ResponseWriter, r *http.Request) {
_, err = db.Exec("insert into vulnapp.userdetails (uid,userimage,address,animal,word) values (?,?,?,?,?)", uid, "noimage.png", address, animal, word) if err != nil { fmt.Printf("%+v\n", err) http.NotFound(w, nil) return } }
|
原因跟注册处的储存型XSS一样,都是没有经过严格的过滤而导致的。
复现:直接将内容修改为XSS Payload即可
后台TimeLine处储存型XSS漏洞
TimeLine是一个类似于留言板的地方,而传入留言板的内容也没有经过过滤直接储存到数据库内。最后渲染出来造成XSS漏洞。
pkg/post/post.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func ShowAddPostPage(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { } else if r.Method == "POST" { if cookie.CheckSessionID(r) { postText := r.FormValue("post") fmt.Println(reflect.TypeOf(postText)) StorePost(uid, postText) http.Redirect(w, r, "/post", 301) } } else { http.NotFound(w, nil) } }
|
跟踪StorePost()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func StorePost(uid int, postText string) { db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/vulnapp") if err != nil { fmt.Printf("%+v\n", err) return } defer db.Close()
_, err = db.Exec("insert into vulnapp.posts(uid,post) values (?,?)", uid, postText) if err != nil { fmt.Printf("%+v\n", err) return } }
|
原因跟前面的XSS一样,都是没有经过严格的过滤而导致的。
复现:在文本框中输入XSS Payload即可
SQL注入
在这个系统当中,大部分传递SQL语句是这样传递的:
1 2 3 4
| if err := db.QueryRow("select id from user where mail=?", mail).Scan(&userID); err != nil { fmt.Println("no set :", err) } log.Println(userID)
|
语句的”?”相当于一个占位符,将第二个参数mail替换过去。而替换过去的mail会被转义。相当于经过了一次addslashes()
处理。
比如我给mail定义:makefoxm@qq.com' and if(1=1,sleep(5),1)#
那最终会被执行的SQL语句如下:
select id from user where mail='makefoxm@qq.com\' and if(1=1,sleep(5),1)#'
所以,如果要去寻找SQL注入漏洞的话,就得去寻找没有过滤并且是字符串之间直接拼接的点。
后台TimeLine搜索处存在SQL注入漏洞
pkg/search/search.go:
1 2 3 4 5 6 7 8 9 10 11 12
| func SearchPosts(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { searchWord := r.FormValue("post") fmt.Println("value : ", searchWord) testStr := "mysql -h 127.0.0.1 -u root -proot -e 'select post,created_at from vulnapp.posts where post like \"%" + searchWord + "%\"'" fmt.Println(testStr) testres, err := exec.Command("sh", "-c", testStr).Output() } else { http.NotFound(w, nil) } }
|
从testStr赋值处可以看到,这里的SQL语句是直接用+
进行拼接的,没有使用”?”进行替换。所以这里能够直接构造Payload进行SQL注入。
复现:TimeLine搜索内容:123%" and if(sleep(5),1,1)#
页面延迟,构造其他语句就可以进一步进行利用。
任意文件上传
后台头像上传处存在任意文件上传漏洞
在后台Profile处可以上传头像,但是对文件名及文件内容没有经过过滤。导致任意任意文件上传。具体代码如下:
pkg/image/imageUploader.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func UploadImage(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { if cookie.CheckSessionID(r) { file, handler, err := r.FormFile("uploadfile") if err != nil { fmt.Printf("%+v\n", err) return } defer file.Close() f, err := os.OpenFile("./assets/img/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Printf("%+v\n", err) return } defer f.Close() io.Copy(f, file) UpdateDatabase(r, handler.Filename) http.Redirect(w, r, "/profile", 301) } } else { http.NotFound(w, nil) } }
|
漏洞复现:直接用Brupsuite抓包可以修改上传的地址。
问题来了,怎么进行Getshell呢?Go语言跟PHP不太一样,它没有类似一句话这样的“工具”。并且要通过路由定义才能够通过web访问到。我最初的想法是能不能覆盖一个路由中已有的函数文件,通过修改函数中的语句来达到命令执行的效果。但在参考文章中有一个的方式更加方便,就是通过修改crontabs定时任务来进行利用。如图:
(图片取自参考文章内)
这次搭建的题目环境是windows,配置linux环境太麻烦,就不复现了(怕了配置环境)。
命令执行
管理员后台处存在命令执行漏洞
首先来看pkg/admin/admin.go
中的ShowAdminPage函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| func ShowAdminPage(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { adminSID, err := r.Cookie("adminSID") if err != nil { fmt.Printf("%+v\n", err) } fmt.Println(adminSID.Value) adminUid, err := GetAdminSid(adminSID.Value) } else { http.NotFound(w, nil) } }
|
继续跟踪GetAdminSid:
1 2 3 4 5 6 7 8 9 10 11 12 13
| func GetAdminSid(adminSessionCookie string) (results string, err error) { commandLine := "mysql -h mysql -u root -prootwolf -e 'select adminsid from vulnapp.adminsessions where adminsessionid=\"" + adminSessionCookie + "\";'" res, err := exec.Command("sh", "-c", commandLine).Output() if err != nil { fmt.Println(err) } results = string(res) if results != "" { return results, nil } err = xerrors.New("recode was not set") return "", err }
|
可以看到,commandLine是会被传递到exec.Command命令当中去执行命令,而commandLine中的语句,是直接通过与adminSessionCookie进行拼接得到的,没有经过任何的过滤。所以这里造成了命令执行漏洞。
同样的问题,在admin/confirm.go的也是造成了命令执行漏洞。
CSRF漏洞
后台多处存在CSRF漏洞
先来看pkg./user/usermanager.go
中的ConfirmPasswdChange
函数
1 2 3 4 5 6 7 8 9 10
| func ConfirmPasswdChange(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { if cookie.CheckSessionID(r) { if r.Referer() == "http://127.0.0.1/profile/changepasswd" { } else { http.NotFound(w, nil) } }
|
可以看到,这里是限制了Referer只能为http://127.0.0.1/profile/changepasswd
所以这里是没有CSRF的,但是整个后台,除了修改密码处验证了Referer,其他修改内容功能的点都没有验证,因此都存在CSRF漏洞。比如Profie用户信息修改,TimeLine发送留言等。
比如TimeLine发送留言:
直接用Brupsuite构造CSRF的poc即可。
参考