这里是无意中在一道ctf中遇到了这道题目,认为有必要去审计记录一下各漏洞触发点,所以将其发表出来.
这里我们进入环境后:
成功找到了后台登录地址,那么现在我们就是要想办法去使用我们的admin用户登录了
那么这里我们下载好了源代码,那么我们进去看看:
这里给了我们一个sql文件,我们导入看看:
这里我们可以直接去我们的sql中来进行导入:
首先进入到我们想要导入的数据库里面,
然后我们使用这个命令导入:
source +sql文件的路径导入
导入成功后我们查看:
这里我们导入了这么多的数据,那么接下来我们去
查看system_user的账号和密码:
这里我们获取到了用户名和密码,那么我们把密码拿去md5解码:
admincccbbb123
文件上传漏洞
这里我们成功登录到了后台,那么接下来我们就去找我们的文件上传点:(这里是常规思路,一般进入后台后都会去找有无文件上传点这种功能块)
这里我们在这个地方:
找到了我们可以上传文件的类型,那么我们可以往里面增加php类型
在站点信息这里我们发现了文件上传的点:
那么我们随便上传一个图片抓包看看:
这里有这几个值,但是我们知道要实现文件上传,那么我们首先就是要知道路径
点击放行之后,我们确实看到了这个文件上传的目录:
(这里和我们后面分析的逻辑是一样的)
那么这里我们如何知道这几个值和我们的路径有什么关系呢,那么这里我们就要去审计我们的源代码了:
这里我们就需要去找到对应的源代码:
这里对应的就是我们的这个upstate函数:
那么首先我们先来看到这个函数,首先我们的变量$filename会由我们post的md5值和我们的filename值组成,这里我们来看看这个
join函数:
join() 函数返回由数组元素组合成的字符串。
这里会以join的第一个为分割符号分割每一个数组元素
那么我们再来看看这个str_split函数:
所以这里的这句话的意思就是:
假如这里我们的md5值是:
1dd114c26d2e32d9afee242d946cd61f
经过这句话:
join('/', str_split($post['md5'], 16))
之后就变成了:
1dd114c26d2e32d9/afee242d946cd61f
然后再和我们的pathinfo函数的值进行拼接,当这个函数的第二个参数为4的时候,表示的是截取.jpg文件后缀,所以这里我们的filename变为了:
1dd114c26d2e32d9/afee242d946cd61f.jpg
接下来是对我们请求参数的处理,这里我们知道上面我们的请求参数是local,那么我们去看看处理方式
会把我们上面生成的这个$filename和我们的session_id拼接在一起,然后再来一次md5值,这里我们去看看这个session_id()函数的作用:
session_id() 可以用来获取/设置 当前会话 ID。
而在这个时候我们看:
我们是没有当前会话的,因为没有phpsess_ID,所以这里默认为空
所以这里我们的这个token值其实是我们可以控制的,因为我们可以在post的时候修改md5值
然后会去调用到:
这个result函数:
在这里面似乎并没有什么其他的操作,那么现在在这个upstate的函数中我们能分析的已经分析完了,那么接下来我们就来到了另外一个函数upload:
这里我们首先我们的$file是再怎么来的,我们看看这里的file函数:
这里传入进来的name参数是字符串file,我们看到这个类是用来处理我们上传的文件的
这里处理逻辑看不懂,我们就先略过,那么我们再往下看:
这里的作用依旧是取出我们上传文件的后缀,那么继续往下:
这里会把我们刚才的post的md5以16个字符为一组分割为数组
然后再拼接起来,那么这里我们达到的效果其实和上面函数是一样的,那么如果正常情况下这里的$filename和上面那个函数的$filname是一样的
那么我们再往下看:
这里很明显是一些过滤,首先它要求我们的这个文件的后缀不能是php,或者不是storage_local_exts
里面的,这个是可以通过管理面板改配置来控制的。其次就是于要求我们post的token值要和我们刚才拼接而成的这个$filename的md5值相等(这里我们的session_id()默认为空)
如果这两个地方的验证都通过了,那么就会把我们的文件移动到这个:
/static/upload/$md5[0]/$md5[1]
这个目录下,这里的这个$md5[0]和$md5[1]就是上面根据我们md5值以16位的长度进行分割而形成的数组的值
最终就会上传文件成功,那么这里我们的目的就是去绕过第一个if,这里飞一个if说了我们的后缀不可以是php,那么我们可不可以在我们的最后的$md5[1]里面构造出一个.php呢?
那么这里我们先不管,我们先跟进我们的move函数中去:
首先这里我们的$savename变量是我们想要操作的,那么我们重点看这个变量,这里当刚刚传入的时候这个变量是:
$md5[1]也就是我们可以构造的这段值:afee242d946cd61f
前面是一些对图片的检测:
其中这个check函数,会对我们图片的一些基本信息进行检测:
也就是检测我们上传的是否是一张图片,一般这种类型的检测,如检测大小和Mime类型或者后缀等,我们可以使用这个图片头来进行一个绕过
那么我们接着往下看:
对于这里的path也就是我们第一个参数,也就是这里的
/static/upload/$md5[0]
rtrim() 函数移除字符串右侧的空白字符或其他预定义字符。
也就是这里是去除后边的第一个/符号,然后再拼接上一个/符号,总之最后会变成:
/static/upload/$md5[0]/
那么们看最后的这个参数,$save变量,这里会传入到我们的这个buildSaveName函数中去,那么我们跟进去看看:
因为这里我们的$savename不是true,所以这里我们会进入到最后一个if,这里我们看最后一个if判断就有问题,因为这里我们先来看这个strpos函数:
也就是说这里检测到要是没有出现.的话那么就在我们的$savename变量后边拼接上.上传文件后缀(.jpg),如果我们的这个变量中有.的话那么就直接return出去了,那么如果我们的文件名变为了:
xxxxxxxxxx.php
的话那么就直接把这个名字return出去了
这里我们构造我们的16位长度的值不就可以了吗?
所以我们可以这样构造:
下面是我们总的可以控制的md5值
1dd114c26d2e32d9afee242d946cd61f
那么我们改为:
1dd114c26d2e32d9afee242d946c.php
依旧要保持总的长度为32位,让后面的为16位,那么这里return出去后也就成了:
/static/upload/1dd114c26d2e32d9/afee242d946c.php
这里因为我们的这个$filename会变为:
/static/upload/1dd114c26d2e32d9/afee242d946c.php.jpg
所以这个if那儿进不去,会返回给我们上传失败,但是文件上传的部分我们在move函数中已经完成了,所以还是上传成功了
这里我们上传的时候还是先找一张普通的这个png图片,然后把我们的代码如<?php phpinfo()?>
放到后面去就可以了,或者直接使用图片马
这里我们还是选择制作一个图片马:
1.使用记事本打开图片
2.不管里面的内容有多少,只保留前面三行(因为jpg,png的头保存在前三行,若删除则无法被识别成图片文件)
3.之后我们保存下来
然后我们抓包:
j将上面的md5值修改为:
e52360073082563ea6d4a31029d7.php
(注意这里生成的md5值是后端自动生成的,然后我们再去获取这个值,所以当我们改了后在我们这个upstate函数中获取到的md5值就是我们修改后的md5值了)
然后我们先利用上面这段md5值去生成一个token:
<?php
$md5="e52360073082563ea6d4a31029d7.php";
$md5=str_split($md5,16);
$ext="jpg";
$filename = join('/', $md5) . ".{$ext}";
echo md5($filename);
这里的token值是:
00ee0c7f512728e8529cfd35d6f77ad4
然后我们放上去,抓包修改后放包,直到遇到在upload函数处理的时候再次准备修改数据包:
这个时候我们看到它生成的用于比对的token值果然改变了:
但是这里在upload函数中生成的用于对比的token为这一阶段:
而这里去获取的依旧是后端自动生成的:
所以我们同样需要去修改upload函数中用于生成的用来和前面进行对比token值的md5的值,修改为一样的:
然后我们发送:
这里显示的是上传文件失败,但是前面我们分析了为什么显示 失败但是我们还是上传上去了的原因
那么我们去访问到这个目录:
/static/upload/e52360073082563e/a6d4a31029d7.php
成功实现php文件上传,那么接下来我们去上传一个一句话木马:
到此我们文件上传的漏洞就利用成功了
未授权访问添加超级用户
那么在最上面,其实那个登录后台的地方还存在一个问题
也就是说当我们如果说我们是在真实环境里面,我们是没有这个sql用户表的,那么更不可能去拿着我们的什么md5加密后的东西拿去解密了,那么这里我们还是来到我们的这个后台登录界面抓包看看:
这里我们发现抓取的数据包中并没有什么异常
这个时候我们有两个思路,第一个思路是看能不能修改后台管理员的账号密码,第二个思路就是看能不能自己创建一个拥有权限的用户,然后登录
这里我们在这个目录下:
D:\phpstudy_pro\WWW\webfaka\web_faka\html\application\admin\controller\Index.php
发现了两个函数:
1.第一个是pass函数:
public function pass()
{
if (intval($this->request->request('id')) !== intval(session('user.id'))) {
$this->error('只能修改当前用户的密码!');
}
if ($this->request->isGet()) {
$this->assign('verify', true);
return $this->_form('SystemUser', 'user/pass');
}
$data = $this->request->post();
if ($data['password'] !== $data['repassword']) {
$this->error('两次输入的密码不一致,请重新输入!');
}
$user = Db::name('SystemUser')->where('id', session('user.id'))->find();
if (md5($data['oldpassword']) !== $user['password']) {
$this->error('旧密码验证失败,请重新输入!');
}
if (DataService::save('SystemUser', ['id' => session('user.id'), 'password' => md5($data['password'])])) {
$this->success('密码修改成功,下次请使用新密码登录!', '');
}
$this->error('密码修改失败,请稍候再试!');
}
这个函数对应有一个未授权就可以访问的路由:
/admin/index/pass
这里我们看到源代码发现要想修改我们的密码,我们必须还是要知道原来的密码,那么这里就行不通了
并且抓包也没有发现任何可更改数据
那么我们再往下看:
public function info()
{
if (intval($this->request->request('id')) === intval(session('user.id'))) {
return $this->_form('SystemUser', 'user/form');
}
$this->error('只能修改当前用户的资料!');
}
这里还有一个这个info函数,那么我们去访问对应的路由:
发现同样可以访问到,这里也是一个未授权就可以访问到的路由,那么我们看这个路由的作用是用来添加我们用户的,那么这里我们随便添加一个试试看:
这里我们打算去添加一个admin用户
发现这里不可以添加的原因是因为已经有了一个admin用户了,那么我们去添加一个其他用户看看:
成功添加了一位用户,那么我们去登录一下:
这里成功登录了上去,说明我们添加用户成功
但是这里我们可以看到该用户的功能特别的少,那么这里我们应该知道是我们的身份(权限)不对,那么这里我们继续去审计源代码:
这里我们跟进这个_form函数:
protected function _form($dbQuery = null, $tplFile = '', $pkField = '', $where = [], $extendData = [])
{
$db = is_null($dbQuery) ? Db::name($this->table) : (is_string($dbQuery) ? Db::name($dbQuery) : $dbQuery);
$pk = empty($pkField) ? ($db->getPk() ? $db->getPk() : 'id') : $pkField;
$pkValue = $this->request->request($pk, isset($where[$pk]) ? $where[$pk] : (isset($extendData[$pk]) ? $extendData[$pk] : null));
// 非POST请求, 获取数据并显示表单页面
if (!$this->request->isPost()) {
$vo = ($pkValue !== null) ? array_merge((array)$db->where($pk, $pkValue)->where($where)->find(), $extendData) : $extendData;
if (false !== $this->_callback('_form_filter', $vo)) {
empty($this->title) || $this->assign('title', $this->title);
return $this->fetch($tplFile, ['vo' => $vo]);
}
return $vo;
}
// POST请求, 数据自动存库
$data = array_merge($this->request->post(), $extendData);
if(isset($data['password'])){
if( !empty($data['password'])) {
$data['password'] = md5($data['password']);
}else{
unset($data['password']);
}
}
if (false !== $this->_callback('_form_filter', $data)) {
$result = DataService::save($db, $data, $pk, $where);
if (false !== $this->_callback('_form_result', $result)) {
if ($result !== false) {
$this->success('恭喜, 数据保存成功!', '');
}
$this->error('数据保存失败, 请稍候再试!');
}
}
}
前面没有什么特别的操作,主要是看到这里对我们含有密码的$data数组处理:
这里会将我们的data变量传入进我们的回调函数_form_filter
中,那么我们跟进这个_form_filter
函数看看:
这里我们全局搜索这个函数:
最终在这里找到了它:
public function _form_filter(&$data)
{
if ($this->request->isPost()) {
if (isset($data['authorize']) && is_array($data['authorize'])) {
$data['authorize'] = join(',', $data['authorize']);
}
if (isset($data['id'])) {
unset($data['username']);
} elseif (Db::name($this->table)->where(['username' => $data['username']])->count() > 0) {
$this->error('用户账号已经存在,请使用其它账号!');
}
} else {
$data['authorize'] = explode(',', isset($data['authorize']) ? $data['authorize'] : '');
$this->assign('authorizes', Db::name('SystemAuth')->where(['status' => '1'])->select());
}
}
我们发现这里在我们的$data数组中还有一个authorize的键值,而根据在这个键值的意思这个应该是权限的意思,那么这里我们去看到我们admin用户的该值是多少:
我们可以看到是3,那么这里默认是没有设置这个值的,那么它就是一个普通用户,而我们需要在post的时候自带这个值:
那么我们放包之后 :
注册成功,我们去登录:
成功登录该用户,那么这样我们admin权限用户就伪造成功了
任意文件下载:
其实在这个时候,本来是打算已经结束审计的了,但是在功能点处又看到了这样的字样:
那么我们知道,备份一般会存在一个下载备份文件的这么一个操作,我们点进去看,发现果然可以下载我们的备份文件
我们点击下载后抓包:
发现了一个我们熟悉的可能存在漏洞的形式:下载路径可控
那么这里我们尝试着去测试一下:
我们成功下载了下来,改为txt格式尝试:
果然读取成功了
到这里所发现的漏洞点已经全部审计完毕,这次审计也是一般的思路流程:
后台--->功能点---->漏洞点