Vulnerability-goapp-Go语言漏洞平台审计过程
2020-02-21 10:16:28 Author: xz.aliyun.com(查看原文) 阅读量:414 收藏

说在前面

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

项目地址:Vulnerability-goapp

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

熟悉架构

文件结构

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

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

/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的值并将其替换。最终作为返回数据返回。所以在传递到模板之后只会进行替换,不会进行转义或其他过滤操作。

XSS

首页反射型XSS

漏洞点源码: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

注册处储存型XSS

注册处源码: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处多个储存型XSS

后台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漏洞

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注入

在这个系统当中,大部分传递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注入漏洞的话,就得去寻找没有过滤并且是字符串之间直接拼接的点。

后台TimeLine搜索处存在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的也是造成了命令执行漏洞。

CSRF漏洞

后台多处存在CSRF漏洞

先来看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即可。

参考


文章来源: http://xz.aliyun.com/t/7243
如有侵权请联系:admin#unsafe.sh