漏洞简介

金和网络是专业信息化服务商,为城市监管部门提供了互联网+监管解决方案,为企事业单位提供组织协同OA系统开发平台,电子政务一体化平台,智慧电商平台等服务。金和OA C6 PlanGiveOut.aspx 接口处存在SQL注入漏洞XSS漏洞、越权访问IDOR漏洞,攻击者除了可以利用SQL注入漏洞获取 数据库中的信息(例如,管理员后台密码、站点的用户个人信息)之外,甚至在高权限的情况可向服务器中写入木马,进一步获取服务器系统权限。

影响版本

金和OA C6

fofa语法

app="金和网络-金和OA”

漏洞分析

攻击链全景

8aa3ec7b-5b79-4934-95f8-0479682af26b.png

一、鉴权分析

1.1 鉴权方式总览

PlanGiveOut 的鉴权由 四层构成,但实际防护极度薄弱:

层级

机制

代码位置

实际效果

① web.config

ASP.NET 内置 forms 认证

web.config

仅要求"已登录",无角色限制

② Global.asax

全局鉴权事件

Global.cs

空实现,无自定义鉴权

③ 基类 Page

强制角色/模块校验

Page.cs(OnLoad)

未调用,无强制鉴权

④ 业务页

页面级鉴权

PlanGiveOut.cs 全文

RoleCtrl/KeyCtrl 调用

① web.config 认证配置

<authentication mode="Windows">  <!-- -->
    <forms name="authenticationcookie"  <!-- -->
           loginUrl="JHSoft.Web.CustomQuery/info.aspx"
           protection="All" path="/" timeout="10">
    </forms>
</authentication>
<authorization>
    <allow users="*"></allow>  <!-- 允许所有(已认证)用户 -->
    <deny users="?"></deny>  <!-- 拒绝匿名用户 -->
</authorization>

mode="Windows"<forms> 子节点共存属反常配置,实际由 forms 票据(authenticationcookie)生效。<deny users="?"> 仅拦截匿名用户——只要持有任意有效登录态即放行,不校验角色/模块权限

② Global.asax 全局鉴权事件为空(Global.cs)

// JHSoftWare.dll → JHSoftWare.Global(Global.asax.cs)
protected void Application_BeginRequest(object sender, EventArgs e)
{
}   // Global.cs —— 空实现,请求入口阶段无任何检查

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
}   // Global.cs —— 空实现,认证阶段无任何自定义逻辑

Application_BeginRequest(请求最早阶段)与 Application_AuthenticateRequest(认证阶段)均为空方法。系统没有任何自定义鉴权拦截器,鉴权 100% 依赖 ASP.NET 内置机制。

三个已注册 HttpModule(web.config)同样不拦截鉴权:

// JHSoft.CustomQuery.HttpUploadModule.BeginRequest —— 仅处理上传,其余 return
private void Application_BeginRequest(object sender, EventArgs e)
{
    string text = context.Request.Path.ToLower();
    if (text.IndexOf("uploadfileiframe.aspx") == -1  // :只认上传页
        && text.IndexOf("uploadvideofileiframe.aspx") == -1
        && text.IndexOf("addnewfile.aspx") == -1)
        return;                                            // 非上传页直接放行
    ...
}
// JHWeb.qqfly.Upload.HttpUploadModule.BeginRequest —— 仅处理上传请求体
// JHSoft.Log.LogHttpModule.BeginRequest —— 完全空方法 {}


③ 基类 JHSoft.Base.Page 提供了鉴权方法但未强制调用(Page.cs)

// JHSoft.Base.dll → JHSoft.Base.Page(所有业务页基类)
public class Page : Page                                    // Page.cs
{
    protected override void OnLoad(EventArgs e)             // Page.cs
    {
        ...
        if (HttpContext.Current.Session["UserCode"] != null)  // Page.cs
        {
            string text2 = this.Session["UserCode"].ToString(); // Page.cs
            ...   // 仅读取用户配置(皮肤等),不做鉴权
        }
        this.OnLoad(e);                                     // Page.cs
    }

