请问,GetAdminSid的命令注入应该怎么构造,我尝试了并没有执行啊。。。。。
说在前面
平台的漏洞是比较偏基础的,很多内容都是简单傻瓜式的漏洞。但尽管如此,这个平台用来了解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定义: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即可。