ShowDoc代码审计(失败分析)

前置知识

2020年ShowDoc修复了一个高危漏洞

复现过程可以看

可以看到的是,上传数据库的文件后缀为.<>php,这是绕过了黑名单的关键

image-20210512192708058

为什么要这个样子写呢?

image-20210512192845467

很明显的可以看到可以绕过黑名单的判断

那为什么<>不见了呢?

原因在于,这套代码基于ThinkPHP 3.2.3的内核

在底层的上传逻辑中,在生成文件名之前,存在一个过滤标签的操作

image-20210512193103788

那有没有其他办法绕过这里的黑名单呢?

完全可以~!

  • 第一个关注点在,程序验证后缀的文件只是通过editormd-image-file参数传入的
  • 第二个关注点,在调用上传方法的时候代码是$upload->upload(),这里为进行传参

那么在底层逻辑中

当传入的file参数为空的时候,是会重新获取参数的

image-20210512194506280

也就是说我们只需要传入任意非检测参数即可

比如传参file

image-20210512194551701

至于文件名字,暂不做讨论

image-20210512194704320

代码审计(失败)

前台的上传点,直接返回了false,并且限制了上传后缀

那么需要登陆的呢?

发现这里没有设置白名单

image-20210512194904688

根进Attachmentupload方法

没有办法,这里向底层的upload方法传了参数

而且接受文件的参数固定,无法更改editormd-image-file

image-20210512195141429

就是在这里没有关注uploadOneupload方法的区别,从一开始方向就错了

总结发现

  • 常规情况下无法继续上传了

  • 未发现直接调用底层upload方法而不进行传参的地方

  • 传进底层的是一个参数名为editormd-image-file$_FILES数组

  • 这套代码是ThinkPHP 3.2.3

抛出问题

  • ThinkPHP 3.2.35版本的对比,一个很容易被忽略的小知识点,就是3未设置error_reporting,这就导致,我们可以出现很多警告,只要不是致命错误即可
  • 接下来就是就是$_FILES数组了,可能很少人去搞他,多文件上传也是不同的名字即可,那我就想用同一个参数名字传多个文件可不可以做到呢?

对于上面的问题,其实有答案

我们平时用的$_FILES基本都是$_FILES['file']['name']来获取一个上传文件的文件名字

但它其实还有一个维度,那就是当file为数组的时候

写法为:$_FILES['file']['name'][$i]$i就是区分第几个文件

代码测试

测试代码

1
2
3
<?php
var_dump($_FILES['file']);
?>

当不传入数组的时候

image-20210512200953662

传入数组的时候

image-20210512201043524

可以看到这就是代表,第一个文件是1.php,第二个文件是2.php

为什么要提这个,因为ThinkPHP中对这种模式的传参,有着自己的处理逻辑,直接上代码

继续代码分析

upload.class.phpdealFiles方法

image-20210512201426405

正常情况下是没问题,但是当传参为一个具体的文件信息时会出问题

文件的处理逻辑

  • 比如.net$_FILES[0]['name'],代表上传数组的第一个文件名字
  • 但是php中上面说了,$_FILES['name'][0]才代表第一个文件名字

经过这段代码处理后

比如$_FILES有以下几个键名

  • name
  • type
  • tmp_name
  • error
  • size

直接就变成了五个文件

测试代码

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
<?php
$a = $_FILES['file'];
$b = dealFiles($a);
function dealFiles($files) {
$fileArray = array();
$n = 0;
foreach ($files as $key=>$file){
if(is_array($file['name'])) {
$keys = array_keys($file);
$count = count($file['name']);
for ($i=0; $i<$count; $i++) {
$fileArray[$n]['key'] = $key;
foreach ($keys as $_key){
$fileArray[$n][$_key] = $file[$_key][$i];
}
$n++;
}
}else{
$fileArray = $files;
break;
}
}
return $fileArray;
}
var_dump($b);

结果

image-20210512203824457

那么01的键值可控吗

自然可控

进行如下构造,发现我们可以在type文件出构造一个完整的文件数组

image-20210512204258711

为什么不在name中构造呢

因为filename中不能出现目录,只会取文件名

那么tmp_name的目录可控吗,其实默认配置下

  • WindowsC:\windows\php71E2.tmp,一般不会修改,我这是纯属第一次没看到缓存文件才改的
  • linux默认自查

那怎么操作呢

输入一个固定的缓存文件,总情况是确定的,肯定在上传文件的时候会出现一个和我们输入的缓存名一样的缓存

为什么非要这样呢,因为php底层对move_uploaded_file的缓存文件有签名认证的地方,所以必须是上传时生成的才行

测试代码

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
74
75
76
77
78
79
<?php
// var_dump($_FILES['file']['name']);
$a = $_FILES['file'];
if (strstr(strip_tags(strtolower($a['name'])), ".php") ) {
echo "aaaaa";
}else{
$files = dealFiles($a);
if(function_exists('finfo_open')){
$finfo = finfo_open ( FILEINFO_MIME_TYPE );
}
foreach ($files as $key => $file) {
$file['name'] = strip_tags($file['name']);
if(!isset($file['key'])) $file['key'] = $key;
/* 通过扩展获取文件类型,可解决FLASH上传$FILES数组返回文件类型错误的问题 */
if(isset($finfo)){
$file['type'] = finfo_file ( $finfo , $file['tmp_name'] );
}

/* 获取上传文件后缀,允许上传无后缀文件 */
$file['ext'] = pathinfo($file['name'], PATHINFO_EXTENSION);

/* 文件上传检测 */
if (!check($file)){
continue;
}

/* 获取文件hash */
if(1){
$file['md5'] = md5_file($file['tmp_name']);
$file['sha1'] = sha1_file($file['tmp_name']);
}
move_uploaded_file($file['tmp_name'],
"upload/1.txt");

/* 检测并创建子目录 */

}
// var_dump($b);
}
function dealFiles($files) {
$fileArray = array();
$n = 0;
foreach ($files as $key=>$file){
if(is_array($file['name'])) {
$keys = array_keys($file);
$count = count($file['name']);
for ($i=0; $i<$count; $i++) {
$fileArray[$n]['key'] = $key;
foreach ($keys as $_key){
$fileArray[$n][$_key] = $file[$_key][$i];
}
$n++;
}
}else{
$fileArray = $files;
break;
}
}
return $fileArray;
}
function check($file) {
/* 文件上传失败,捕获错误代码 */
if ($file['error']) {
return false;
}

/* 无效上传 */
if (empty($file['name'])){
$error = '未知上传错误!';
}

/* 检查是否合法上传 */
if (!is_uploaded_file($file['tmp_name'])) {
$error = '非法上传文件!';
return false;
}
return true;
}
?>

运气有点好

瞬间跑出来了

image-20210512205011557

然后激动的去测试,发现了一点

uploadOne调用upload的时候,又加了一层array

就是说不能直接调用upload

$upload->upload($_FILES['file'])

这种在正常情况下,也不能上传,执着了执着了

失败了,暂时这样吧

总结

写给自己看

这就像极了小时候的考试不审题

出发点的角度错了,终点的偏差会更加的大

当然也不能全然说一点收获没有

比如通过upload方法上传的文件,黑名单是限制不住的,可以通过多文件来绕过