使用 curl 获取 123 网盘不限速下载链接

123 网盘具有上传和下载不限速的特点,而且新用户注册送 2T 的网盘空间,但是非会员只能下载 100M 以内的文件不限速,要么开会员,要么按次收费下载,那有没有什么方法绕过收费,实现任意大小的文件无限制下载呢?

该网盘在下载资源的时候会校验下载者是否为 vip 用户,如果是 vip 用户,那么下载别人分享的链接资源时,不限制下载大小,而我们实现无限制下载的的原理是 vip 用户可以获取该资源的下载链接,而通过获取到这个下载链接,就可以实现非 vip 用户的无限制下载,所以在进行本教程操作之前,需要先开通一个 vip,作为一个 24 小时生成下载链接的服务端。

然后以下面的无提取码的示例链接为例,该文件大小 18.30G:

https://www.123684.com/s/fBcNTd-T4MUh

一、使用 curl 提交 POST 请求进行登录

我们知道 POST 请求的信息是携带在信息体中的,因此我们需要知道,在登录 123 网盘的时候,携带用户名的字段是什么,携带密码的字段又是什么?

在浏览器中输入资源的分享链接,我们点击下载后可以看到,需要我们进行付费才能进行单次下载:

如果我们是 vip,且为登录状态的话,就可以直接进行下载,因此我们需要先利用 curl 工具进行 POST 登录,我们点击登录跳转到登录界面,填入账号和密码:

先不要进行登录,因为我们需要捕捉登录时提交的 POST 表单信息,因此我们按下 F12,进入开发者界面,我们用的是 Microsoft Edge,勾选 保存日志 和 选择 Fetch/XHR

勾选 保存日志 是因为登录完成后浏览器会降登录的 POST 请求删除掉,而选择 Fetch/XHR 是为了筛选出 所有的 GET 和 POST 请求,避免被其他无用的资源信息而干扰。

然后接下来我们选择登录,我们可以在调试器界面看到一个 sign_in 的请求,应该是和登录操作相关的请求:

我们点进去观察,发现这是一个 POST 请求,在 响应标头 部分,我们发现了请求成功后返回了一个 token,猜测这个 token 应该是登录后继续和服务器通信的凭证:

接下来我们点开 负载 部分,这个部分会展示 GET 和 POST 请求所提交的字段和信息,我们可以看到,我们浏览器发送了一个携带三个信息的 POST 请求实现了登录:

接下来我们可以测试一下,是否能够通过 curl 构造 POST 请求进行登录,首先我们利用上面的三个字段构造一个 POST 请求:

curl -X POST https://login.123pan.com/api/user/sign_in -d "passport=myName&password=myPwd&remember=true"

结果如下:

通过上面的结果,我们可以发现我们已经登录成功了,而且返回了两个比较重要的数据,第一个是登录状态的超时时间,第二个是超时时间的时间戳(1970年1月1日午夜到现在所经过的秒数)表示形式,第三个是后续用于验证身份的 token,时间戳转换工具可以参考下面网址:
https://tool.lu/timestamp/#:~:text=时间戳,是从1970

二、分析登录后如何获取到高速下载链接

通过 curl 能够实现成功登录后,我们继续回到浏览器,并且手动登录上账号,由于我们现在登录的是 vip 账号,当我们点击下载的时候,浏览器就会对资源进行下载操作,我们需要观察,浏览器是如何得到无限制的下载链接的,我们同样按 F12 打开调试器界面,然后点击下载,对发送的请求进行捕获:

可以看到我们捕获了六个请求,我们依次查看 响应 部分,可以看到,第三个请求中的 响应 部分包含了一个 DownLoadURL 字段,那么这个字段的大概率就是和下载链接相关的字段了,我们继续查看下一个请求:

请求的响应部分是一段 html 代码,应该是我们发送了请求,然后接收到了这段 html,对我们的请求 url 进行了一些操作,为了证实我们的猜测,我们点开 标头 部分,看看发送了什么请求:

