首页
社区
课程
招聘
[原创]从编译内核开始复现思科产品任意文件上传漏洞(CVE-2023-20073)
发表于: 2024-4-1 16:40 14265

[原创]从编译内核开始复现思科产品任意文件上传漏洞(CVE-2023-20073)

2024-4-1 16:40
14265

漏洞编号: CVE-2023-20073

CVSS3 评分: 9.8

标签: #漏洞复现

关键字: 未授权文件上传

引用链接:

AVD

armel和armhf区别

一步步教你:如何用Qemu来模拟ARM系统

为什么ext4 rootfs会被挂载成只读模式

linux: running self compiled kernel in qemu: VFS: Unable to mount root fs on unknown wn-block(0,0)

Linux下关闭和开启IPv6的方法

Vexpress device tree blob

从零开始复现 CVE-2023-20073

⚠️ 影响设备:

Cisco rv340w_firmware

Cisco rv340_firmware

Cisco rv345p_firmware

补充:笔者的操作系统环境

Windows11+WSL2

如果您的apt工具可以下载到低版本的gcc↓

本篇我们需要用到的架构是armhf,所以需要安装gcc-arm-linux-gnueabihf

由于笔者的内核版本较高,最低只能下载到gcc-11,所以需要手动下载编译工具交叉编译工具

具体的GCC版本请根据您要编译的Linux内核进行区分(检查文件linux-x.x.x/include/linux/compiler-gccX.h)

本篇使用的Linux内核版本是3.2.1,如有需要请自行下载其它版本

内核源码中针对不同架构和开发板型号已经提供了不同的模板配置文件(arch/${arch}/configs),我们可选择一项先生成一份.config,这里我们选择的处理器型号是vexpress

针对本篇的固件模拟,编译内核前我们需要对配置文件进行一些修改

由于v3.2.1的内核不支持通过启用配置项CONFIG_ATAGS来从命令行中获取内核启动参数,所以我们需要在配置文件中通过字段CONFIG_CMDLINE来提前设置(使用qemu -append参数无效)

这是笔者的参数设置

如果不指定此项在启动服务时系统会因为内存溢出而崩溃

提前声明,下述解决办法并不一定能100%让您的内核被成功编译并且启动,如遇报错,请第一时间检查您的GCC版本与内核版本是否匹配

编译完成后我们得到了内核文件(arch/${arch}/boot/zImage),另外我们还需要设备树文件,Linux源码中提供了部分这样的文件(arch/${arch}/boot/dts),若未找到可自行下载

这里我们使用从固件中解包得到的文件系统,首先创建一个磁盘镜像

然后将其格式化,这里注意您的内核版本支持的文件系统格式,笔者使用的是3.2.1版本内核且编译时没有开启ext4文件系统支持,所以使用ext3

创建一个临时文件夹作为挂载点,并将解包得到的文件系统复制进其中

图片描述

图片描述

图片描述

本篇的固件文件系统默认启动了telnet以供外部进行访问,若需要从内向外的ssh连接操作则可能遇见以下问题

解决(主机):

重启ssh服务

解决(主机):

重启ssh服务

图片描述

图片描述

图片描述

注意看到最后一行PnP Agent is starting!,它启动了另一个服务,前提是您的操作系统支持IPv6,否则最后会启动失败,这就是为什么在内核编译的时候我强调了要开启IPv6的配置选项

图片描述

nginx最后会根据几个配置文件来启动uwsgi来负责监听几个端口以处理数据

图片描述

访问成功

已知是未授权的文件上传漏洞

这里最后启动了uwsgi-launcher

请求路径/upload会查找token文件进行授权验证,失败返回403,成功则保存临时文件并转发给form-file-upload处理

请求路径/api/operations/ciscosb-file:form-file-upload仅通过请求头字段Authorization存在与否判断是否转发给form-file-upload处理,此处nginx的授权验证可轻松绕过

这里直接贴我在IDA里分析过的伪代码

通过上述伪代码可发现,授权验证部分在移动上传文件的后面,意味着不论验证通过与否,文件都将从临时文件中被转移并保存

本函数负责了处理移动文件的操作,部分伪代码如下所示

通过此处可知若能控制pathParam=Portal则能将文件保存目录控制在/tmp/www/下,而此处正是访问时路由页面的存放位置

这个函数初始化了一个结构体,该结构体结构示意图如下

图片描述

最后该结构体被传递到multipart_parser_execute进行进一步处理

笔者在最初调试cgi的时候尝试通过qemu-user+IDA的来进行调试,所以遇到了一些问题,便分析了这个函数,众嗦粥知 :),cgi读取请求数据是通过stdin进行读取的,所以我尝试通过这种形式来直接模拟请求输入数据

图片描述

也既直接在终端中输入,但不论如何都没有数据被成功解析,最后发现问题出在这个函数中,该函数通过结构体的boundary作为一个判断,index作为数据光标位置和operateCode记录当前所处位置数据所属(既数据还是头)

而其中判断operateCode的重要决定条件就是换行符\r\n,其它的头部判断则通过:,;等符号进行

这就是为什么上述偷懒方法不生效的原因

上传文件处理程序upload.cgi中将文件保存操作提前至授权检查前,使其只需被调用且参数正确即可将文件保存至不同目录下。其中保存目录部分可控,保存文件名可控,保存目录可控中可利用项有/var/www/目录,可通过覆盖其主路由页面实施存储型XSS攻击

需要控制的字段(参数):

由于上述的偷懒调试法因为解析函数的原因无效(或许有其它的数据输入法,但是我没有探究了)。于是需要对upload.cgi进行patch后调试,因为cgi程序的特性,一次请求一次执行瞬间完成,既无法可控的通过调试器启动也无法在正常情况下当其执行时附加它,所以需要进行patch

通过上文的upload.cgi main函数伪代码可知其通过调用freadstdin进行读取,所以可以patch它的读取长度参数使其等待

将原长度R6修改为R6+1即可,现在我们发送测试报文如下

图片描述

此时BurpSuite进入了等待状态,我们查看虚拟机的后台进程

图片描述

从主机传入 gdb-server 对其进行附加调试,gdb-multiarch连接后可见其停在了SYS_read调用处等待,我们直接修改PC寄存器让其跳过并恢复正常流程

图片描述

将断点设置在参数获取的位置并放行

图片描述

图片描述

图片描述

图片描述

可见参数pathparam成功的控制了,而filename是生成的,这个是临时文件的保存目录,不影响所以无需控制。

已知需要控制内容如下

图片描述

图片描述

图片描述

图片描述

图片描述

eh。花了一周多的时间,终于把本篇内容完成并把笔记和文章写完了,最后文章总结下来其实内容不多(当然我也不擅长表达和总结),多少有点删减,只挑了我觉得重要的地方指出来和关键的部分记录下来,这是我做的第四个,hum算第三个吧,因为的第二个DIR823G其实和第一个DIR815本质上没区别,第二个做的是TOTOLINK的命令注入漏洞,那个也没什么很复杂的逻辑,

hum因为我比较倔,最开始其实4.12.12的内核成功编译了并且跑起来了环境(没有IPV6和启动参数)的问题,不过我就是想用3.2.1的:)。但是同样的编译通过,使用3.2.1内核的虚拟机却是各种起不来,我各种参数都调了一边,比如文件系统格式支持等。最开始的报错信息如下

我在配置文件中启用了ext4的支持但依旧如此,最后发现是由于内核启动参数没有生效导致,必须在编译的时候指定CONFIG_CMDLINE

上文说过在启动confd服务的时候要看见最后的PnP Agent is starting!才算成功,但是我最开始一直失败,报错如下

最后在配置文件里找到了IPV6的选项并启用了

只要涉及写操作都会报错

然后发现系统根目录挂载的是ro的,但是我也没法通过指令重新挂载,这个问题不论在4.12.12版本的内核还是3.2.1版本的内核都出现了,最后在一篇文章中找到答案,在配置文件中设置CONFIG_LBDAF=y

这个问题在上文中我也简述过了,在3.2.1版本的内核中出现,解决方法是在启动参数中添加mem=128M

有读者可能会好奇我上文怎么不用hackbar去发请求而是选择直接在bp中发,当我在 hackbar 中写入如下数据

乍一看好像其实没什么问题,但是注意在数据段头和数据之间的换行,那有一个空格,这导致最后发出去的包是这个样子的

这个问题我还没有找到原因,推测是数据段长度不够(但这只是现象),因为我各种测试之后只得出了这个结论,结合上一个问题错上加错之下让我碰壁相当之久

例如当我发送数据

nginx将这个临时文件保存了,但是没有成功转发给uwsgi(我附加调试了它),但是nginx本身似乎也在等待回复,所以临时文件既没有被清除,nginx也没有回复我的请求。加上上一个错误之后,我发送了错误的(我自己不知道)报文如下

它被Nginx转发了,但是自然而然的upload.cgi无法解析了

在准备环境,既内核那一部分,加上踩坑我花了两天半来解决,在hackbar的那个部分遇见问题到弄清楚原因,我花了两天半的时间来解决,其它大大小小的问题也是一天多,差不多一周,感觉还是挺,容易进坑的我 :)

到最后还有一个遗留问题就是nginx为什么没有转发请求到uwsgi,蹲一个路过大神回复解答一手

