[网鼎杯2020半决赛]faka

给的源码代码审计

有个sql文件

image-20241115213030036

在自己的本地部署一下小皮

image-20241115213141457

导入登录phpMyadmin即可

在system_user的表里面有admin这个账号和密码

image-20241115213248329

md5解密得到密码:

1
admincccbbb123

第一个一般是解密不出来的

第二个方法就是:在admin/Index/info的网址里有(未授权访问:添加用户,但是没有authorize这个选项,如果直接创建就是一般用户,我们得是root,所以得用这个admin的选项是3)

image-20241115214304015

也是登录进来了

image-20241115214437909

文件上传

image-20241115214453875

因为有源码,可以不用一个一个盲传

image-20241115214605677

可以看见是tp5.0版本,有一个任意文件读取的漏洞

位于/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
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;
}
}

这个可以目录穿梭,可以直接../../../../flag.txt获取flag

然后就是代码审计加文件上传了

看一下upload方法(位于application/admin/controller/Plugs.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
/**
* 通用文件上传
* @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
6
    // 文件上传Token验证
if ($this->request->post('token') !== md5($filename . session_id())) {
return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
}
默认session_id()是空,所以这里的token也很容易构造出来。
接下来就是进入move()函数:
1
$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
/**
* 移动文件
* @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
$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
if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}

return $savename;

最终相当于写入的文件是static/upload/$md5[0]/$md5[1].$ext
因此php文件写不了,虽然可写入的其他后缀可控,但是没法写入.htaccess之类的,因此也解析不了,正常是没法写马的,我把这些代码看了一遍后也是这么想的,所以我还是太菜了。

仔细想想,还是这里:

image-20241115222520140

能不来token但是你那个什么上传文件包的都没有,逆天

以后再看看吧,现在估计是搞了了

再贴个wp(出不来的):

这个系统的后台上传是有两个步骤首先会调用 admin模块plugin控制器的update方法 通过 post的md5 和 filename 生成一个加密token 这个token会用于后面的检验

image-20241115222720121

对应源码:

image-20241115222757062

image-20241115222805351

再来看看第二步 当然就是上传文件拉 下面就是个平常的上传文件包

image-20241115222819370

对应的源码:

image-20241115222835096

这里

1
if ($this->request->post('token') !== md5($filename . session_id()))

这一个判断是可以绕过的 因为这里

1
$filename = join('/', $md5) . ".{$ext}";

而 $md5是可以控制的 ext 也是可以控制的 所以$filename可以控制 而且 post 的token也可以控制 这样当然可以绕过 具体的生成方法就是利用上传文件的第一步 自己可以随意构造post的md5值 并且没有检验针对post的md5参数 这是getshell的背景之一

然后关键点在

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

这个move函数里 跟进这个move函数 在这个move函数里有一个关键的调用

1
2
$saveName = $this->buildSaveName($savename);
$filename = $path . $saveName;

我们来看看这个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
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;
}

兄弟萌 看见没 hh 关键点就在 最后一个if判断上 判断 $savename里是否有. 有的话就会直接 return $savename 那么这个savename是什么呢 看前面的调用发现 这个savename就是 调用move函数的第二个参数 也就是 $md5[1] 这个是咱们可以控制的 而且看move函数后面是将这个作为文件名了的 那么我们将$md5[1]设置成xxxx.php(要长与16位) 是不是已经成了!!! hh

还需要注意一下 上传的时候 png图片前面一部分的格式需要保留 因为有检测 php代码丢后面就好 或者直接用图片马什么的【

image-20241115222851341

你别看它返回的是上传失败 其实已经上传成功了 路径就是 xxxxx/static/upload/$md5[0]/$md5[1]

我刚开始百思不得其解为什么会上传失败 一切都这么的流畅…. 最后看了几遍找不出错在哪,不得已自己在本地搭建了环境,然后实验,偶然间,去瞟了一眼上传目录,发现……其实已经上传成功了….. 应该自己去访问一下的 这个地方有点傻了

最终

弄了个phpinfo上去

image-20241115222903472

参考链接:

https://xz.aliyun.com/t/7838?u_atoken=04a843600b8c30a58b59f2cb26591b33&u_asig=ac11000117316796642016072e013e

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