原来是将上一个请求返回的 DownLoadURL 字段重新进行了一次 GET 请求,然后返回了一堆 html 代码,我们继续看下面一个请求的 响应 部分:

我们又看到了一个新的 redirect_url,顾名思义,是一个重定位的 url 地址,经过测试,这条地址是我们最终进行资源下载的地址,我们可以在浏览器的下载界面右键刚刚下载的文件,复制链接,比较两个链接就会发现是同一个:

既然我们已经找到了最终的下载链接,那么我们现在开始逆向,浏览器是经过了哪些操作,得到了这个下载链接。

我们首先看一下是发送了什么请求返回了这个链接,我们点回该请求的 标头 字段:

可以看到,浏览器发送了一个返回状态码为 210 的 GET 请求,得到了最终的下载地址,那么这个用于 GET 请求的地址又是哪里来的呢?我们继续往上追:

来到了刚刚返回的一堆 html 代码,并没有返回我们刚刚的上一个 GET 请求的 url 地址,但是这一段 html 代码是我们通过提交下面这个 GET 请求得到的:

那就奇怪了,我们对一个 url 提交了一个 GET 请求,得到了一段 html 代码,然后不知道从哪里冒出来一个新的 url ,对这个新的 url 进行 GET 请求并重定向后得到了最终的地址,那我们有充分的理由怀疑,这一段 html 代码肯定是对 标头 请求得 url 进行了某些不为人知的操作,才得到了一个新的 url,那么我们必须对这段 html 进行分析,看看它到底做了什么才行。

查看返回的 html 代码也可以通过在浏览器中输入:view-source:url,这样会把对这个 url 的 GET 返回的 html 源代码直接展示出来,适合查看

