平台的漏洞是比较偏基础的,很多内容都是简单傻瓜式的漏洞。但尽管如此,这个平台用来了解go语言的web流程还是可以的。
项目地址:Vulnerability-goapp
Ps:项目中的docker环境我搭不起来,总是报错。所以是直接把源码下载到本机windows环境下自己改了源码搭的。
一些比较重要的文件夹与文件:
/login
页面:
主程序先从pkg中引入各功能模块
在main函数中定义路由,可以从这里通过功能定位函数
以/login
页面为例,对应的函数是login.Login。跟踪到pkg/login/login.go
。然后来看下这个函数的整个过程是怎么样的。
func Login(w http.ResponseWriter, r *http.Request) { // r为请求对象,w为返回对象 fmt.Println("method ", r.Method) // 通过r.Method获取请求的方式 if r.Method == "GET" { if cookie.CheckSessionID(r) { // 通过CheckSessionID函数检查是否登录 http.Redirect(w, r, "/top", 302) // 登录了就直接跳转到top } else { t, _ := template.ParseFiles("./views/public/login.gtpl") // 读入模板文件 t.Execute(w, nil) // 模板解析并返回 } } else if r.Method == "POST" { r.ParseForm() // 解析获取到的数据,GET/POST解析都要有这个语句才能使用r.Form[] if isZeroString(r.FormValue("mail")) && isZeroString(r.FormValue("passwd")) { fmt.Println("passwd", r.Form["passwd"]) fmt.Println("mail", r.Form["mail"]) // r.FormValue和r.Form的区别是前者只获取同名的第一个数据值,后者会返回一个slice(数组形式) mail := r.FormValue("mail") id := SearchID(mail) // 通过邮箱获取一个用户id if id != 0 { passwd := r.FormValue("passwd") name := CheckPasswd(id, passwd) // 验证密码 if name != "" { // 如果登录成功 fmt.Println(name) t, _ := template.ParseFiles("./views/public/logined.gtpl") // 读入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) // 以上部分是设置Cookies p := Person{UserName: name} // 这里定义了p,传递到模板中进行解析 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
模板是怎么解析的:
<!doctype html> <html lang="ja"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <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>
可以看到,这里使用了{{.UserName}}
来读取p中的UserName的值并将其替换。最终作为返回数据返回。所以在传递到模板之后只会进行替换,不会进行转义或其他过滤操作。
漏洞点源码:main.go
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 { // 循环获取GET与POST参数与参数值 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
注册处源码:pkg/register/register.go
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")) // value值都是从FormValue当中获取的 if err != nil { fmt.Println(err) return false } return true }
从源码中可以知道,插入到数据库的数据是直接从表单提交的数据中获取的。期间并没有经过过滤。虽然经过了一个换位符的处理,但是对xss的payload起不到过滤的效果。
注册时使用用户名:test<script>alert(1)</script>
登录后即可弹窗
后台Profile处可以修改个人信息,Name、Address、Favorite Animal、Word三处内容都可以造成储存型XSS。
pkg/user/usermanager.go:
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漏洞。
pkg/post/post.go:
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()
:
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语句是这样传递的:
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定义:[email protected]' and if(1=1,sleep(5),1)#
那最终会被执行的SQL语句如下:
select id from user where mail='[email protected]\' and if(1=1,sleep(5),1)#'
所以,如果要去寻找SQL注入漏洞的话,就得去寻找没有过滤并且是字符串之间直接拼接的点。
pkg/search/search.go:
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
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函数
func ShowAdminPage(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { adminSID, err := r.Cookie("adminSID") // 通过Cookie获取adminSID if err != nil { fmt.Printf("%+v\n", err) } fmt.Println(adminSID.Value) adminUid, err := GetAdminSid(adminSID.Value) // 调用了GetAdminSid // 部分源码经过省略 } else { http.NotFound(w, nil) } }
继续跟踪GetAdminSid:
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的也是造成了命令执行漏洞。
先来看pkg./user/usermanager.go
中的ConfirmPasswdChange
函数
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即可。