    public bool RoleCtrl(string Role1, string Role2)        // Page.cs —— 角色校验(需主动调用)
    {
        if (this.Session["UserCode"] != null)               // Page.cs
            text = this.Session["UserCode"].ToString();
        if (text == "Admin") flag = true;                   // Admin 直接放行
        ...
    }

    public void KeyCtrl(string keyCode)                     // Page.cs —— 模块校验(需主动调用)
}

RoleCtrl/KeyCtrl可选方法,需业务页主动调用才生效。PlanGiveOut 从未调用它们(全文无 RoleCtrl/KeyCtrl),所以第 ③ 层形同虚设。

④ PlanGiveOut 自身无鉴权(PlanGiveOut.cs)

// JHSoft.Web.PlanSummarize.dll → JHSoft.Web.PlanSummarize.PlanGiveOut
protected void Page_Load(object sender, EventArgs e)        // PlanGiveOut.cs
{
    ...
    if (this.Session["UserCode"] != null)                   // PlanGiveOut.cs —— 仅"读取",非校验
        this.strUserID = this.Session["UserCode"].ToString(); // PlanGiveOut.cs
    ...
    this.ShowPlanInfo(this.strPlanID);                      // PlanGiveOut.cs —— 直接进入业务逻辑
}

Page_Load 仅读取 Session["UserCode"](第 212-214 行),没有用它做任何权限判断strUserID 取了值却在 ShowPlanInfo 中完全未参与 SQL 过滤(详见 2.3 IDOR)。

1.2 鉴权绕过

系统存在 两条独立的鉴权绕过路径,使 PlanGiveOut 的所有漏洞升级为未授权可达

绕过方式 A:pathInfo 鉴权绕过(零成本,全站性)

现象(实测):/PlanGiveOut.aspx/PlanGiveOut.aspx/(带尾斜杠)返回 302 跳登录;而 /PlanGiveOut.aspx/Planselect/PlanGiveOut.aspx/PlanGiveOut/PlanGiveOut.aspx/任意字符 不触发鉴权,直接响应

根因:IIS 集成模式 + ASP.NET 4.x 下,带 pathInfo(.aspx/附加路径)的 URL 在 AuthorizeRequest 阶段的鉴权判定与纯页面 URL 不一致,<deny users="?"> 规则匹配不到带 pathInfo 的请求。

关键佐证(web.config):

<pages validateRequest="false" enableEventValidation="false"
       enableViewStateMac="false"></pages>  <!-- 三防护全关 -->
<httpRuntime requestValidationMode="2.0" .../>  <!-- -->
<validation validateIntegratedModeConfiguration="false"/>  <!-- 关闭集成模式校验 -->

关键性质:绕过与附加路径内容无关,只要存在 .aspx/yyy 结构即生效——PlanGiveOut 代码中无任何 [WebMethod],因此 /Planselect/PlanGiveOut 并非调用特定方法,而是 pathInfo 结构本身让鉴权模块"看不见"该请求需鉴权。由于 Global.cs、三个 HttpModule、基类均不拦截 .aspx/(1.1 节已逐层确证),此绕过为全站性,适用于任意 .aspx 页面。

GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/x   →  绕过 <deny users="?"> ,未授权直达
GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx      →  正常鉴权,302 跳登录
GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/     →  尾斜杠被规范化,无 pathInfo,302 跳登录

绕过方式 B:LoginByURL 日期密钥免密登录

入口:Jhsoft.Web.login/LoginByURL.aspxLoginByURL.cs(JHSoft.Web.Login.dll)。

Decryptstr 方法用当天日期派生 AES 密钥(LoginByURL.cs):

// LoginByURL.cs
byte[] bytes = Encoding.Default.GetBytes(
    DateTime.Now.ToString("yyyyMMdd") + DateTime.Now.ToString("yyyyMMdd"));
//   IV  = 当天日期重复 2 次,如 "2026072920260729"(16 字节)
byte[] bytes2 = Encoding.Default.GetBytes(
    DateTime.Now.ToString("yyyyMMdd") + DateTime.Now.ToString("yyyyMMdd")
    + DateTime.Now.ToString("yyyyMMdd") + DateTime.Now.ToString("yyyyMMdd"));
