# [网鼎杯 2020 半决赛] faka
给的源码代码审计
有个 sql 文件
在自己的本地部署一下小皮
导入登录 phpMyadmin 即可
在 system_user 的表里面有 admin 这个账号和密码
md5 解密得到密码:
admincccbbb123
第一个一般是解密不出来的
第二个方法就是:在 admin/Index/info 的网址里有(未授权访问:添加用户,但是没有 authorize 这个选项,如果直接创建就是一般用户,我们得是 root,所以得用这个 admin 的选项是 3)
也是登录进来了
文件上传
因为有源码,可以不用一个一个盲传
可以看见是 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 类用来处理上传的文件,注意这个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[1].$ext
因此 php 文件写不了,虽然可写入的其他后缀可控,但是没法写入.htaccess 之类的,因此也解析不了,正常是没法写马的,我把这些代码看了一遍后也是这么想的,所以我还是太菜了。
仔细想想,还是这里:
能不来 token 但是你那个什么上传文件包的都没有,逆天
以后再看看吧,现在估计是搞了了
再贴个 wp(出不来的):
这个系统的后台上传是有两个步骤首先会调用 admin 模块 plugin 控制器的 update 方法 通过 post 的 md5 和 filename 生成一个加密 token 这个 token 会用于后面的检验
对应源码:
再来看看第二步 当然就是上传文件拉 下面就是个平常的上传文件包
对应的源码:
这里
if ($this->request->post('token') !== md5($filename . session_id()))
这一个判断是可以绕过的 因为这里
$filename = join('/', $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] 设置成 xxxx.php (要长与 16 位) 是不是已经成了!!!hh
还需要注意一下 上传的时候 png 图片前面一部分的格式需要保留 因为有检测 php 代码丢后面就好 或者直接用图片马什么的【
你别看它返回的是上传失败 其实已经上传成功了 路径就是 xxxxx/static/upload/md5[1]
我刚开始百思不得其解为什么会上传失败 一切都这么的流畅… 最后看了几遍找不出错在哪,不得已自己在本地搭建了环境,然后实验,偶然间,去瞟了一眼上传目录,发现… 其实已经上传成功了… 应该自己去访问一下的 这个地方有点傻了
最终:
弄了个 phpinfo 上去
参考链接:
https://xz.aliyun.com/t/7838?u_atoken=04a843600b8c30a58b59f2cb26591b33&u_asig=ac11000117316796642016072e013e
https://blog.csdn.net/rfrder/article/details/115067196