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

2020-02-21 约 845 字 预计阅读 4 分钟

声明:本文 【Vulnerability-goapp-Go语言漏洞平台审计过程】 由作者 Threezh1 于 2020-02-21 09:16:28 首发 先知社区 曾经 浏览数 173 次

感谢 Threezh1 的辛苦付出!

说在前面

平台的漏洞是比较偏基础的,很多内容都是简单傻瓜式的漏洞。但尽管如此,这个平台用来了解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 , 222.UserName}} !!</p>
    <h2><a href="/top">Top Page</a></h2>
</div>
</body>
</html>

可以看到,这里使用了222.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定义: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:

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即可。

参考

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now