当然虽然我们也不是专门写网页端代码的专家,但是也不是说完全看不懂,为了提高效率,我们直接把这段 html 代码丢到 GPT 中逐行解析,分析如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
  <script type="text/javascript">
  // 关闭页面,根据不同的浏览器进行不同的操作
    function ClosePage(){  
      if (navigator.userAgent.indexOf("MSIE") > 0){
        if (navigator.userAgent.indexOf("MSIE 6.0") > 0){
          window.opener = null;
          window.close();
        } else {
          window.open('', '_top');
          window.top.close();
        }
      } else if (navigator.userAgent.indexOf("Firefox") > 0) {
        window.location.href = 'about:blank';
      } else {
        window.opener = null;
        window.open('', '_self', '');
        window.close();
      }
    }
    // 用于从 URL 中获取特定查询字符串(?号后面跟着的字符串)的值
    function GetQueryString(name) {
      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
      var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
      var context = "";
      if (r != null)
      context = r[2];
      reg = null;
      r = null;
      return context == null || context == "" || context == "undefined" ? "" : context;
    }
  </script>
  // 在 html 中添加一个 div 标签
  <body>
    <div id="app"></div>
  </body>
  <script type="text/javascript">
  const targetOrigin = '*'; 
  // 用于初始化 AJAX 请求的错误处理
  function _errorAjaxInit () { 
    let protocol = window.location.protocol;
    if (protocol === 'file:') return;
    if (!window.XMLHttpRequest) {
        return; 
    }
    let xmlhttp = window.XMLHttpRequest;
    let _oldSend = xmlhttp.prototype.send;
    let _handleEvent = function (event) {
          try {
            if (event && event.currentTarget && (event.currentTarget.status < 200 || event.currentTarget.status > 299)) {
              window.parent.postMessage({status:event.currentTarget.status,type:'fail',statusText: event.currentTarget.statusText,resourceUrl:event.currentTarget.responseURL || event.currentTarget.ajaxUrl}, targetOrigin);
            }
          } catch (e) {
            window.parent.postMessage({status:event.currentTarget.status,type:'fail',statusText: 'error',resourceUrl:event.currentTarget.responseURL || event.currentTarget.ajaxUrl}, targetOrigin);
            console.log('Tools error: ' + e);
          }
    };
    xmlhttp.prototype.send = function () {
      this.addEventListener('error', _handleEvent);
      this.addEventListener('load', _handleEvent);
      // this.addEventListener('abort', _handleEvent); 
      return _oldSend.apply(this, arguments);
    }; 
    var _oldOpen = xmlhttp.prototype.open;
    xmlhttp.prototype.open = function (method, url) {
      _oldOpen.apply(this, arguments);
      this.ajaxUrl = url;
    };
  }
  _errorAjaxInit();
  // 从 url 中获取查询字符串 params 和 is_s3 的值
  const params = GetQueryString('params');
  const isS3 = GetQueryString('is_s3');
  // 对 params 进行 Base64 解码
  let dCode = atob(params);
  // decodeURI() 函数可对 encodeURI() 函数编码过的 URI 进行解码
  let dCodeStr = decodeURI(dCode);
  if(params){
    if(isS3 == 1){ // s3(在我们的请求链接中 isS3 查询字符串一直为 0)
    // 将两次解码后的 url 赋值给了 a 标签,并把 a 标签隐藏了
      var download_file = {};
      if (typeof (download_file.iframe) == "undefined") {
        var iframe = document.createElement("a");
        download_file.iframe = iframe;
        document.body.appendChild(download_file.iframe);
      }
      download_file.iframe.style.display = "none";
      // 对 a 标签进行了模拟点击,这通常用于下载文件
      download_file.iframe.href = dCodeStr;  
      iframe.click();
    }else{
      // 我们的浏览器执行了接下来的代码
      var xhr = new XMLHttpRequest();
      // 对两次解码后的 url 进行了一次 GET 请求
      xhr.open('GET', dCodeStr, true);
        xhr.onreadystatechange = async function () {
          if(xhr.readyState == 3){    // 请求状态为 3 表示处理请求
              if(xhr.status == 200){
                xhr.abort();
                window.parent.postMessage({status:xhr.status,type:'success',statusText: xhr.statusText,resourceUrl:xhr.responseURL}, targetOrigin);
                var download_file = {};
                if (typeof (download_file.iframe) == "undefined") {
                  var iframe = document.createElement("a");
                  download_file.iframe = iframe;
                  document.body.appendChild(download_file.iframe);
                }
                download_file.iframe.style.display = "none";
                download_file.iframe.href = dCodeStr;  
                iframe.click();
              }else if(xhr.status == 210){    // 我们可以看到我们浏览器的这次 GET 请求返回码为 210
                if(xhr.response.indexOf("code") > -1){    // 返回的 code 为 0
                  let resData = JSON.parse(xhr.response);
                  if(resData.code == 0){
                    window.parent.postMessage({status:xhr.status,type:'success',statusText: xhr.statusText,resourceUrl:xhr.responseURL}, targetOrigin);
                    var download_file = {};
                    if (typeof (download_file.iframe) == "undefined") {    // 检查download_file对象是否已经有一个iframe属性
                      var iframe = document.createElement("a");
                      download_file.iframe = iframe;
                      document.body.appendChild(download_file.iframe);  // 将 a 标签添加到 body 中
                    }
                    download_file.iframe.style.display = "none";
                    download_file.iframe.href = resData.data.redirect_url;  // 把重定向的下载地址赋值给 a 标签  
                    iframe.click();    // 对 a 标签进行了模拟点击,这通常用于下载文件
                  }else{
                    window.parent.postMessage({status:resData.code,type:'fail',statusText: resData.message,resourceUrl:xhr.responseURL}, targetOrigin);
                  }  
                }
              }
          }   
        };
        xhr.ontimeout = function(event){
      window.parent.postMessage({status:0,type:'fail',statusText: '超时',resourceUrl:dCodeStr}, targetOrigin);
    };
        xhr.timeout = 5 * 1000;
        xhr.send(null);  
    }
  }
  </script>
</html>

