前言
今天看到php的代码审计,忽然兴致使然,想从新学习一遍php的mvc代码审计,审计的是lmxcms,适合新手,采用的mvc架构
前台SQL注入(留言板)
位于留言板处,index.php?m=Book&a=index,对应的是Book控制器的index方法,寻找到Book控制器的index方法
public function index(){
if(isset($_POST['setbook'])){//提交留言
$data = $this->checkData();
if($this->bookModel->add($data)){
$this->setBookTime(); //存储提交时间
rewrite::succ($this->l['book_ok']);
}else{
rewrite::error($this->l['book_error']);
}
}
//判断是否调用留言数据
if($GLOBALS['public']['isbookdata']){
//判断是否只调用审核
$where = '';
if($GLOBALS['public']['bookDisplay']) $where = 'ischeck=1';
$count = $this->bookModel->count($where);
$page = new page($count,$GLOBALS['public']['booknum']);
$data = $this->bookModel->getData($page->returnLimit(),$where);
$this->smarty->assign('list',$data);
$this->smarty->assign('num',$count);
$this->smarty->assign('page',$page->html());
}
$this->smarty->display('book/index.html');
}
首先需要传入setbook参数,主要是看$data = $this->checkData();
这个函数
private function checkData(){
$arr['name'] = '';
$arr['content'] = '';
$arr['mail'] = '';
$arr['tel'] = '';
$arr['ip'] = getip();
//验证短时间内过多留言
if($this->bookModel->is_ip($arr['ip'],$this->config['book_out_time']) >= $this->config['book_out_time_num']){
rewrite::error($this->l['book_outtime']);
}
$this->bookTime(); //验证提交间隔时间
$_POST = filter_strs($_POST);
$data = p(1,1,1); //验证前台数据 转义并且过滤非法字符
$data = array_merge($arr,$data); //合并arr和data数组
if(!$data['name']) rewrite::js_back($this->l['book_name_must']);
if(!$data['content']) rewrite::js_back($this->l['book_content_must']);
//过滤html代码 预防XSS
foreach($data as $k => $v){
$data[$k] = string::delHtml($v);
}
unset($data['setbook']);
return $data;
}
$_POST用filter_strs方法进行处理,然后把p方法的值赋给$data,并且合并arr和data数组。先来看filter_strs方法
function filter_strs($data){
if(!$data) return $data;
if(is_array($data)){
foreach($data as &$v){
$v = filter_strs($v);
}
}else{
$data = urldecode($data); //url解码
$data = strip_tags($data); //去除html标签和php标记
$data = str_replace('%','',$data); //%替换为空
}
return $data;
}
判断
function p($type=1,$pe=false,$sql=false,$mysql=false){
if($type == 1){
$data = $_POST;
}else if($type == 2){
$data = $_GET;
}else{
$data = $type;
}
if($sql) filter_sql($data);
if($mysql) mysql_retain($data);
foreach($data as $k => $v){
if(is_array($v)){
$newdata[$k] = p($v,$pe,$sql,$mysql);
}else{
if($pe){
$newdata[$k] = string::addslashes($v);
}else{
$newdata[$k] = trim($v);
}
}
}
return $newdata;
}
我们调用的时候是传入三个1,对应的是post方法,然后经过filter_sql方法处理,并且遍历了
data,对其值进行了addslashes处理,但是没有对他的键进行处理。先看filter_sql方法function filter_sql(array $data){
foreach($data as $v){
if(is_array($v)){
filter_sql($v);
}else{
//转换小写
$v = strtolower($v);
if(preg_match('/count|create|delete|select|update|use|drop|insert|info|from/',$v)){
rewrite::js_back('【'.$v.'】数据非法');
exit();
}
}
}
}
过滤了一些关键字,并且结合前面说的addslashes方法,预防了我们的sql注入,但是遗漏的地方就是没有对键名进行过滤。看完了过滤,看看他具体怎么调用。
获取到了
public function add($data){
$data['time'] = time();
return parent::addModel($data);
}
继续往下跟进
protected function addModel($data){
return parent::addDB($this->tab[0],$data);
}
继续跟进
protected function addDB($tab,$data){
foreach($data as $key=>$v){
$field[]=$key;
$value[]="'$v'";
}
$field = implode(',',$field); //把数组以,间隔分成字符串
$value = implode(",",$value);
$sql="INSERT INTO ".DB_PRE."$tab($field) VALUES($value)";
echo $sql;
$this->query($sql);
return mysql_insert_id();
}
这里只对值进行了单引号的包裹,上面的echo
sql是我加入的,为了输出掉哟的语句方便测试现在看是正常的,但是记不记得之前是有把arr和data数组合并的一个操作?我们可以尝试一下传入其他参数看看
提示报错没有a这个参数,我们可以看一下数据表里具体有什么字段
这里的ischeck是是否显示,我们直接选1,不然前台看不到,然后直接构造payload即可
前台SQL注入(搜索框)
抓了一个搜索框的包,index.php?m=Search&a=index&classid=5&tem=index&field=title&keywords=1 ,应该是在Search控制器下的index方法
这里数据是在check里进行验证的,直接跟进check方法
private function check(){
//获取get数据
$_GET = filter_strs($_GET);
$data = p(2,1,1);
$this->param['keywords'] = string::delHtml($data['keywords']);
if(!$this->param['keywords'] && $this->config['search_isnull']){
rewrite::error($this->l['search_is_keywords']);
}
$this->param['classid'] = (int)$data['classid'];
$this->param['mid'] = (int)$data['mid'];
if(!$this->param['classid'] && !$this->param['mid']) rewrite::error($this->l['search_is_param']);
if($this->param['classid'] && !isset($GLOBALS['allclass'][$this->param['classid']])){
rewrite::error($this->l['search_is_classid']);
}
if($this->param['mid'] && !isset($GLOBALS['allmodule'][$this->param['mid']])){
rewrite::error($this->l['search_is_mid']);
}
$this->param['tem'] = $data['tem'];
$this->param['field'] = $data['field'];
$this->param['time'] = $data['time'] ? $data['time'] : $this->config['search_time'];
$this->param['tuijian'] = $data['tuijian'];
$this->param['remen'] = $data['remen'];
}
对get获取的数据进行了一系列操作,filter_strs具体的代码前面放了,这就不放了,然后依旧是p,防止了sql注入,必须要传入keywords,除了keywords之外,其他有几个参数是可以加的。知道了获取的参数以及过滤的方法之后,来看程序调用的过程
getSerachField方法如下,主要是初始化了一些参数的值,来看下一个方法
public function getSerachField($arr){
$arr['tem'] = $arr['tem'] ? $arr['tem'] : 'index';
$arr['ischild'] = $arr['ischild'] ? true : false;
$arr['field'] = $arr['field'] ? $arr['field'] : 'title';
if($arr['time'])$arr['time'] = time() - $arr['time'] * 24 * 3600;
return $arr;
}
searchCoutn方法如下
public function searchCoutn($searchInfo){
$param = $this->sqlStr($searchInfo);
$param['force'] = 'title';
return parent::countModel($param);
}
跟进countModel
protected function countModel($param=array()){
return parent::countDB($this->tab['0'],$param);
}
继续跟进
protected function countDB($tab,$param){
$We = $this->where($param);
$sql="SELECT count(1) FROM ".DB_PRE."$tab $We";
$result=$this->query($sql);
$data = mysql_fetch_row($result);
$this->result($result);
return $data['0'];
}
这里还是老方法,继续把他sql语句输出出来看看,当我们get传入如下参数时
index.php?m=Search&a=index&classid=5&tem=index&field=title&keywords=1&tuijian=2
sql语句如下
SELECT count(1) FROM lmx_product_data WHERE time > 1619446530 AND tuijian=2 AND classid in(11,12,13,14,5) AND (title like '%1%') ORDER BY id desc
构造payload如下
这里奇怪的是我sleep(1)是十秒,不应该是1秒吗,0.1的话他就变成1秒了,很奇怪
后台任意文件删除
如下位置抓包
POST /admin.php?m=File&a=delete HTTP/1.1
Host: lmxcms.com
Content-Length: 149
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://lmxcms.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://lmxcms.com/admin.php?m=File&a=imageMain&type=0
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_590a74fa0c262c903817f1bb7292db93=1650975259; Hm_lpvt_590a74fa0c262c903817f1bb7292db93=1650975259; PHPSESSID=jt97lo3n30a5fpehtj8upavr00; _dd_s=logs=1&id=72c3c0b9-9826-4e49-8501-83a901c685f7&created=1650977821212&expire=1650984809538
Connection: close
type=0&delImages=%E5%88%A0%E9%99%A4%E9%80%89%E4%B8%AD%E5%9B%BE%E7%89%87&fid%5B%5D=2%23%23%23%23%23%2Ffile%2Fslide%2F20140827%2F201408271523025580.jpg
位于File控制器的delete方法
public function delete(){
if(!$_POST['fid']) rewrite::js_back('请选择要删除的文件');
$this->fileModel->delete($_POST);
addlog('删除文件、图片');
rewrite::succ('删除成功');
}
跟进delete方法
public function delete($data){
$param['where'][] = 'type='.$data['type'];
foreach($data['fid'] as $k => $v){
$fileInfo = explode('#####',$v);
$fid[] = $fileInfo[0];
$path[] = trim($fileInfo[1],'/');
}
$fid = implode(',',$fid);
$param['where'][] = 'fid in('.$fid.')';
if(parent::deleteModel($param)){
//删除文件
foreach($path as $v){
file::unLink(ROOT_PATH.$v);
}
}
}
没有啥过滤,只要value以#####开头即可,把刚刚包里的fid参数url解码,结果如下
fid[]=2#####/file/slide/20140827/201408271523025580.jpg
所以我们直接测试一下,在根目录下创建个test.txt,删除成功
后台任意文件写入+读取
在后台模板管理处,对文件进行编辑抓包
GET /admin.php?m=Template&a=editfile&dir=default/error.html HTTP/1.1
Host: lmxcms.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://lmxcms.com/admin.php?m=Template&a=opendir&dir=default
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_590a74fa0c262c903817f1bb7292db93=1650975259; Hm_lpvt_590a74fa0c262c903817f1bb7292db93=1650975259; PHPSESSID=jt97lo3n30a5fpehtj8upavr00; _dd_s=logs=1&id=72c3c0b9-9826-4e49-8501-83a901c685f7&created=1650977821212&expire=1650985309347
Connection: close
dir参数我们手动修改成../index.php,发现可以读取文件,其实这里应该是可以编辑的。我们现来审计下源码
editfile方法如下
public function editfile(){
$dir = $_GET['dir'];
//保存修改
if(isset($_POST['settemcontent'])){
if($this->config['template_edit']){
rewrite::js_back('系统设置禁止修改模板文件');
}
file::put($this->config['template'].$dir.'/'.$_POST['filename'],string::stripslashes($_POST['temcontent']));
addlog('修改模板文件'.$this->config['template'].$dir);
rewrite::succ('修改成功','?m=Template&a=opendir&dir='.$dir);
exit();
}
$pathinfo = pathinfo($dir);
//获取文件内容
$content = string::html_char(file::getcon($this->config['template'].$dir));
$this->smarty->assign('filename',$pathinfo['basename']);
$this->smarty->assign('temcontent',$content);
$this->smarty->assign('dir',dirname($_GET['dir']));
$this->smarty->display('Template/temedit.html');
}
跟进put方法,直接调用file_put_contents把data写到文件里,那这里可以直接构造写入文件
public static function put($path,$data){
if(file_put_contents($path,$data) === false)
rewrite::js_back('请检查【'.$path.'】是否有读写权限');
}
-END-
如果本文对您有帮助,来个点赞、在看就是对我们莫大的鼓励。
推荐关注:
团队全员均持CISP-PTE(注册信息安全专业人员-渗透测试工程师)认证,积极参与着各类网络安全赛事并屡获佳绩,同时多次高水准的完成了国家级、省部级攻防演习活动以及相关重报工作,均得到甲方的一致青睐与肯定。
原文始发于微信公众号(弱口令安全实验室):lmxcms代码审计(PHP)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论