问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
巧用Chrome-CDP远程调用Debug突破JS逆向
渗透测试
CDP远程调用非常方便,他允许我们直接可以通过代码来操作浏览器完成一系列行为,希望通过我的这篇文章让师傅们对其有一定了解,学习并赋能与我们的渗透测试与安全研究工作之中,提升效率!
巧用Chrome-CDP远程调用Debug突破JS逆向 =========================== 前言 -- 我在测试一个网站的时候,大多数的网站不会对接口的请求数据做加密处理,但有的时候在测试一些重点行业的网络资产时,常常会碰到一些功能点存在加密,例如登录,个人信息查询,搜索等等。在以往我遇到这些存在接口加密站点的时候,一般就是通过一些调试手段,找到加解密函数,从F12的控制台对变量手动加密,解密,但这一方法的弊病就是我们每次都必须断好点,同时必须提前预知要加解密的内容,还要拼接函数执行,因此如果遇到需要FUZZ或者爆破的接口,只能放弃。再者就是通过开发者工具断点跟栈,一点点找函数调用关系,还原加密算法,但是一旦遇到加密复杂,存在js混淆的网站,这个方法的失败率就会成指数级上升。 于是,为了解决这个需求,在网上冲浪的时候,便发现了**Chrome DevTools Protocol**这个好东西,这是一套开放协议,简称CDP,允许外部程序通过Chrome浏览器提供的接口与其进行交互,CDP提供了丰富的接口,使得我们可以通过WebSocket协议来对接口传输数据,进而操作浏览器的各种功能,例如打开标签页,断点调试,执行js函数等等。这里有兴趣的小伙伴可以看一下官方文档:<https://chromedevtools.github.io/devtools-protocol/> 我在网上搜索资料的时候发现这方面教程极少,应用于安全领域方面的文章也是不超过五篇,在文章最后已经注明了地址,在按照文章学习使用的过程中,也是遇到了挫折重重,但是经过研究分析之后,本篇文章也都会尽可能详细地向大家介绍,CDP远程调用非常方便,他允许我们直接可以通过代码来操作浏览器完成一系列行为,希望通过我的这篇文章让师傅们对其有一定了解,学习并赋能与我们的渗透测试与安全研究工作之中,提升效率! CDP,启动! ------- ### 开启远程调试 首先我们要开启谷歌浏览器的远程调试,并且设置端口,同时允许所有的源访问调试界面: 进入Chrome的根目录,呼出cmd: ![image-20241219173023743.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-7ffa8e8c04a67c460e6add284da6cc201b8a52ae.png) 执行命令,打开Chrome浏览器,同时设置远程调试端口,允许所有源访问调试详情页面: **这里要注意,在运行下面命令的时候,我们要先关闭电脑上运行的所有浏览器,不然会由于冲突导致启动失败。** > chrome --remote-debugging-port=9222 --remote-allow-origins=\* ### 查看正在进行CDP调试的端点信息 执行命令我们会打开一个全新的Chrome,访问[http://localhost:9222/json](http://localhost:9222/json:): ![image-20241219173604462.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-9f5ae3214f1e25281b0daf00eda78f1b815832e0.png) 在上图可以看到,访问之后有一堆json格式的数据,实际这个通俗理解是Chrome为我们以API的方式展示的可供远程调用的端点,我们可以看到我现在有一个标签页,是百度,因此在第二个数据中也看到了它的WebSocke调试地址,webSocketDebuggerUrl,因此假如我们想对其进行远程断点debug,执行js函数,我们就可以对这个地址发命令即可。 这里也标注一下json数据中其他键的含义: - `id`: 会话 ID,用于在 CDP 中与特定页面进行通信。 - `title`: 页面的标题。 - `url`: 当前页面的 URL。 - `webSocketDebuggerUrl`: WebSocket 地址,用于与 CDP 建立通信。 CDP 命令 ------ CDP命令格式,即我们向WebSocket接口发送的数据格式,所有的命令实际上有一个统一的格式规范。 > { 'method': '方法(命令)', 'id': (随便一个数字), 'params': { > > 该方法需要用到的参数都写在这里 } } 官方文档介绍了很多命令,这里我们主要用到一个命令:**Debugger.evaluateOnCallFrame**,主要作用就是:在xx处断点,同时在这里执行xx函数。 > { 'method': 'Debugger.evaluateOnCallFrame', 'id': 123, 'params': { 'callFrameId': callFrameId, 'expression': expression, 'objectGroup': 'console', 'includeCommandLineAPI': True, } } 上面那句话可以看到我给了两个变量,而本方法参数callFrameId,实际就表示了在哪里断点,而expression则是我们要执行的js函数。callFrameId的获取我们在下面的实验部分将有所介绍 实验开始 ---- ### 生成一个示例登录站点 这部分使用GPT来完成,我选用的模型是ChatGPT 4o with canvas,提示词如下: > 请你帮我写一个登录系统,输入账号密码登录即可。要求使用php语言,无需使用mysql,直接用硬编码判断账号admin和密码admin123即可。登陆成功后可以无需有任何页面,这个系统的关键就是在前端写入账号密码,发送到后端接口校验的时候,要利用js对账号和密码的参数进行加密,请你发挥想象,对AES加密算法进行魔改,把加密算法写道单独的额外的js文件中,尽可能地避免通过前端的js代码审计可以复刻出加密算法,一定要确保js加密的安全性,防止被逆向。 在这篇文章完成的时候利用这个提示词,我测试了多个大模型,发现这个提示词会使AI产生幻觉,包括我再次用ChatGPT 4o跑了一遍,代码就有问题了,要么是前端得加密是先A再B再C,结果后端解密时候顺序乱套了,要么就是前后端的密钥和偏移,以及一些函数都不一样,容易出现一些尴尬的小问题,因此我把这个实验用的示例系统源代码也贴在下面,各位朋友可以复制到本地,方便复现学习。当然代码中也有丰富的注释,大家也可以一目了然加密逻辑,方面理解。 示例系统构成:前端页面i代码ndex.html,二次加密混淆逻辑代码secureEncrypt.js,后端校验代码login.php。 **index.html** ```html <html lang\="en"\> <head\> <meta charset\="UTF-8"\> <meta name\="viewport" content\="width=device-width, initial-scale=1.0"\> <title\>史上最安全的登录系统</title\> <script src\="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"\></script\> <script src\="./js/secureEncrypt.js"\></script\> <style\> /\* 页面基础样式 \*/ body { font-family: Arial, sans-serif; background-color: #f4f4f9; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; } /\* 登录容器样式 \*/ .login-container { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); max-width: 400px; width: 100%; } .login-container h1 { margin: 0 0 1.5rem; font-size: 1.8rem; color: #333; text-align: center; } /\* 表单样式 \*/ form { display: flex; flex-direction: column; } label { margin-bottom: 0.5rem; font-size: 0.9rem; color: #555; } input { padding: 0.8rem; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 1rem; font-size: 1rem; transition: border-color 0.3s ease; } input:focus { border-color: #007bff; outline: none; } button { padding: 0.8rem; background-color: #007bff; color: #fff; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; transition: background-color 0.3s ease; } button:hover { background-color: #0056b3; } /\* 错误和成功信息样式 \*/ .message { margin-top: 1rem; text-align: center; font-size: 0.9rem; } .message.error { color: #ff0000; } .message.success { color: #28a745; } </style\> </head\> <body\> <div class\="login-container"\> <h1\>史上最安全的登录系统</h1\> <form id\="loginForm"\> <label for\="account"\>账号:</label\> <input type\="text" id\="account" name\="account" placeholder\="请输入你的账号" required\> <label for\="password"\>密码:</label\> <input type\="password" id\="password" name\="password" placeholder\="请输入你的密码" required\> <button type\="submit"\>登录</button\> <div id\="message" class\="message"\></div\> </form\> </div\> <script\> document.getElementById('loginForm').addEventListener('submit', async function (e) { e.preventDefault(); const account \= document.getElementById('account').value.trim(); const password \= document.getElementById('password').value.trim(); const messageBox \= document.getElementById('message'); if (!account || !password) { messageBox.textContent \= "Please fill out all fields."; messageBox.className \= "message error"; return; } messageBox.textContent \= ""; // 清除之前的消息 const key \= "MySuperSecretKey1234567890123456"; // 必须 32 字节 const iv \= "RandomInitVector"; // 必须 16 字节 try { // 加密数据 const encryptedAccount \= secureEncrypt(account, key, iv); const encryptedPassword \= secureEncrypt(password, key, iv); // 发送加密数据到后端 const response \= await fetch('login.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ account: encryptedAccount, password: encryptedPassword, }), }); const result \= await response.json(); if (response.ok) { messageBox.textContent \= result.message; messageBox.className \= "message success"; } else { messageBox.textContent \= result.message || "Login failed."; messageBox.className \= "message error"; } } catch (err) { console.error('Error:', err); messageBox.textContent \= "An error occurred while logging in."; messageBox.className \= "message error"; } }); </script\> </body\> </html\> ``` secureEncrypt.js ```js // 自定义加密函数 function secureEncrypt(input, key, iv) { // AES 加密的核心函数 function aesEncrypt(data, key, iv) { return CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(key), { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }).toString(); // Base64 输出 } // 生成随机干扰数据 function addNoise(data) { const noise \= Math.random().toString(36).substring(2, 8); // 6 位随机字符 return noise + '|' + data + '|' + noise.split('').reverse().join(''); } // 替换部分字符以增加混淆 function obfuscate(data) { const map \= { 'a': '@', 'e': '3', '1': 'l', 'o': '0', 's': '$' }; return data.replace(/\[aeios\]/g, char \=> map\[char\] || char); } // 混淆处理 const noisyInput \= addNoise(input); // 添加干扰数据 const obfuscatedInput \= obfuscate(noisyInput); // 字符替换 // 加密混淆后的数据 return aesEncrypt(obfuscatedInput, key, iv); } ``` login.php ```php <?php header('Content-Type: application/json'); // 定义密钥和偏移量 $key \= "MySuperSecretKey1234567890123456"; // 32 字节 $iv \= "RandomInitVector"; // 16 字节 // AES 解密函数 function customDecrypt($data, $key, $iv) { $method \= "AES-256-CBC"; $options \= OPENSSL\_RAW\_DATA; // 确保 key 和 iv 使用 UTF-8 编码 $key \= mb\_convert\_encoding($key, 'UTF-8'); $iv \= mb\_convert\_encoding($iv, 'UTF-8'); // 解密 $ciphertext \= base64\_decode($data); $plaintext \= openssl\_decrypt($ciphertext, $method, $key, $options, $iv); return $plaintext; } // 反混淆函数 function removeNoiseAndReverse($data) { // 反字符替换 $map \= \['@' \=> 'a', 'e' \=> '3', 'l' \=> '1', 'o' \=> '0', '$' \=> 's'\]; $replacedData \= strtr($data, $map); // 替换回原始字符 // 提取原始数据 $parts \= explode('|', $replacedData); if (count($parts) !== 3) { throw new Exception("Invalid data format."); } return $parts\[1\]; // 返回中间的原始数据 } try { // 获取加密后的账号和密码 $encryptedAccount \= $\_POST\['account'\] ?? ''; $encryptedPassword \= $\_POST\['password'\] ?? ''; // AES 解密 $decryptedAccountRaw \= customDecrypt($encryptedAccount, $key, $iv); $decryptedPasswordRaw \= customDecrypt($encryptedPassword, $key, $iv); if ($decryptedAccountRaw \=== false || $decryptedPasswordRaw \=== false) { throw new Exception("AES decryption failed."); } // 反混淆和提取原始数据 $decryptedAccount \= removeNoiseAndReverse($decryptedAccountRaw); $decryptedPassword \= removeNoiseAndReverse($decryptedPasswordRaw); // 硬编码的账号密码 $validAccount \= 'admin'; $validPassword \= 'admin123'; if ($decryptedAccount \=== $validAccount && $decryptedPassword \=== $validPassword) { echo json\_encode(\['success' \=> true, 'message' \=> '登录成功!'\]); } else { echo json\_encode(\['success' \=> false, 'message' \=> "账号或密码错误!"\]); } } catch (Exception $e) { // 返回错误信息 echo json\_encode(\['success' \=> false, 'message' \=> 'Error: ' . $e\]); } ?> ``` 搭建完成,运行之后,我们抓登陆包,目前已经达到了请求包加密效果: ![image-20241219181532035.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-7c88de0f6278a768a7ff3472d8b328b95f1ffbb3.png) ### 开启协议监听 这一步骤用于监听我们执行debug的地方,得到callFrameId 打开网页,按F12进入开发者工具,进入设置,实验,开启Protocol Monitor ![image-20241219182307732.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-1c6f1388c120795e79f86db4e56b1e68b5bd71e9.png) 开启后紧接着我们去更多工具,开启协议监视器 ![image-20241219182500894.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-19dbd982c4679eac19e21213693a4b18ef687b7c.png) ![image-20241219182515883.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-9d54516bf6f8274cb1727c878c762f0f8c0e74dc.png) 开启后我们进行抓包,接下来便是常规的js逆向初始阶段,但是我们不用还原加密算法,直接找到加解密的函数即可,这个很简单,使用最常规的方法,点一下登录之后到抓包界面,点击js ![image-20241219182639501.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-1831ce0e7459dfc5ece202fdad51a0848a31328e.png) 进入后开启断点,再点击一下登录,发现程序已经断点,向上跟栈,得到加解密的最终步骤 ![image-20241219182728008.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-cf241b2484361dda3ecce7479b7de9f429a6f0c6.png) 这里由于是我们自己创建的实验环境,所以很简单的找到了加密函数地点,我们在这里再断点,重新运行后让网页在这里停下 ![image-20241219182909947.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-579b760375c583d1cc5988f18e07dadbf72462bd.png) 紧接着我们到控制台执行任意一个函数,这里我们直接输入变量,请求控制台把加密后的值打印出来 ![image-20241219183133822.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-a14a5e3064390120b334096686210e21b0c1dade.png) 接下来进入协议监视器,拉到最下方,突然发现一个熟悉的字眼!没错,这就是我们上面刚刚提到的一个CDP命令,在右边也是出现了命令执行的结果,再加上上面熟悉的请求/响应,整个过程很像抓包有木有,哈哈哈哈 ![image-20241219183241126.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-51d2157cc8bc13e436405932ef42c5d7ce354984.png) 从请求中,我们可以看到这次方法调用的参数,得到我们之后要用的**callFrameId**,这里细心观察可以看到,我们上面的命令编写,实际就是重写了一下这里的参数,我们只需要再包装成相同的格式,替换callFrameId后再重发即可,替换expression的值,执行我们想要的js函数,即可实现远程调用 ![image-20241219183403403.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-da860d36d526710fa62dfc7690c2544f604da990.png) ### 代码远程调试 从上面的步骤我们抓到了callFrameId为"9154534773061276043.7.0",接下来我们使用Python的websocket-client模块尝试远程调用。 在开始之前我们别忘了要到<http://localhost:9222/json>里面看下这个标签页的webSocketDebuggerUrl ![image-20241219194219469.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-053203abb75e68ce17e74b919cda03bf3f0fcadc.png) 代码如下: ```python import json import websocket webSocketDebuggerUrl \= "ws://localhost:9222/devtools/page/9F47572FDBFB63E8B1EF661D1714E3DC" command \= { 'method': 'Debugger.evaluateOnCallFrame', 'id': 123, 'params': { 'callFrameId': "9154534773061276043.7.0", 'expression': "encryptedAccount", 'objectGroup': 'console', 'includeCommandLineAPI': True, } } connection \= websocket.create\_connection(webSocketDebuggerUrl) connection.send(json.dumps(command)) print(json.loads(connection.recv())) ``` 运行之后,发现返回一个json格式的数据,这个数据恰好也就是我们从浏览器看到响应模块的内容,里面包含了该函数运行的结果 ![image-20241219194646655.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-8d13007a178aba3b80e0dc2fff061d7f498e5b99.png) 更改expression的值,运行其他函数,测试一下,这里我改成明文的函数,可以看到已经返回了我输入的明文 ![image-20241219194835812.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-e7a3f19f6cb5e879a76c30929b7d10ccb8ca79b8.png) ![image-20241219194820165-1734685938850-1.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-3a936c00b335a75007e7b7928836c8ca61feedfd.png) 至此,我们就成功实现了CDP远程调用,在代码层面执行了JS函数,实现逆向! 思路拓展 ---- ### 脚本化批量执行实现爆破 我们知道既然可以用Python远程调用CDP命令,执行JS函数,那么我们可以应用这一功能,批量执行目标网站的JS加密函数,把我们本地的明文字典,批量跑函数,得到密文字典,从而方便后续爆破测试,如下实战应用: 1. 目标网站登录存在复杂加密,js逆向逆不出来 ![image-20241220185041531.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-c1dd9b5c8b2f3d430e79f8eea1eed1013583eaa5.png) 2. 但是通过断点,我们可以得到加解密的函数,算法无法还原 ![image-20241220185119746.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-490cb7aa2323b04d578e27f5f154264a3cb9570a.png) 3. 使用Python进行CDP远程调用,批量跑加密函数,使明文字典变为密文字典 ![image-20241220185413609.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-e2eb0f38847301f56b94ecc46c44208f6185e8c1.png) 如下图代码,我把执行CDP命令部分封装成了一个方法,直接调用这个方法,即可打印批量函数运行的结果,得到密文字典 ```python import json import websocket def execute_dp(connection, data): command = { 'method': 'Debugger.evaluateOnCallFrame', 'id': 123, 'params': { 'callFrameId': "-2349054866915643001.1.0", 'expression': f'secureEncrypt("{data}", key, iv)', 'objectGroup': 'console', 'includeCommandLineAPI': True, } } connection.send(json.dumps(command)) return json.loads(connection.recv()) if __name__ == '__main__': webSocketDebuggerUrl = "ws://localhost:9222/devtools/page/1EC8507DECF7CA1E2FB1F9A9E9FB0862" connection = websocket.create_connection(webSocketDebuggerUrl) f = open("zhuan.txt", "r", encoding="utf-8") g = open("result.txt", "a", encoding="utf-8") for i in f.read().split("\n"): g.write(execute_dp(connection, i)['result']['result']['value'] + "\n") ``` ![image-20241220190844744.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-3e49cd2dec78196ab19682ba20c7998476ab17a6.png) 4. 使用Burp,Intruder模块,密码选用密文字典爆破 ![image-20241220191705786.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-e71c61f0dce59f7a77c9a9e8504d040fc162534b.png) 5. 返回200,成功爆破带有登录参数加密的系统 ![image-20241220191647023.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-101d6a6b0c73caf7994ff64bf67c6212037bafa5.png) ### 配合mitmproxy实现全流量加解密 mitmproxy是一款非常好用的工具,可以截获请求,同时支持将请求体内容存储为变量,因此我们就可以使用mitmproxy截获请求,将请求体的某个参数,以变量形式作为执行CDP命令的参数,将他解密之后再拼回请求体,转发至burp,这样我们在burp侧看到的请求就是解密后的,同时我们在burp设置代理转发,将流量转回mitmproxy,运行加密脚本,将明文加密成密文再拼接请求体,发给服务端。 这样就可以达到burp明文测试的结果,具体流量发送步骤如下: ![image-20241220204641779.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-e8e3bb28debf77affe597205b85c5c59986c12df.png) 我们直接使用python写一个decrypt.py(ChatGPT也非常OK) ```python from mitmproxy import http, ctx import urllib.parse # 用于解析和编码 URL 编码数据 import zhuan class ModifyPassword: def request(self, flow: http.HTTPFlow) -> None: # 判断是否是目标主机和POST请求 if "xx.xx" in flow.request.headers.get("Host", "") and flow.request.method == "POST": try: # 获取原始请求体 req_data = flow.request.text # 请求体是字符串 ctx.log.info(f"原始请求体: {req_data}") # 解析请求体为字典 parsed_data = urllib.parse.parse_qs(req_data) # 替换 password 参数值 if "password" in parsed_data: old_password = parsed_data["password"][0] # 原始 password 值 new_password = execute_cdp(data) # 替换为你需要的值 parsed_data["password"] = [new_password] ctx.log.info(f"旧的 password 值: {old_password}") ctx.log.info(f"替换后的 password 值: {new_password}") else: ctx.log.info("请求体中未找到 password 参数") # 重新编码请求体 new_req_data = urllib.parse.urlencode(parsed_data, doseq=True) flow.request.text = new_req_data # 替换请求体为新的数据 ctx.log.info(f"修改后的请求体: {new_req_data}") except Exception as e: ctx.log.error(f"处理请求时出错: {str(e)}") addons = [ ModifyPassword() ] ``` 启用cmd,运行mitmproxy > mitmproxy --mode upstream:[http://127.0.0.1:8080](http://127.0.0.1:8080/) -s decrypt.py --listen-port 8989 --ssl-insecure 其中 --mode upstream:[http://127.0.0.1:8080](http://127.0.0.1:8080/) 指的是二次转发至burp 运行后可以看到mitmproxy已经抓到了,是密文 ![image-20241220204937396.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-8eb2cdcdc3c29e33af31ab8869cc0bcffe571f70.png) 但是我们到burp,拦截,发现password字段已经变成了明文 ![image-20241220205035691.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-7759430f364a3d60f057a9259a9ead9a76542ce6.png) 同理我们使用相同的方法,也可达到相应包全流程加解密。 结语 -- Chrom的CDP命令是一个十分强大的功能,这个我们也许往往忘记深入挖掘,实际其中的很多接口,很多方法都能对我们渗透测试过程提供莫大帮助,本文只是介绍了冰山一角,展现其对于js逆向的遍历,我们不用再拘束于传统的算法逆向,而是直接找到关键函数即可,希望读完这篇文章的你可以继续探索,欢迎评论交流,我是小安,感谢你的阅读! 致谢 -- - [https://blog.csdn.net/qq\_44657899/article/details/130987135](https://blog.csdn.net/qq_44657899/article/details/130987135) - [https://blog.csdn.net/qqyy\_sj/article/details/143672162](https://blog.csdn.net/qqyy_sj/article/details/143672162) - <https://www.t00ls.com/thread-69052-1-1.html> - <https://www.t00ls.com/thread-69054-1-1.html>
发表于 2025-01-22 09:00:01
阅读 ( 629 )
分类:
WEB安全
0 推荐
收藏
0 条评论
请先
登录
后评论
新人小安
2 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!