通过上面对 html 代码的分析可以得出,其核心逻辑就是对我们该次 GET 请求的 url 进行了两次解码操作,用到的函数分别是 JS 脚本语言中的 atobdecodeURI,我们可以在下面的网址方便的查询这两个函数的作用,并通过示例代码来对我们的 url 进行解码,非常方便:
https://www.runoob.com/jsref/met-win-atob.html#:~:text=Window ato

通过查询可知,atob 函数是对 Base64 进行解码,而 encodeURI 函数用于将 url 中的中文进行编码,提高 url 的兼容性,而 decodeURI 进行反向操作,那接下来明了了,原来这段 html 的操作是对该次请求 GET 请求的 url 中的查询字符串 param 进行了两次解码操作,得到了一个新的 url,然后再对这个新的 url 发起了一个 GET 请求,得到了最终的下载链接,最后将下载链接赋值给一个 a 标签,并将其隐藏,最后通过模拟点击 a 标签来实现下载的操作。

为了验证我们的想法,我们进入刚才的网址,找到 atob 函数,然后找到示例代码,将查询字符串中的 params 丢进去进行 Base64 解码操作:

果然如此,decodeURI 也是同样的操作,那么通过对下面 url 的 GET 请求返回的一段 html 代码的分析,成功解析出了下一步的资源下载地址,那这段 GET 请求的 url 又是哪里来的呢?

我们之前分析过,来源于上面这个请求的 响应

而这个请求的 标头 如下:

可以看到这个请求的 URL 应该是固定的,它是一个 POST 请求,所以我们还要看看它所携带的 请求负载

这个 POST 请求有五个 请求负载,而这部分不同的资源文件是不同的,所以我们还需要往上追这部分是哪里来的,然后不管我们怎么翻上面两个请求的内容,也找不到关于这五个 请求负载 的一点信息了,那就只用一种可能了,那就是这部分信息在我们执行登录操作的时候就已经发给我们了,所以我们还得退出登录,看看到底浏览器发送了哪个请求,得到了这个链接。

于是我们在浏览器中退出登录登录,输入资源链接,点击下载,然后点击登录,继续弹出登录界面(必须从资源链接这里进入登录界面),按下 F12,点击登录:

因为我们是从 sign_in 登录后才能进行其他操作,所有我们从这个请求开始往下排查,我们发现在 发起程序请求发起程序链 里面有一条引人注目的 http 链接,而且后面的查询字符串带了登录成功后返回给我们的 token,这就有意思了,我们逆向了这么久,这个 token 一直没有用到,才想起来,原来在这里憋着坏呢?

我们直接把这个 url 丢到浏览器里面,回车:

可以看到,这个链接是已经处于 vip 账号登录阶段的链接,直接把 url 扔到浏览器可以是作为一个 GET 请求,那么这个返回的 html 已登录界面底层有没有藏着我们需要的信息呢?

我们直接用 curl 工具通过之前的登录获得的 token,构建一个该 url 的 GET 语句:

curl -X GET https://www.123684.com/s/fBcNTd-T4MUh?token=myToken

