# [网鼎杯 2020 半决赛] faka

给的源码代码审计

有个 sql 文件

image-20241115213030036

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

image-20241115213141457

导入登录 phpMyadmin 即可

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

image-20241115213248329

md5 解密得到密码:

admincccbbb123

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

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

image-20241115214304015

也是登录进来了

image-20241115214437909

文件上传

image-20241115214453875

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

image-20241115214605677

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

位于 /application/manage/controller/Backup.php 的 downloadBak 方法

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)

/**
 * 通用文件上传
 * @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=strsplit(md5 = str_split(this->request->post (‘md5’), 16);,filename 是这样拼接而来的:$filename = join (’/’, md5) . ".{ext}";,然后检测后缀,不能是 php,或者不是 storage_local_exts 里面的,这个是可以通过管理面板改配置来控制的。
接下来是 token 的验证:

// 文件上传 Token 验证
    if ($this->request->post('token') !== md5($filename . session_id())) {
        return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
    }
默认session_id()是空,所以这里的token也很容易构造出来。
接下来就是进入move()函数:
$file->move('static' . DS . 'upload' . DS . $md5[0], $md5[1], true)
/**
     * 移动文件
     * @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 () 函数中有是否是图片的检测,利用图片头绕过即可。
之后注意这里:

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

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

/**
     * 获取保存文件名
     * @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;
    }

这些代码起作用:

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

最终相当于写入的文件是 static/upload/md5[0]/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

这里

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

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

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

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

然后关键点在

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

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

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

我们来看看这个 buildsavename 函数

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] 这个是咱们可以控制的 而且看move函数后面是将这个作为文件名了的 那么我们将 md5 [1] 设置成 xxxx.php (要长与 16 位) 是不是已经成了!!!hh

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

image-20241115222851341

你别看它返回的是上传失败 其实已经上传成功了 路径就是 xxxxx/static/upload/md5[0]/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