【JS逆向】webpack入门之某医保平台

62 阅读5分钟

声明

本文章所有内容仅供学习交流使用,不用于其他任何目的,其中的抓包内容、数据接口、敏感网址等均已做脱敏处理,严禁用于商业用途和非法用途,否则,由此产生的一切后果均与作者无关,若有侵权,请联系作者立即删除!

  • 目标网址:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL3BoYXJtYWNpZXM/Y29kZT0xNzQwMDAmbWVzc2FnZT1zZXJ2ZXJVcmwlMjBpcyUyMG51bGwmZ2JGbGFnPXRydWU=
  • 目标数据接口:L3F1ZXJ5UnRhbFBoYWNCSW5mbw==

image.png

第一步:抓包分析,查看接口参数、返回值

  • header中可以看到有多个疑似加密字段,不确定是否需要处理,暂且放置。
  • 经过多次重放请求发现cookie值内容一样,所以cookie可以写死不用管。

image.png

  • 请求参数中appCode是不变的,我们疑似需要处理的是encData、signData,优先处理encData

image.png

  • 这样看请求参数是变化的且加密的,所以这部分内容是必须处理的,那么就可以先处理请求参数后再去验证header中的参数是否必须处理,如果请求参数处理完发现header写死也可以正常返回数据,那么皆大欢喜,如果还是无法正常返回则再尝试分析处理请求头。
  • 返回结果中看到返回值也是密文,encData也是必处理项

image.png

第二步:定位请求参数

通过关键字搜索在几个疑似位置打上断点,刷新页面发现在一处断住

image.png

发现第一次断住不是我们目标接口,放行断点继续查看,第二次断住是我们的目标请求,并且找到了参数字段。分页参数是动态的,regnCode是地区编码可以根据需要传入固定值,queryDataSource固定值es,其他字段值都是空

image.png

向下调试发现在该处返回了密文,可以确定这里就是加密函数位置

image.png

往上翻翻明显看到这是webpack结构,我们的加密逻辑就在7d92这个模块里

image.png

进入n函数,也就是核心加载器函数o

image.png

把整个加载器文件抠到本地补上window环境,加上模块调用日志输出

image.png

尝试运行,发现少document环境

image.png

补上环境发现还少其他环境,那么我们分析加载器代码,是否存在初始化自执行模块来进行环境检测,下面发现自执行了o(o.s = 0),直接注释掉,再次运行,不再报环境错误。

image.png

把加载器挂在到全局,方便在外部调用,打印加载器测试

image.png

我们看到webpack在导出时把f函数和g函数重新命名为a、b导出,所以我们在外部使用时需要使用导出的模块函数名,f函数就是我们加密所在函数,所以我们使用window.loader("7d92").a作为加密函数。

image.png

image.png

最后在本地构建参数对象,测试后发现其他相关字段和header值都一同出来了

image.png

image.png

原来是f加密函数中做了所有的事情,后续关于请求部分我们就不用再分析header和signData了,现在请求部分已经完成,接下来我们同样的方式找到解密模块,最终发现就是g函数,也就是导出的b。最后在本地封装js函数和python代码实现完整流程:调用js获取加密参数 → 请求接口获取加密结果 → 调用js解密函数解出明文数据。

#.....扣下来的加载器部分js代码略


// ================== 初始化 ==================
const mod = window.loader("7d92")

const buildReq = mod.a        // 构建请求
const decodeData = mod.b     // 解密函数

// ================== 构建请求 ==================
function buildRequest(pageNum) {
    const req = {
        headers: {},
        data: {
            addr: "",
            regnCode: "330100",
            medinsName: "",
            businessLvOutMedOtp: "",
            pageNum: pageNum,
            pageSize: 10,
            queryDataSource: "es"
        }
    }
    return buildReq(req)
}

// ================== 解密 ==================
function decode(encDataStr) {
    const resp = JSON.parse(encDataStr);  // 把完整 JSON 字符串解析成对象
    return decodeData("SM4", resp);
}



// ================== CLI 入口 ==================
const mode = process.argv[2]

if (mode === "encrypt") {
    const pageNum = parseInt(process.argv[3]) || 1
    const result = buildRequest(pageNum)
    console.log(JSON.stringify(result))
}
else if (mode === "decrypt") {
    const encData = process.argv[3] || ""
    const result = decode(encData)
    console.log(JSON.stringify(result))
}
else {
    console.error("Usage:")
    console.error("  node crypto.js encrypt <pageNum>")
    console.error("  node crypto.js decrypt <encData>")
    process.exit(1)
}
import subprocess
import json
import logging
import requests
from typing import Dict, Any