//   Key = 当天日期重复 4 次,如 "20260729"×4(32 字节)
aES.CreateKey(bytes2, bytes);   // 用完全可预测的密钥解密登录凭据

AES 类(JHSoft.CustomQuery.dllAES.cs)是标准 Rijndael,CreateKey 直接采用传入 Key/IV,无 KDF、无加盐:

// AES.cs
public void CreateKey(byte[] keyInfo, byte[] IVInfo)
{
    rij = Rijndael.Create();      // AES.cs
    rij.IV = IVInfo;              // AES.cs —— 直接用传入 IV
    rij.Key = keyInfo;            // AES.cs —— 直接用传入 Key
}

 数据流转:

攻击者(已知当天日期)本地计算 Key/IV
   → AES 加密 "username=password=timestamp(当前时间)"
   → GET /Jhsoft.Web.login/LoginByURL.aspx?<Base64密文>
       ↓ LoginByURL.cs  Decryptstr 用【相同日期密钥】解密 → "Yes"
       ↓ LoginByURL.cs  roles.GetUserID(username) 取真实用户ID
       ↓ LoginByURL.cs  签发 FormsAuthenticationTicket(600 分钟有效)
       ↓ LoginByURL.cs  FormsAuthentication.Encrypt → 写 authenticationcookie
       ↓ LoginByURL.cs  CreateSession(text)
       ↓ CreateSession → LoginByURL.cs  Session["UserCode"] = 该用户UserID
   → 攻击者获得有效登录态


时间校验仅 ±1 分钟(LoginByURL.cs),即时生成即时使用即可,不构成障碍。

f1220217-3745-48e4-821f-ff193e6a820b.png

二、代码分析

2.1 入口文件与反编译定位

PlanGiveOut.aspx (前台)
   └─ Inherits="JHSoft.Web.PlanSummarize.PlanGiveOut"
        └─ 编译于 JHSoft.Web.PlanSummarize.dll → 反编译得 PlanGiveOut.cs
              └─ 数据访问调用 DBOperatorFactory.GetDBOperator()
                   └─ JHSoft.IDAL.dll → SqlDBOperator(实现类)

Page_Load(PlanGiveOut.cs)从请求取参数、调用 ShowPlanInfo:

protected void Page_Load(object sender, EventArgs e)
{
    if (this.Request.QueryString["isCopy"] != null)  // —— 反射XSS Source
        this.isreadonly = this.Request.QueryString["isCopy"];  // —— 原样赋值无校验
    ...
    if (this.Request["planid"] != null)  // —— SQL注入 Source
        this.strPlanID = this.Request["planid"].ToString();  // if (this.Session["UserCode"] != null)  // this.strUserID = this.Session["UserCode"].ToString();  // —— 取了但查询不用
    if (this.Request["httpOID"] != null)  // —— 第二个注入 Source
    {
        this.strPlanID = this.Request["httpOID"].ToString();  // —— 覆盖 planid
        this.strHttpOID = this.strPlanID;
    }
    this.ShowPlanInfo(this.strPlanID);  // —— 进入注入 Sink
}

2.2 反编译关键 DLL — SQL 注入 Sink 追踪

ShowPlanInfo(PlanGiveOut.cs)将 strPlanID 直接字符串拼接进 SQL:

private void ShowPlanInfo(string strPlanID)                 // PlanGiveOut.cs
{
    DBOperator dBOperator = DBOperatorFactory.GetDBOperator();  // empty = " select UserID,username,planyear,... from [plan] left join ...";
    empty = empty + " where planid=" + strPlanID;  // —— 注入点①(数字型)
    empty = empty + " select * from plancontent where planfatherid="
            + strPlanID + " order by plancontent.PlanID asc";  // —— 注入点②(数字型)
    dataSet = dBOperator.ExecSQLReDataSet(empty);  // —— 执行
    ...
    // 二阶注入(来自首次查询结果)
    empty = "select * from PlanContent where PlanContent.PlanFatherID=(";
    empty = empty + " select top 1 PlanID from [Plan] where PlanFlag=" + text3
            + " and PlanYear=" + array[0]
            + " and PlanMonW=" + array[1]
            + " and RegCode='" + text6 + "' and PlanTypeID=" + text2 + ")";  // —— 注入点③
    dataTable = dBOperator.ExecSQLReDataTable(empty);  // }

逐层下沉到 SqlDBOperator(JHSoft.IDAL.dll),确证 零参数化:

// SqlDBOperator.cs
public override DataSet ExecSQLReDataSet(string QueryString)  // {
    DataSet dataSet = new DataSet();
    ReturnMethord returnResult = ReturnDataSet;
    ExecSQL(QueryString, dataSet, returnResult);  // return dataSet;
}