返回的 html 页面袋面如下(去掉无关信息后):

            window.g_useSSR = true;
            window.g_initialProps = {
                "res": {
                    "code": 0,
                    "message": "",
                    "data": {
                        "UserNickName": "17********8",
                        "UserID": 1840800539,
                        "ShareName": "95女鬼枪剑士.7z",
                        "HasPwd": false,
                        "Expiration": "2099-12-12T08:00:00+08:00",
                        "CreateAt": "2024-09-23T20:22:31+08:00",
                        "Expired": false,
                        "ShareKey": "fBcNTd-T4MUh",
                        "HeadImage": "https:\u002F\u002Fstatics.123pan.com\u002Fstatic-by-custom\u002Fdefault_avatar.png",
                        "IsVip": true,
                        "DisplayStatus": 1,
                        "isPayShare": 0,
                        "isReward": 1,
                        "payAmount": 0,
                        "resourceDesc": "",
                        "isChange": false,
                        "payStatus": 0,
                        "IsSVip": false,
                        "BackendImgUrl": "",
                        "DomainStatus": 0,
                        "HTTPSStatus": 0,
                        "SharePageDomain": "",
                        "IsShowAdvertisement": true,
                        "domainList": ["www.123pan.com", "www.123pan.cn", "www.123865.com", "www.123684.com", "www.123pan.cn"],
                        "shareBackground": [{
                            "type": 1,
                            "viewImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F提取码页面.png",
                            "viewUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
                            "payImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F付费资源页面.png",
                            "payUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
                            "fileImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F文件列表t.png",
                            "fileUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember"
                        }, {
                            "type": 2,
                            "viewImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F提取码页面.png",
                            "viewUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
                            "payImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F付费资源页面.png",
                            "payUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
                            "fileImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F文件列表t.png",
                            "fileUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember"
                        }, {
                            "type": 3,
                            "viewImg": "",
                            "viewUrl": "",
                            "payImg": "",
                            "payUrl": "",
                            "fileImg": "",
                            "fileUrl": ""
                        }, {
                            "type": 4,
                            "viewImg": "",
                            "viewUrl": "",
                            "payImg": "",
                            "payUrl": "",
                            "fileImg": "",
                            "fileUrl": ""
                        }],
                        "staticDomain": "statics.123957.com",
                        "sharePayDomain": "www.123pan.cn"
                    }
                },
                "reslist": {
                    "code": 0,
                    "message": "",
                    "data": {
                        "Next": "18446744073709551615",
                        "Len": 1,
                        "IsFirst": true,
                        "Expired": false,
                        "InfoList": [{
                            "FileId": 8171949,
                            "FileName": "95女鬼枪剑士.7z",
                            "Type": 0,
                            "Size": 19650979289,
                            "ContentType": "0",
                            "S3KeyFlag": "1840800539-0",
                            "CreateAt": "2024-09-21T20:37:19+08:00",
                            "UpdateAt": "2024-09-23T20:22:31+08:00",
                            "Etag": "deb837b0b0d3f092787000e76fb9da25",
                            "DownloadUrl": "",
                            "Status": 5,
                            "ParentFileId": 8171631,
                            "Category": 10,
                            "PunishFlag": 0,
                            "StorageNode": "m54",
                            "PreviewType": 0,
                            "checked": true
                        }]
                    }
                },
                "publicPath": "https:\u002F\u002Fwww.123684.com\u002Fb\u002Fapi\u002F"
            };

看到我们需要找的 5 个构造 POST 请求的 负载信息 赫然在列,当然也可以直接在浏览器中按 F12,找到这段返回的 html 代码。

那现在获取链接的完整流程已经明了,如下:

  1. 首先我们通过构造一个用于登录的 POST 请求进行登录,会返回一个 token
  2. 我们用这个 tokenhttps://www.123684.com/s/fBcNTd-T4MUh? 网址构造一个 GET 请求,里面会返回 5 个和特定资源相关的 负载信息
  3. 利用上面返回的 5 个 负载信息https://www.123684.com/b/api/share/download/info?3654564465=1727318211-6994024-1490120705 网址(经测试,后面的查询字符串可去除)构造一个 POST 请求,返回 DownloadURL
  4. 对这个 DownloadURL 发起一个 GET 请求,会返回一段 html 代码,而这段 html 代码会对 DownloadURL 中的查询字符串 params 进行两次解码,得到一个新的 url。
  5. 我们继续对这个新的 url 发起一个 GET 请求,返回状态代码为 210,并得到最终的资源下载地址。

为了验证我们推断出的流程有没有错误,我们接下来使用 curl 工具对整个流程进行测试,得到最终的下载地址。

三、通过 curl 验证整个流程

step 1 进行登录

首先我们构造一个 POST 登录请求:

curl -X POST https://login.123pan.com/api/user/sign_in -d "passport=myName&password=myPwd&remember=true"

会返回一个 token:

