免责申明
本文章仅用于信息安全防御技术分享,因用于其他用途而产生不良后果,作者不承担任何法律责任,请严格遵循中华人民共和国相关法律法规,禁止做一切违法犯罪行为。
一、前言
好久没有给大家更新了,这次记录一下相关的一个代码审计案例项目,这个项目是自己上周做的了,今天来大概复盘一下。
二、审计经过
当时客户说只要前台漏洞,并且基本上规定了只要前台RCE以及相关的前台注入等等,危害性较大的漏洞才可以交差,这里也是运气好水了一个。
废话不多说了,这里来看案例:
首先打开源码其实可以看到是一套基于ThinkPHP的代码:
大概点开lou一眼还是很有TP的味道的,然后自己进行查找,当时先看的登录点分析。
下面这个代码是案例相关代码,不想看直接跳过就行,下面定位有问题的点。
public function login()
{
if (empty($this->imageKey) || empty(Cache::get($this->imageKey))) {
return json(["status" => 1, "msg" => "缺少图片验证码信息"]);
}
$imageData = Cache::get($this->imageKey);
if ($imageData['past'] === 0) {
return json(["status" => 1, "msg" => "图片验证码未通过"]);
}
$request = $this->request;
$username = $request->param('username', '');
$ret = check_pwd_wrong_times($username);
if ($ret['err']) {
return json(["status" => 1, "msg" => $ret['msg'], 'state' => 0]);
}
$password = $request->param('password', '');
if (!$username) {
return json(["status" => 1, "msg" => "帐号不能为空"]);
}
if (!$password) {
return json(["status" => 1, "msg" => "密码不能为空"]);
}
$ip = $this->request->ip();
$cacheKey = 'login_attempts_num' . $ip . $password;
$lastAttemptTimeKey = 'login_attempts_' . $ip . $password;
// 获取已登录尝试次数
$attempts = Cache::get($cacheKey, 0);
// 设置限制的尝试次数
$maxAttempts = 3;
// 设置登录失败后的等待时间(单位:秒)
$waitTime = 60; // 1分钟
if ($attempts >= $maxAttempts) {
// 获取上一次登录尝试的时间
$lastAttemptTime = Cache::get($lastAttemptTimeKey, 0);
$currentTime = time();
$remainingTime = $lastAttemptTime + $waitTime - $currentTime;
if ($remainingTime > 0) {
return json(["status" => 1, "msg" => '登录失败次数过多,请稍后再试']);
}
}
$member = Member::where("mobile", $username)->find();
$memberlogs = new Memberlogs();
if (!$member) {
Cache::inc($cacheKey, 1);
Cache::set($lastAttemptTimeKey, time());
/**登录日志**/
$data['userid'] = 0;
$data['username'] = $username;
$data['memo'] = "尝试登录(" . $password . ")";
$data['status'] = 0;
$memberlogs->savelog($data);
return json(["status" => 1, "msg" => "账号或密码错误"]);
} else {
if ($member->state == '0') {
/**登录日志**/
$data['userid'] = $member->id;
$data['username'] = $username;
$data['memo'] = "帐号禁用中";
$data['status'] = 0;
$memberlogs->savelog($data);
return json(["status" => 1, "msg" => "帐号禁用中"]);
}
$mpassword = $member->password;
if ($mpassword == md5($password)) {
/**登录日志**/
$data['userid'] = $member->id;
$data['username'] = $username;
$data['memo'] = "登录成功";
$data['status'] = 1;
$memberlogs->savelog($data);
$token = self::createTokenByTel($member->id, $username);
Cache::set('login.data.' . $username, $token, 86400 * 15); //将此token保存15天存活期
$member->logintime = date("Y-m-d H:i:s");
$member->token = $token;
$member->save();
if ($this->imageKey) {
Cache::delete($this->imageKey);
}
$msg = '登录成功';
if (!$this->checkPassword($password)) {
$msg = '登录成功,密码过于简单请尽快重置';
}
Cache::delete($cacheKey);
Cache::delete($lastAttemptTimeKey);
return json(["status" => 0, "msg" => $msg . $request->param('client_id'), "xtoken" => $token, 'userid' => $member->id, 'picImg' => $member->picImg]);
} else {
Cache::inc($cacheKey, 1);
Cache::set($lastAttemptTimeKey, time());
/**登录日志**/
$data['userid'] = $member->id;
$data['username'] = $member->username;
$data['memo'] = "密码错误";
$data['status'] = 0;
$memberlogs->savelog($data);
return json(["status" => 1, "msg" => "账号或密码错误"]);
}
}
}
通过代码其实可以发现在登录成功之后调用了一个方法为createTokenByTel根据名字可以看到其实就是创建token的方法,我们跟入查看如何创建的。
还是很经典的,果然弱口令才是永远的0day!,其实我们通过这个伪造了jwt令牌之后就可以来进行尝试审计后台漏洞了,全局搜索上传方法。
下面这个代码是案例相关代码,不想看直接跳过就行,下面定位有问题的点。
public function multi($ci)
{
$pathArr = [];
// 获取表单上传文件 例如上传了001.jpg
$isFile = request()->file();
if (empty($isFile) || !isset($isFile[$ci['name']]) || empty($isFile[$ci['name']])) {
return Service::error('上传文件不能空');
}
$files = request()->file($ci['name']);
// 验证,移动到框架应用根目录/uploads/ 目录下
foreach ($files as $file) {
// 返回大小的具体信息
if ($file->getInfo('size') > $ci['size']) {
return Service::error(sprintf("单文件大小不能超出 %s MB", $ci['size'] / 1024 / 1024));
}
$info = $file->validate(
[
'size' => $ci['size'],
'ext' => $ci['ext'],
'type' => $ci['type'],
]
)
->rule('md5')
->move($ci['path']);
if ($info) {
$path = str_replace('\', '/', sprintf("////%s/%s", $ci['path'], $info->getSaveName()));
// 图片压缩
if ($file->getInfo('size') > 0.1 * 1024 * 1024) {
self::mkThumbnail($path, input('max_size') ?? $this->img_max_width);
}
// 成功上传后 获取上传信息
$pathArr [] = [
'path' => $path,
'md5' => $info->md5(),
'name' => $info->getInfo('name'),
'size' => $info->getInfo('size'),
];
} else {
// 上传失败获取错误信息
return Service::error($file->getError());
}
}
return Service::success($pathArr);
}
其实没有什么过滤的地方,所以我们要看哪里调用了方法multi方法。
其实还是很明显的,直接的一个文件上传,导致的任意文件上传,这里就不贴出来图片了
二、完结
完结啦!还是非常不错的,可以吃个KFC了
作者本人联系方式!
原文始发于微信公众号(进击安全):一次代码审计项目案例
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论