private object ExecSQLNotInTrans(string QueryString, ...)  // {
    ...
    comm = new SqlCommand(QueryString, conn);  // —— QueryString 即完整SQL
    comm.CommandType = CommandType.Text;  // —— 纯文本命令
    ...
    ReValue = ReturnResult(comm, ReValue);  // }

private object ReturnDataSet(SqlCommand comm, object ReValue)  // {
    SqlDataAdapter val = new SqlDataAdapter(comm);
    ((DataAdapter)val).Fill(ReValue as DataSet);  // —— Fill 支持批处理
    return ReValue;
}

Sink 确证:new SqlCommand(QueryString, conn)直接吞下拼接好的 SQL 字符串,CommandType.Text按原始 SQL 解析,SqlDataAdapter.Fill(DataSet)执行。整条链路无任何参数化绑定

d308fdbd-c921-4e44-83ec-898e9123553b.png

2.3 存在的漏洞点

漏洞 1:SQL 注入(严重,Critical)

注入点

行号

Source

Sink

类型

PlanGiveOut.cs

Request["planid"]

ExecSQLReDataSet

数字型

PlanGiveOut.cs

Request["planid"]/httpOID

同上

数字型

PlanGiveOut.cs

DB(PlanFlag/RegCode等)

ExecSQLReDataTable

二阶

  • 支持方式:因 Request["planid"] 为通用索引器(QueryString → Form → Cookies),GET / POST / Cookie 三种方式均可注入

  • 支持手法:Fill(DataSet) 支持批处理(第 246-247 行拼了两条 select,中间无分号即被分入两个 Table),故联合查询注入与堆叠注入均可

  • 类型校验在注入之后:int.Parse(text4)ExecSQLReDataSet之后执行,注入已完成,后续异常不影响效果。

漏洞 2:存储型 XSS(高)

PlanGiveOut.aspx<%= %>(Response.Write 等价)输出 数据库字段,仅 Replace("\n","<br>")无 HTML 编码:

前台输出(ASPX)

后端赋值(PlanGiveOut.cs)

DB 来源

<%=PrioPlanContent%>

dataTable.Rows[i]["PlanContent"]

PlanContent

<%=strPlanSum%>

PlanSumUp + Replace("\n","<br>")

PlanSumUp

<%=CurrentPlanContent%>

dataSet.Tables[1][...]["PlanContent"]

PlanContent

<%=strLeaderIdea%>

LeaderIdea + Replace("\n","<br>")

LeaderIdea

写入入口为计划录入页(WorkPlanAdd.aspx 等),写入后被本页未编码输出,触发于任何查看者(含领导/管理员)。

漏洞 3:反射型 XSS(高)— isCopy

Source(PlanGiveOut.cs):

if (this.Request.QueryString["isCopy"] != null)  // —— 仅 QueryString
    this.isreadonly = this.Request.QueryString["isCopy"];  // —— 无白名单校验

Sink(PlanGiveOut.aspx):

<body ... onselectstart="return !<%=isreadonly%>">

注入 ?isCopy=false;alert(document.cookie)// → 渲染为 onselectstart="return !false;alert(...)//" 触发执行。该参数只能经 GET(明确用 QueryString)。

漏洞 4:越权访问 IDOR(高)

strUserID取自 Session["UserCode"],但 ShowPlanInfo 的 SQL 完全未用 strUserID 做归属过滤(where planid=strPlanID)。任意已登录用户枚举 planid 即可越权查看他人/他部门的计划、总结、领导批示。