step 2 得到用于获取下载链接的 POST 请求参数

我们用这个 tokenhttps://www.123684.com/s/fBcNTd-T4MUh? 网址构造一个 GET 请求:

curl -X GET https://www.123684.com/s/fBcNTd-T4MUh?token=myToken

返回 5 个和特定资源相关的 负载信息,分别是 EtagFileIDS3keyFlagShareKeySize,用于构造构造获取下载链接的 POST 请求:

step 3 构造 POST 请求获取下载链接

首先我们通过得到的 负载信息 构造一个 POST 请求:

curl -X POST https://www.123684.com/b/api/share/download/info -d "Etag=deb837b0b0d3f092787000e76fb9da25&FileID=8171949&S3KeyFlag=1840800539-0&ShareKey=fBcNTd-T4MUh&Size=19650979289" -H "app-version:3" -H "platform:web" -H "authorization:Bearer myToken"

可以看到我们这次的 POST 请求额外多指定了三个请求头,分别是 app-versionplatformauthorization,经测试至少需要指定这三个请求头,前面两个分别表示请求的版本和平台,第三个传递的是登陆后返回的 token,提交请求后得到 DownloadURL

step 4 对下载链接的请求字符串进行解码

对这个 DownloadURL 构造一个 GET 请求:

curl -X GET myDownloadURL

返回了我们熟悉的那段 html 代码:

因此实际上我们不需要发起这个请求,直接将 DownloadURL 里面的查询字符串 params 进行 Base64 解码即可:

step 5 通过解码后的链接得到最终下载链接

我们继续对这个解码后的 url 构造一个 GET 请求:

curl -X GET "myDecodeURL"

当然,由于我们的 url 里面有 & 特殊字符,所有我们需要把 url 用双引号引起来,或者在每个 & 字符前面加一个 ^ 符号也行,终于得到了我们最终的下载链接:

得到最终的下载链接后把其中的转义字符 \ 去掉即可。

四、获取带提取码的下载链接

获取带提取码的下载链接步骤基本一致,唯一不同点在 step 2 得到用于获取下载链接的 POST 请求参数 步骤有所差别。

通过之前类似的方法进行分析,找到如下的 GET 请求:

在分析获取无提取码资源下载链接时,获取 EtagFileIDS3keyFlagShareKeySize 这五个请求参数是构造一个 GET 请求来实现的:

curl -X GET https://www.123684.com/s/fBcNTd-T4MUh?token=myToken

而带提取码的文件在获取这五个参数的时候也采用的是 GET 方法,对 https://www.123684.com/b/api/share/get? 网址发起请求,并且附带了 10 个参数(2972272565=1728980514-657908-1876151490 参数经测试可以忽略),分别是 limitnextorderByorderDirectionshareKeySharePwdParentFileIdPageeventoperateType,得到的结果如下:

因此,通过构造如下命令即可得到 step 2 中的五个参数:

curl -X GET "https://www.123684.com/b/api/share/get?limit=100&next=0&orderBy=file_name&orderDirection=asc&shareKey=fBcNTd-wXMUh&SharePwd=S3kU&ParentFileId=0&Page=1&event=homeListFile&operateType=1"

返回的结果如下:

五、获取无提取码的文件夹下载链接

如果分享的不是单个文件,而是整个文件夹,那么我们应该如何处理呢?我们对下载文件夹的资源链接进行分析:

我们看到,在按照前面的方法获取 EtagFileIDS3keyFlagShareKeySize 这五个参数后,浏览器对 https://www.123pan.com/b/api/file/batch_download_share_info 网址发起了 POST 请求,并携带了参数 ShareKeyfileIdList

然后得到了合并文件的下载链接:

解析该下载链接得到最终的下载链接,我们可以看到该链接下载的是一个 .rar 后缀的文件,是将需要下载的文件夹压缩成了一个 .rar 文件,并给出对应的下载链接:

posted @ 2024-09-26 15:04  lostin9772  阅读(31)  评论(0)    收藏  举报