# =========================
# 日志配置
# =========================
LOG_FORMAT = (
    "[%(asctime)s] "
    "[%(levelname)s] "
    "[%(filename)s:%(lineno)d] "
    "%(message)s"
)

logging.basicConfig(
    level=logging.INFO,
    format=LOG_FORMAT,
)

logger = logging.getLogger(__name__)


# =========================
# JS:生成加密参数
# =========================
def get_encrypted_params(page_num: int) -> Dict[str, Any]:
    """
    调用 Node.js 获取接口加密参数
    """
    logger.info("生成加密参数 | page=%s", page_num)

    try:
        result = subprocess.run(
            ["node", "loader.js", "encrypt", str(page_num)],
            capture_output=True,
            text=True,
            encoding="utf-8",
            timeout=10,
        )
    except subprocess.TimeoutExpired:
        raise RuntimeError("JS 加密执行超时")

    if result.returncode != 0:
        logger.error("JS 加密失败: %s", result.stderr)
        raise RuntimeError("JS 加密失败")

    stdout = result.stdout.strip()
    logger.info("JS encrypt 输出: %s", stdout)

    try:
        data = json.loads(stdout)
    except json.JSONDecodeError:
        raise RuntimeError("JS encrypt 输出不是合法 JSON")

    if "headers" not in data or "data" not in data:
        raise RuntimeError("JS encrypt 返回结构异常")

    return data


# =========================
# JS:解密 encData
# =========================
def decrypt_enc_data(enc_data: str) -> Dict[str, Any]:
    """
    调用 Node.js 解密接口返回的 encData
    """
    logger.info("开始解密 encData | length=%s", len(enc_data))

    try:
        result = subprocess.run(
            ["node", "loader.js", "decrypt", enc_data],
            capture_output=True,
            text=True,
            encoding="utf-8",
            timeout=15,
        )
    except subprocess.TimeoutExpired:
        raise RuntimeError("JS 解密执行超时")

    if result.returncode != 0:
        logger.error("JS 解密失败: %s", result.stderr)
        raise RuntimeError("JS 解密失败")

    stdout = result.stdout.strip()
    logger.debug("JS decrypt 输出: %s", stdout)

    try:
        decrypted = json.loads(stdout)
    except json.JSONDecodeError:
        raise RuntimeError("JS decrypt 输出不是合法 JSON")

    return decrypted


# =========================
# 请求接口
# =========================
def fetch_page_data(page_num: int) :
    """
    请求接口获取加密响应
    """
    encrypted = get_encrypted_params(page_num)

    headers = {k: str(v) for k, v in encrypted["headers"].items()}
    headers.update({
        "Cache-Control": "no-cache",
        "Pragma": "no-cache",
        "channel": "web",
    })

    cookies = {
        "amap_local": "330100",
    }

    body = encrypted["data"]
    if isinstance(body, str):
        body = json.loads(body)

    url = "https://xxxxx"

    logger.info("请求接口 | page=%s", page_num)
    logger.info("请求 body: %s", body)

    resp = requests.post(
        url,
        headers=headers,
        cookies=cookies,
        json=body,
        timeout=30,
    )

    resp.raise_for_status()

    return resp.text

# =========================
# 主流程
# =========================
def main():
    """
    逐页执行:
    加密 → 请求 → 解密 → 立即输出
    """
    for page in range(1, 3):
        logger.info("========== 开始处理第 %s 页 ==========", page)

        try:
            # 1. 请求接口
            encrypted_resp = fetch_page_data(page)

            logger.info("加密结果: %s", encrypted_resp)

            # 2. 解密
            decrypted_data = decrypt_enc_data(encrypted_resp)

            # 3. 输出结果
            result = {
                "page": page,
                "encrypted_response": encrypted_resp,
                "decrypted_data": decrypted_data,
            }

            print(
                json.dumps(
                    result,
                    ensure_ascii=False,
                    indent=2
                )
            )

            logger.info("========== 第 %s 页处理完成 ==========", page)

        except Exception as e:
            logger.exception("第 %s 页处理失败", page)

            print(
                json.dumps(
                    {
                        "page": page,
                        "error": str(e)
                    },
                    ensure_ascii=False,
                    indent=2
                )
            )


if __name__ == "__main__":
    main()
    logger.info("全部任务执行完成")

最终完美输出结果


新人入门水平,大佬们多多交流指教。<抱拳>