Chybeta

GitStack <= 2.3.10 远程命令执行漏洞分析-【CVE-2018-5955】

GitStack <= 2.3.10 远程命令执行漏洞分析-【CVE-2018-5955】

GitStack

GitStack是一款win平台下的Git可视化平台。其最新版本2.3.10存在一个远程命令执行漏洞(CVE-2018-5955),对应下载地址: https://gitstack.com/download/

安装完成后,登陆入口在 http://192.168.248.130/registration/login/?next=/gitstack/ 。默认用户名/密码分别为: admin/admin

漏洞分析

一些“小”漏洞

views.py中的问题太多了,为后续的命令执行利用,这里仅列一些。目测开发者在开发的时候想这些接口开放着也没关系。。

用户相关rest_user

首先在app/rest/views.py中定义了rest_user方法:

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
@csrf_exempt
def rest_user(request):
try:
# create user
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
# get the username/password from the request
# check the username
matcher = re.compile("^[A-Za-z]\w{2,}$")
if matcher.match(username) is None:
raise Exception("Please enter an alphanumeric name without spaces")
if(username == ""):
raise Exception("Please enter a non empty name")
user = UserFactory.instantiate_user(username, password)
user.create()
return HttpResponse("User created")
# get retrieve_all the users
if request.method == 'GET':
# convert list of objects to list of strings
user_list_str = []
user_list_obj = UserFactory.instantiate_user('').retrieve_all()
for user in user_list_obj:
user_list_str.append(user.username)
json_reply = json.dumps(user_list_str)
return HttpResponse(json_reply)
# update the user
if request.method == 'PUT':
# retrieve the credentials from the json
credentials = json.loads(request.raw_post_data)
# create an instance of the user and update it
user = UserFactory.instantiate_user(credentials['username'], credentials['password'])
user.update()
return HttpResponse("User successfully updated")
except Exception as e:
return HttpResponseServerError(e)

在默认情况下:

  1. 使用GET方式可以直接查看GitStack仓库的用户列表,存在未授权访问信息泄露漏洞

  2. 通过POST方法,指定username和password可以直接添加仓库用户,存在任意用户添加漏洞:

  3. 通过PUT方法,以JSON格式即可重置任意用户密码:

project相关

任意创建repo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# create a repository
def rest_repository(request):
# Add new repository
if request.method == 'POST':
name=request.POST['name']
try:
# check the repo name
matcher = re.compile("^\w{1,}$")
if matcher.match(name) is None:
raise Exception("Please enter an alphanumeric name without spaces")
if(name == ""):
raise Exception("Please enter a non empty name")
# create the repo
repository = Repository(name)
repository.create()
....

直接POST一个name即可创建对应的project,不过在POST的时候需要带上CSRF_TOKEN

CSRF_TOKEN的获得如下,访问登陆页面,比如 http://192.168.248.130/registration/login/?next=/gitstack/ ,查看源代码:

任意repo添加user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@csrf_exempt
def rest_repo_user(request, repo_name, username):
repo = Repository(repo_name)
user = UserFactory.instantiate_user(username)
# Add user
if request.method == 'POST':
try:
# Get the repository and add the user
repo.add_user(user)
repo.add_user_read(user)
repo.add_user_write(user)
repo.save()
return HttpResponse("User " + username + " added to " + repo_name)
...

按照下面这个格式即可添加:

1
POST http://xx/rest/repository/项目名/user/用户名/

远程命令执行漏洞

默认情况下GitStack的Web Interface接口时开启的。访问http://xx/web/index.php也即访问gitphp目录下的index.php.

第 153 行进行了认证操作:

1
2
3
4
5
6
7
8
<?php
/*
* Authentification
*/
$auth = new GitPHP_Authentication();
$auth->authenticate();
...
?>

GitPHP_Authentication定义在gitphp/include/Authentication.class.php中:

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
52
53
54
55
56
57
58
59
60
61
62
63
class GitPHP_Authentication
{
....
// Authenticate the user
public function authenticate()
{
// Get the project name
if(isset($_GET['p'])){
//$this->project_name = substr($_GET['p'], 0, -1);
$this->project_name = $_GET['p'];
// Read the users of the project
$users = $this->readRepositoryReadUsers();
// check if the user everyone is in the list
if(in_array('everyone', $users))
{
// yes
return true; // the user do not need to be authenticated
}
else
{
// The user should be authenticated
// Ask for username/password
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Enter a username/password of a user which has the rights to access to this repository. ADMIN PASSWORD WON\'T WORK"');
header('HTTP/1.0 401 Unauthorized');
echo 'xxx省略';
exit;
} else {
// try to authenticate
$authenticated = false;
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// Check if the user is in the array of read users
if(in_array($username, $users)){
$authMethod = $this->getAuthMethod();
// authenticate with ldap or by file
if($authMethod == "file"){
$authenticated = $this->authenticateFile($username, $password);
} if($authMethod == "ldap") {
$authenticated = $this->authenticateLdap($username, $password);
}
if ($authenticated == false){
$this->denyAuthentication();
}
} else {
$this->denyAuthentication();
}
}
}
}
}

当访问index.php时指定了参数p,也即project_name,会通过$this->readRepositoryReadUsers()将该project对应的user提取出来。倘若该project并非公开,即everyone并不在$users中,则进入authenticated阶段。

可以看到,在这部分的认证中,采用了HTTP Basic Authentication的方式

根据php手册,当PHP以Apache模块方式运行时可以用 header()函数来向客户端浏览器发送认证请求信息。而当用户输入用户名和密码后,包含有URL的PHP脚本将会把变量PHP_AUTH_USER,PHP_AUTH_PWAUTH_TYPE分别被设定为用户名,密码和认证类型。也就是说,这里的usernamepassword即我们可控,且未加以过滤的变量:

1
2
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];

在确认输入的用户名($username)在project的用户列表后,开始进行真正的认证操作。首先是获取认证类型$authMethod = $this->getAuthMethod();

1
2
3
4
5
6
7
8
9
10
private function getAuthMethod(){
// Read the gitstack settings file
$settingsDir = GitPHP_Config::GetInstance()->GetValue('gitstacksettings', '');
// read the ini file
$ini_array = parse_ini_file($settingsDir, true, INI_SCANNER_RAW);
$authMethod = $ini_array['authentication']['authmethod'];
// should contain "ldap" or "file"
return $authMethod;
}

gitstacksettings的默认值在data/settings.ini中设定,其中:

1
2
3
[authentication]
authmethod = file
ldapprotocol =

也即在默认情况下采用的是file方式的认证方法,程序流程进入:

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
if($authMethod == "file"){
$authenticated = $this->authenticateFile($username, $password);
}
```
`authenticateFile`定义在`gitphp/include/Authentication.class.php`第182行:
```php
private function authenticateFile($username, $password){
$authenticated = false;
// Will contains username as key, salt and encrypted pass as value
$userInfos = Array();
// exec the open ssl command
$installDir = GitPHP_Config::GetInstance()->GetValue('gitstackinstalldir', '');
$lines = file($installDir . "/data/passwdfile");
// Fill the userInfos array
foreach($lines as $line)
{
。。。省略
}
// if the user exist in the array
if(array_key_exists($username, $userInfos)){
// run the openssl command to verify the password
$currentUser = $userInfos[$username];
$result = exec($installDir . '/apache/bin/openssl.exe passwd -apr1 -salt ' . $currentUser['salt'] . " " . $password);
// result = $apr1$v1Ds2Lf9$hNL6r81eGFXrUmh5wbQpn0
// split the result to get only the encrypted password part
$split = explode('$', $result);
$encryptedPassword = $split[3];
if($encryptedPassword == $currentUser['encryptedPass'])
$authenticated = true;
}
return $authenticated;
}

此处的流程就是将project的用户信息从/data/passwdfile读出,经过一定的处理,然后通过openssl来进行响应的验证。注意这里的代码:

1
$result = exec($installDir . '/apache/bin/openssl.exe passwd -apr1 -salt ' . $currentUser['salt'] . " " . $password);

我们传入的$password直接拼接到了语句中,然后exec执行,这里即存在命令执行漏洞,且由于并不需要认证成功。

Exploit

不过这里的任意命令执行漏洞有一些限制,它需要在进行HTTP Basic Authentication时在用户名处填入project的用户列表中的某一个,然后通过在密码处注入payload,才能到达exec处。因此结合前面第一部分的未授权访问/任意添加用户等等漏洞,可以梳理如下两种方法:

  1. 通过GET /rest/user获取到所有的用户列表,然后直接进行爆破,总有某些用户是属于选择的project的用户列表中的。脚本如下:

  2. 通过POST /rest/user添加用户x,接着创建repo,将用户x加入到repo中,然后基于用户x的认证来进行rce。第二种方法的脚本见 https://blogs.securiteam.com/index.php/archives/3557 ,不搬运了。

Refference

本文标题:GitStack <= 2.3.10 远程命令执行漏洞分析-【CVE-2018-5955】

文章作者:chybeta

发布时间:2018年03月30日 - 18:03

最后更新:2018年03月31日 - 08:03

原始链接:http://chybeta.github.io/2018/03/30/GitStack-2-3-10-远程命令执行漏洞分析-【CVE-2018-5955】/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!