Skip to content

OneBlog v2.3.9 — Unauthorized Access to WeChat Official Account Sensitive Tokens #43

@Passwords404

Description

@Passwords404

OneBlog v2.3.9 — Unauthorized Access to WeChat Official Account Sensitive Tokens

1. Vulnerability Summary

Item Detail
Vulnerability Title Unauthorized Disclosure of WeChat Official Account access_token / jsapi_ticket
Affected Product zhangyd-c OneBlog
Affected Version v2.3.9
Vulnerability Type Missing Authentication for Critical Function / Sensitive Information Disclosure
CWE Classification CWE-306: Missing Authentication for Critical Function
Severity High (CVSS 3.1 Score: 7.5 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
Attack Vector Remote, unauthenticated
Prerequisite The target instance must have configured WeChat Official Account AppID and AppSecret in the admin backend

2. Vulnerability Description

OneBlog's frontend module (blog-web) exposes a WeChat JS-SDK signing endpoint at /api/jssdkGetSignature, intended to generate signatures for frontend JS-SDK integration. This endpoint suffers from critical security flaws:

  1. The blog-web module has no authentication framework configured. Apache Shiro is only configured in the blog-admin (backend) module, leaving all blog-web API endpoints accessible without any login or session.
  2. The endpoint method has no authorization annotations — no @RequiresPermissions, no @RequiresUser, no form of access control whatsoever.
  3. The endpoint returns the raw access_token and jsapi_ticket directly to the caller, these credentials can be used to invoke WeChat APIs with full Official Account privileges.

An attacker can obtain the target site's WeChat access_token (valid for 7200 seconds) and jsapi_ticket (valid for 7200 seconds) by sending a single unauthenticated POST request, enabling identity spoofing and unauthorized operations as the Official Account.


3. Technical Root Cause Analysis

3.1 Architecture Background

OneBlog uses a dual-module independent deployment architecture:

Module Function Default Port Authentication
blog-admin Admin backend 8085 Apache Shiro (full authentication & authorization)
blog-web Public frontend 8443 None (only Braum rate limiter)

3.2 Root Cause — Missing Authentication in blog-web

blog-admin module — Full Shiro authentication configured:

blog-admin/src/main/java/com/zyd/blog/core/config/ShiroConfig.java

→ Configures SecurityManager, ShiroFilterFactoryBean, CookieRememberMeManager, and other complete Shiro components.

blog-admin/src/main/java/com/zyd/blog/core/config/WebMvcConfig.java

→ Registers RememberAuthenticationInterceptor, intercepting all paths (/**).

blog-web module — Rate limiter only, no authentication:

blog-web/src/main/java/com/zyd/blog/core/WebMvcConfig.java

→ Only registers BraumIntercepter (malicious request rate limiting). No authentication interceptor is registered.

Search entire blog-web/ module for ShiroConfig → No results found

The blog-web module has no Shiro configuration at all.

3.3 Vulnerable Code Location

Vulnerable endpoint:

File: blog-web/src/main/java/com/zyd/blog/controller/RestApiController.java

@PostMapping("/jssdkGetSignature")
public ResponseVO jssdkGetSignature(String url) {

    if (StringUtils.isEmpty(url)) {
        return ResultUtil.error("页面地址为空");
    }

    // ... URL decoding ...

    // 1. Read WeChat config from database (NO AUTHENTICATION)
    SysConfig configId = sysConfigService.getByKey(ConfigKeyEnum.WX_GZH_APP_ID.getKey());
    SysConfig configSecret = sysConfigService.getByKey(ConfigKeyEnum.WX_GZH_APP_SECRET.getKey());
    if (StringUtils.isEmpty(configId) || StringUtils.isEmpty(configSecret)) {
        return ResultUtil.error("微信公众号appId、AppSecret未配置");
    }

    // 2. Request access_token from WeChat API using AppSecret
    Map<String, String> tokenMap = getAccessTokenComponent.getAccessToken(
        configId.getConfigValue(), configSecret.getConfigValue());
    String accessToken = tokenMap.get("accessToken");
    if (StringUtils.isEmpty(accessToken)) {
        return ResultUtil.error("accessToken is empty");
    }

    // 3. Request jsapi_ticket using access_token
    Map<String, String> ticketMap = jsApiTicketComponent.JsapiTicket(accessToken);
    String ticket = ticketMap.get("ticket");

    // 4. ⚠️ Return access_token and jsapi_ticket directly to the caller
    Map<String, Object> map = new HashMap<>();
    map.put("appid", configId.getConfigValue());
    map.put("timestamp", timestamp);
    map.put("noncestr", nonceStr);
    map.put("accessToken", accessToken);   // ⚠️ LEAKED
    map.put("ticket", ticket);              // ⚠️ LEAKED
    map.put("signature", signature);
    return ResultUtil.success("jssdkGetSignature获取成功", map);
}

access_token retrieval:

File: blog-core/src/main/java/com/zyd/blog/util/GetAccessTokenComponent.java

public Map<String, String> getAccessToken(String appId, String AppSecret) {
    String requestUrl = AccessTokenUrl
        .replace("APPID", appId)
        .replace("SECRET", AppSecret);
    // Direct call to WeChat API with no secondary validation
    HttpClient client = new DefaultHttpClient();
    HttpGet httpget = new HttpGet(requestUrl);
    String response = client.execute(httpget, responseHandler);
    JSONObject OpenidJSON = JSONObject.parseObject(response);
    String accessToken = String.valueOf(OpenidJSON.get("access_token"));
    result.put("accessToken", accessToken);
    return result;
}

jsapi_ticket retrieval:

File: blog-core/src/main/java/com/zyd/blog/util/JsApiTicketComponent.java

public Map<String, String> JsapiTicket(String accessToken) {
    String requestUrl = AccessTokenUrl.replace("ACCESS_TOKEN", accessToken);
    // Use access_token to obtain jsapi_ticket
    String response = client.execute(httpget, responseHandler);
    JSONObject openidJSON = JSONObject.parseObject(response);
    String ticket = String.valueOf(openidJSON.get("ticket"));
    result.put("ticket", ticket);
    return result;
}

4. Proof of Concept (PoC)

4.1 Prerequisites

The target OneBlog instance must have configured the WeChat Official Account AppID and AppSecret in the backend "System Configuration" page.

4.2 Attack Request

curl -X POST 'http://<target>:8443/api/jssdkGetSignature' \
  -d 'url=http://attacker.com'

No authentication is required. No cookies, tokens, or session credentials are needed.

4.3 Successful Response Example

Image

4.4 Exploitation Chain

Step 1: Send POST request to /api/jssdkGetSignature (no authentication)
    ↓
Step 2: Server reads wxGzhAppId and wxGzhAppSecret from database
    ↓
Step 3: Server requests access_token from api.weixin.qq.com using credentials
    ↓
Step 4: Server requests jsapi_ticket from api.weixin.qq.com using access_token
    ↓
Step 5: access_token and jsapi_ticket are returned directly to the attacker
    ↓
Step 6: Attacker uses access_token to call WeChat Official Account APIs

4.5 Environment Setup (for Verification)

If the target instance has not configured WeChat, credentials can be inserted directly into the database:

INSERT INTO sys_config (config_key, config_value, create_time, update_time)
VALUES
('wxGzhAppId', '<Your_AppID>', NOW(), NOW()),
('wxGzhAppSecret', '<Your_AppSecret>', NOW(), NOW());

Configuration key names are defined in:

File: blog-core/src/main/java/com/zyd/blog/business/enums/ConfigKeyEnum.java

WX_GZH_APP_ID("wxGzhAppId"),
WX_GZH_APP_SECRET("wxGzhAppSecret"),

5. Impact Assessment

5.1 Leaked Credentials and Their Capabilities

Credential Validity Capable Operations
access_token 2 hours (7200s) Mass messaging, follower list retrieval, custom menu management, user info access, tag creation, media upload, customer service messages, etc.
jsapi_ticket 2 hours (7200s) Constructing valid JS-SDK signatures to spoof frontend WeChat features
appid Permanent Used in conjunction with access_token

5.2 Potential Damage

  1. Official Account Identity Spoofing: Attacker can use the stolen access_token to send malicious messages to followers in the name of the Official Account.
  2. User Data Theft: Attacker can retrieve the follower list, user profile information, and other sensitive data.
  3. Menu Tampering: Attacker can modify the custom menu to redirect users to phishing pages.
  4. Content Manipulation: Attacker can upload or delete the Official Account's media materials (articles, images, etc.).
  5. Customer Service Abuse: Attacker can send unauthorized customer service messages to followers.

5.3 Scope of Impact

  • OneBlog v2.3.9 and all earlier versions
  • Any OneBlog instance that has configured WeChat Official Account functionality
  • Instances without WeChat configuration are not affected (the endpoint returns "微信公众号appId、AppSecret未配置")

6. Root Cause Summary

Root Cause Description
Architectural Design Flaw blog-web (frontend) and blog-admin (backend) use independent authentication configurations. blog-web does not integrate Shiro.
Missing Endpoint Authorization The jssdkGetSignature method lacks @RequiresPermissions or any other authorization annotation.
Excessive Credential Exposure The endpoint returns access_token and jsapi_ticket to the frontend caller. The frontend JS-SDK only requires the signature, timestamp, noncestr, and url — not the raw tokens.
Insufficient Security Audit The endpoint exposes backend token retrieval logic to the frontend, violating the principle of least privilege.

7. Remediation Recommendations

7.1 Immediate Fix (Recommended)

Option A: Remove access_token and jsapi_ticket from the response

The access_token and jsapi_ticket should never be returned to the frontend caller. The frontend JS-SDK initialization only requires signature, timestamp, noncestr, and url. All token acquisition and signature computation should be performed server-side, returning only the signature result.

Option B: Add authentication to the blog-web module

Register an authentication interceptor in blog-web's WebMvcConfig to require user authentication for sensitive endpoints like /api/jssdkGetSignature.

7.2 Long-term Fix

  1. Unify the authentication architecture: Apply Apache Shiro (or equivalent) to both blog-web and blog-admin modules.
  2. Apply principle of least privilege: All endpoints involving sensitive credentials must have authentication and authorization checks.
  3. Secure credential management: access_token should be cached server-side to avoid redundant token requests, reducing the exposure window.

8. References


9. Discovery Method

This vulnerability was discovered through static code analysis (white-box audit). The audit process identified:

  1. The blog-admin module configures a complete Shiro authentication system (ShiroConfig.java + WebMvcConfig.java with RememberAuthenticationInterceptor).
  2. The blog-web module has no authentication framework configured (search of the entire module yields no ShiroConfig).
  3. The blog-web WebMvcConfig only registers BraumIntercepter (rate limiter) with no authentication capability.
  4. The RestApiController.jssdkGetSignature() method has no permission annotations of any kind.
  5. The method directly returns the WeChat access_token and jsapi_ticket to the caller.
  6. Conclusion: Any unauthenticated remote attacker can obtain the target instance's WeChat Official Account sensitive credentials.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions