Vulnerability-goapp-Go语言漏洞平台审计过程

平台的漏洞是比较偏基础的,很多内容都是简单傻瓜式的漏洞。但尽管如此,这个平台用来了解go语言的web流程还是可以的。

项目地址:Vulnerability-goapp

Ps:项目中的docker环境我搭不起来,总是报错。所以是直接把源码下载到本机windows环境下自己改了源码搭的。

熟悉架构

文件结构

一些比较重要的文件夹与文件:

  • pkg 平台各功能的源码都在这个目录
  • views html模板目录
  • main.go 主程序

/login页面:

04.png

一个页面的渲染过程

主程序先从pkg中引入各功能模块

02.png

在main函数中定义路由,可以从这里通过功能定位函数

03.png

/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) { // 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模板是怎么解析的:

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>
<!-- 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>

可以看到,这里使用了模板来读取p中的UserName的值并将其替换。最终作为返回数据返回。所以在传递到模板之后只会进行替换,不会进行转义或其他过滤操作。

XSS

首页反射型XSS

漏洞点源码:main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

注册处储存型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")) // value值都是从FormValue当中获取的
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定时任务来进行利用。如图:

images

(图片取自参考文章内)

这次搭建的题目环境是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") // 通过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:

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
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发送留言:

01.png

直接用Brupsuite构造CSRF的poc即可。

参考

CORS原理及利用整理 Hacktm中一道Node.js题分析(Draw with us)
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×