┌──(root㉿W1sh)-[~]
└─# lsb_release -a
No LSB modules are available.
Distributor ID: Kali
Description:    Kali GNU/Linux Rolling
Release:        2024.1
Codename:       kali-rolling
 
┌──(root㉿W1sh)-[~]
└─# uname -a
Linux W1sh 5.15.146.1-microsoft-standard-WSL2 #1 SMP Thu Jan 11 04:09:03 UTC 2024 x86_64 GNU/Linux
┌──(root㉿W1sh)-[~]
└─# lsb_release -a
No LSB modules are available.
Distributor ID: Kali
Description:    Kali GNU/Linux Rolling
Release:        2024.1
Codename:       kali-rolling
 
┌──(root㉿W1sh)-[~]
└─# uname -a
Linux W1sh 5.15.146.1-microsoft-standard-WSL2 #1 SMP Thu Jan 11 04:09:03 UTC 2024 x86_64 GNU/Linux
apt install gcc-arm-linux-gnueabi #armel
apt install gcc-arm-linux-gnueabihf #armhf
apt install gcc-arm-linux-gnueabi #armel
apt install gcc-arm-linux-gnueabihf #armhf
make CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm vexpress_defconfig
make CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm vexpress_defconfig
CONFIG_CMDLINE="root=/dev/mmcblk0p2 rw console=ttyAMA0 mem=128M"
CONFIG_IPV6=y
CONFIG_LBDAF=y
CONFIG_CMDLINE="root=/dev/mmcblk0p2 rw console=ttyAMA0 mem=128M"
CONFIG_IPV6=y
CONFIG_LBDAF=y
make CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm
make CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm
apt-get install zlib1g
apt-get install zlib1g:i386
apt-get install libc6-i386 lib32stdc++6 lib32gcc1 lib32ncurses5
apt-get install zlib1g
apt-get install zlib1g:i386
apt-get install libc6-i386 lib32stdc++6 lib32gcc1 lib32ncurses5
@arch/arm/include/asm/ftrace.
- extern inline void *return_address(unsigned int level)
+ static inline void *return_address(unsigned int level)
@arch/arm/include/asm/ftrace.
- extern inline void *return_address(unsigned int level)
+ static inline void *return_address(unsigned int level)
@arch/arm/kernel/return_address.c
- void *return_address(unsigned int level)
- {
-   return NULL;
- }
@arch/arm/kernel/return_address.c
- void *return_address(unsigned int level)
- {
-   return NULL;
- }
@kernel/timeconst.pl
- if(!defined(@val))
+ if(!@val)
@kernel/timeconst.pl
- if(!defined(@val))
+ if(!@val)
qemu-img create -f raw disk.img 512M
qemu-img create -f raw disk.img 512M
mkfs -t ext3 ./disk.img
mkfs -t ext3 ./disk.img
mkdir tmpfs
sudo mount -o loop ./disk.img tmpfs/ 
sudo cp -r rootfs/* tmpfs/
sudo umount tmpfs
mkdir tmpfs
sudo mount -o loop ./disk.img tmpfs/ 
sudo cp -r rootfs/* tmpfs/
sudo umount tmpfs
sudo qemu-system-arm \
    -M "vexpress-a9" \ #指定开发板型号
    -kernel "/kernel/linux/3.1.2/arch/armhf/zImage" \ #指定内核文件
    -dtb "/kernel/dtb/vexpress-v2p-ca9.dtb" \ #指定设备树文件
    -sd "./disk.img" \ #指定根文件系统
    -net nic \
    -net tap,ifname=tap0,script=no,downscript=no \ #在主机上添加一个网络接口tap0
    -nographic \ #无界面启动
    -smp 4 #指定cpu核心数量(根据具体的开发板型号支持数量定,太低会很卡
sudo qemu-system-arm \
    -M "vexpress-a9" \ #指定开发板型号
    -kernel "/kernel/linux/3.1.2/arch/armhf/zImage" \ #指定内核文件
    -dtb "/kernel/dtb/vexpress-v2p-ca9.dtb" \ #指定设备树文件
    -sd "./disk.img" \ #指定根文件系统
    -net nic \
    -net tap,ifname=tap0,script=no,downscript=no \ #在主机上添加一个网络接口tap0
    -nographic \ #无界面启动
    -smp 4 #指定cpu核心数量(根据具体的开发板型号支持数量定,太低会很卡
ifconfig tap0 192.168.11.1/24 up
ifconfig tap0 192.168.11.1/24 up
ifconfig lo 127.0.0.1 up
ifconfig eth0 192.168.11.2 up
ifconfig lo 127.0.0.1 up
ifconfig eth0 192.168.11.2 up
echo "KexAlgorithms diffie-hellman-group1-sha1" >> /etc/ssh/sshd_config
echo "KexAlgorithms diffie-hellman-group1-sha1" >> /etc/ssh/sshd_config
echo "HostKeyAlgorithms +ssh-rsa" >> /etc/ssh/sshd_config
echo "HostKeyAlgorithms +ssh-rsa" >> /etc/ssh/sshd_config
NGINX_BIN=/usr/sbin/nginx
UPLOAD=/var/upload
NOTIFYD=/usr/bin/notifyd
UWSGI=/usr/bin/uwsgi-launcher
#...
start(){
    #...
    $NGINX_BIN
    $NOTIFYD -i 127.0.0.1 &
 
    $UWSGI start
}
NGINX_BIN=/usr/sbin/nginx
UPLOAD=/var/upload
NOTIFYD=/usr/bin/notifyd
UWSGI=/usr/bin/uwsgi-launcher
#...
start(){
    #...
    $NGINX_BIN
    $NOTIFYD -i 127.0.0.1 &
 
    $UWSGI start
}
start() {
    uwsgi -m --ini /etc/uwsgi/jsonrpc.ini &
    uwsgi -m --ini /etc/uwsgi/blockpage.ini &
    uwsgi -m --ini /etc/uwsgi/upload.ini &
}
start() {
    uwsgi -m --ini /etc/uwsgi/jsonrpc.ini &
    uwsgi -m --ini /etc/uwsgi/blockpage.ini &
    uwsgi -m --ini /etc/uwsgi/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
[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
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 0;
 
    if (-f /tmp/websession/token/$cookie_sessionid) {
            set $deny "${deny}1";
    }
 
    if ($cookie_sessionid ~* "^[a-f0-9]{64}") {
            set $deny "${deny}2";
    }
 
    if ($deny != "012") {
            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;
}
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 0;
 
    if (-f /tmp/websession/token/$cookie_sessionid) {
            set $deny "${deny}1";
    }
 
    if ($cookie_sessionid ~* "^[a-f0-9]{64}") {
            set $deny "${deny}2";
    }
 
    if ($deny != "012") {
            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;
}
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;
}

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2024-4-1 16:58 被LeaMov编辑 ,原因: 标题
收藏
免费 13
支持
分享
最新回复 (3)
雪    币: 538
活跃值: (274)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
非常感谢你的分享,但是我在使用3.2.1内核启动时,由于内核中没有自带vexpress-v2p-ca9.dtb设备树,使用git中提供的设备树启动会报错VFS: Cannot open root device "mmcblk0p2" or unknown-block(179,2) 最终我放弃使用设备树sudo qemu-system-arm -M vexpress-a9 -m 1024M \
-kernel ./linux-3.2.1/arch/arm/boot/zImage \
-append "root=/dev/mmcblk0 rw console=ttyAMA0 init=/etc/init.d/rcS" \
-sd rootfs.ext3 \
-net nic -net tap,ifname=arm1, -nographic -smp 4 成功启动,我想知道你启动3.2.1内核版本时的真实命令 谢谢

另外,我看命令行是3.1.2的内核和文章的3.2.1对不上,想确认下是哪个版本?
2024-6-19 11:02
1
雪    币: 1878
活跃值: (3348)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
3
专业路过 非常感谢你的分享,但是我在使用3.2.1内核启动时,由于内核中没有自带vexpress-v2p-ca9.dtb设备树,使用git中提供的设备树启动会报错VFS: Cannot open root de ...
非常抱歉这么晚回复您
关于报错:请问您是否在编译系统内核的时候配置了`CONFIG_CMDLINE`选项?
关于版本:命令行中3.1.2是我新建文件夹的时候手误打错,实际版本为3.2.1
补充:内核3的版本请用编译选项代替qemu的-append参数,我看见您依旧使用了-append选项来指定内核启动选项挂载至"mmcblk0",而实际报错确是"mmcblk0p2",说明您在编译配置中的"CONFIG_CMDLINE"值是"mmcblk0p2",使用-append选项并不会影响编译时设置的值

感谢您的回复
2024-7-9 17:17
0
雪    币: 1878
活跃值: (3348)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
4
专业路过 非常感谢你的分享,但是我在使用3.2.1内核启动时,由于内核中没有自带vexpress-v2p-ca9.dtb设备树,使用git中提供的设备树启动会报错VFS: Cannot open root de ...

这是我在测试过程中关于内核编译的笔记原文,若出现其它报错您可查找关键词看看是否有相关记录


上传的附件:
2024-7-9 17:20
0
游客
登录 | 注册 方可回帖
返回
//