2.4 参数获取方式 / 请求方式分析

漏洞的可利用性首先取决于"参数从哪种 HTTP 请求里取"。ASP.NET 提供了多个取值 API,它们能触达的请求通道完全不同。本节逐参数对照源码确证。

2.4.1 ASP.NET 取值 API 的通道差异(原理)

取值写法

数据来源查找范围

可触达的请求通道

Request.QueryString["k"]

URL 查询串

只能 GET

Request.Form["k"]

POST 请求体

只能 POST

Request.Cookies["k"]

Cookie 头

只能 Cookie

Request["k"] / Request.Params["k"]

按序查 QueryString → Form → Cookies → ServerVariables

GET / POST / Cookie / 请求头均可

关键区别:Request["k"](通用索引器)不区分通道,会按固定顺序遍历所有集合。这意味着只要代码用它取参,攻击者就能选择最隐蔽的通道注入(如 Cookie,WAF 常不检查)。

2.4.2 逐参数对照源码

PlanGiveOut.Page_Load 中每个 Source 参数的读取方式:

参数

代码写法

API 类型

可注入通道

对应漏洞

planid

Request["planid"]

通用索引器

GET / POST / Cookie / 头

SQL注入 ①②、IDOR

httpOID

Request["httpOID"]

通用索引器

GET / POST / Cookie / 头

SQL注入(覆盖 planid)

isCopy

Request.QueryString["isCopy"]

仅 QueryString

只能 GET

反射型 XSS

源码印证(PlanGiveOut.csPage_Load):

// —— 通用索引器(多通道):SQL 注入面
if (this.Request["planid"] != null)      // GET/POST/Cookie 均可
    this.strPlanID = this.Request["planid"].ToString();
if (this.Request["httpOID"] != null)     // GET/POST/Cookie 均可
    this.strPlanID = this.Request["httpOID"].ToString();

// —— 仅 QueryString(单通道):反射 XSS 面
if (this.Request.QueryString["isCopy"] != null)   // 只能 GET
    this.isreadonly = this.Request.QueryString["isCopy"];

注意 planidhttpOID覆盖关系:httpOIDplanid 之后读取,若两者同传,httpOID覆盖 planid。但两者都走通用索引器,注入通道一致,对攻击者无差别。

2.4.3 各通道的实际利用方式

① GET(URL 参数) — 最直接,但最易被 WAF/日志捕获:

GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/x?planid=1%20union%20select%20... HTTP/1.1
GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/x?isCopy=false;alert(1)// HTTP/1.1

② POST(请求体)Request[] 同样接收,即使前端表单无该字段:

POST /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/x HTTP/1.1
Content-Type: application/x-www-form-urlencoded

planid=1 union select ...

③ Cookie 注入Request[] 会查找 Cookies 集合,隐蔽性最强(URL 干净,常绕过只检 URL 的 WAF/IDS):

GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/x HTTP/1.1
Cookie: planid=1 union select ...;

④ 反射型 XSS 的限制:isCopyRequest.QueryString,只能 GET。无法通过 POST/Cookie 触发该 XSS。

结论:

  • SQL 注入 / IDOR(planidhttpOID):走 Request[] 通用索引器,GET、POST、Cookie 三通道均可。其中 Cookie 注入最隐蔽,是金和 OA 这类老系统(普遍用 Request[])的典型绕 WAF 手法。

  • 反射型 XSS(isCopy):走 Request.QueryString,仅 GET 单通道

  • 结合 1.2 节 pathInfo 绕过,任一通道均可叠加 /x 后缀实现未授权利用

2.5 结合鉴权绕过的未授权利用

经 1.2 节绕过后,上述漏洞全部未授权可达:

GET /JHSoft.Web.PlanSummarize/PlanGiveOut.aspx/x?planid=1 union select ...
    ↑ pathInfo 绕过鉴权(零成本)        ↑ 未授权 SQL 注入

或经 LoginByURL 获得登录态后访问 /PlanGiveOut.aspx?planid=...两条绕过路径均使 PlanGiveOut 的 SQL 注入、IDOR、XSS 降级为未授权可达,无需任何账号凭据。