【全网首发】使用 PHP 绕过 testcookie 模块防护,爬取运行在 MyOwnFreeHost 免费主机上的网页内容

## 前言

[MyOwnFreeHost](https://myownfreehost.net/)(MOFH)是 iFastNet 旗下的一个免费虚拟主机分销提供商,允许用户创建并管理自己的免费虚拟主机分销网站,国外著名的 [InfinityFree](https://www.infinityfree.com/) 和 [ProFreeHost](https://profreehost.com/) 就是它的分销。
大量实验表明, MOFH 给所有分销的免费虚拟主机都配置了反爬虫系统,其行为与请求所携带的 User-Agent(UA)标头和 IP 有关:

  • -

    请求 UA 为 curl 的 UA 时,返回空响应,如果是 curl 会提示:</s>curl: (52) Empty reply from server<e>

  • -

    UA 中包含 `Googlebot`(不区分大小写)或者 IP 在白名单中时,返回页面真正的 HTML 内容,此时可直接获得页面内容。

  • -

    UA 为其他情况时,返回一段内容与用户 UA 和 IP 有关的 HTML 代码,检测当前环境是否为**启用了 JavaScript(JS) 的浏览器**

  • -

    如果客户端的 IP 地址处于黑名单中,那么此时无论用户的 UA 是什么,都会触发以上第三种情况。

  • 因此要获取运行在 MOFH 分销的免费虚拟主机上的网站的内容,有两种方案:

  • -

    使用含有 </s>Googlebot<e> 的 UA 进行请求,适合临时请求且 IP 不在黑名单。

  • -

    按照浏览器的行为进行操作,适合需要频繁请求或者 IP 刚好被 MOFH 拉黑了。

  • 第一种方案没什么可说的,这里主要研究第二种方案。

    ## 探索

    我们以 http://nihao.rf.gd 这个网站为例,对返回的那段用来验证当前环境的 HTML 进行格式化,不难发现其包含一个引用 `/aes.js` 的 script 标签和一个直接包含 JS 代码的 script 标签,对后者的 JS 代码进行格式化处理后得到:

    ```javascript function toNumbers(d) { var e = []; d.replace(/(..)/g, function(d) { e.push(parseInt(d, 16)) }); return e }

    function toHex() {
    for (var d = , d = 1 == arguments.length && arguments[0].constructor == Array ? arguments[0] : arguments, e = “”, f = 0; f < d.length; f++) e += (16 > d[f] ? “0” : “”) + d[f].toString(16);
    return e.toLowerCase()
    }
    var a = toNumbers(“f655ba9d09a112d4968c63579db590b4”),
    b = toNumbers(“98344c2eee86c3994890592585b49f80”),
    c = toNumbers(“53298470b95f64157a57f6ad04e8ec99”);
    document.cookie = “__test=” + toHex(slowAES.decrypt(c, 2, a, b)) + “; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/”;
    location.href = “http://nihao.rf.gd/?i=1”;
    ```

    其中最后两行引起了我的注意。这两行代码先是设置了一个名为 `__test` ,值为调用了几个函数后返回的字符串的 cookie,然后刷新了当前页面,顺带加上了一个参数 `i=1`,似乎是用来更新浏览器缓存的。
    于是就可以猜想:MOFH 的服务器会通过判断名为 `__test` 的 cookie 是否存在以及其值是否正确,决定是否进行环境验证。我在浏览器中获取了这个 cookie 的值,将其添加到请求头中,使用 curl 命令再次请求,成功返回正确的 HTML ,而修改这个值后再进行请求就和没有似的,说明我的猜想是正确的。查阅资料后我发现,这个 cookie 是[一个名为 testcookie 的 nginx 模块](https://github.com/kyprizel/testcookie-nginx-module)设置的,而这个模块的作用正是反爬。
    所以接下来又有两条路:

  • -

    阅读源代码,搞清楚这个 cookie 值的生成过程,直接预判。

  • -

    像浏览器那样,对其进行解密,得到 cookie。

  • 第一条路在 2016 年的时候就已经有人发文探讨过了:https://blog.kwiatkowski.fr/testcookie ,但是已经过去 8 年了,其可行性无法保证。重新走一次吗?我是做不到的,2451 行代码光是想想就觉得头疼,更何况我没学过 C ,看都看不懂。相比之下,第二种方法似乎更可行。
    于是接下来就有三种办法了:

  • -

    使用 SeleniumPuppeteer 等库操作无头浏览器,获取网页内容。需要占用一定的资源,并且需要安装浏览器。

  • -

    使用 JavaScript 解释器(如各大编程语言的 v8 库)运行验证网页中的 JavaScript 代码,获得 cookie 值。需要有安装合适的 JavaScript 解释器。

  • -

    根据验证页面 JS 的解密过程进行解密,而不运行 JS。

  • 作为一个 PHPer ,我选择用 PHP 搞。前两种方案需要安装第三方拓展,本着能省事就省事的原则,我决定尝试第三种方案。
    首先我需要把那段 JavaScript 代码变成 PHP ,借助于 GPT 的力量,这非常简单, PHP 版本如下:

    ```php function toNumbers($hexString) { $numbers = []; preg_match_all('/(..)/', $hexString, $matches); foreach ($matches[0] as $match) { $numbers[] = hexdec($match); } return $numbers; } function toHex(...$args) { $numbers = (count($args) === 1 && is_array($args[0])) ? $args[0] : $args; $hexString = ""; foreach ($numbers as $number) { $hexString .= sprintf('%02x', $number); } return strtolower($hexString); } function getTestCookieValue($a, $b, $c) { return toHex(slowAES::decrypt(toNumbers($c), 2, toNumbers($a), toNumbers($b))); } ```

    这里我把设置 cookie 的那段代码换成了 `getTestCookieValue` 函数,因为我要的就是这个 cookie 的值。
    但是,这个函数此时是调用不起来的,因为并没有一个叫“slowAES”的类,更没有一个属于这个类的 `decrypt` 方法,因此我还得实现这个类。手写是不可能的,直接用 GPT 把 `aes.js` 的代码转换成 PHP 吗?但是 GPT 并不是万能的。我开始尝试在网上搜索 slowAES 的 PHP 实现。最终我在 [一个名为 slowAES 的 Github 仓库](https://github.com/octopius/slowaes)中找到了 PHP 版本的实现。同时,里面还有 JS 、Python 和 Ruby 的实现。经过比对, 这个仓库的 JS 代码和 `aes.js` 的代码几乎一模一样!
    然后,我兴奋地导入了 PHP 文件,但是调用时,提示我 `decrypt` 函数的参数数量不足。于是我简单浏览了一下代码,确认官方的 PHP 实现和 JS 实现并非完全等效。好在多余的参数修补并不复杂,只需要把这个函数开头的代码改成:

    ```php public static function decrypt($cipherIn,$mode,$key,$iv) { $size=count($key); $originalsize=count($cipherIn); ```

    然后保存,重新调用 `getTestCookieValue` 函数,传入 a, b, c 三个参数,可以发现成功返回了一个字符串,把这个字符串作为 `__test` 的值进行请求,成功返回网页原始内容!说明,这条路是行得通的!
    上面我提到了 a, b, c 三个参数,这三个参数都在那段 JS 代码里明文显示,可以用下面的 PHP 代码截取:

    ```php $html = 'xxx'; //验证页面的 HTML 代码 $a = explode('")', expode('a=toNumbers("', $html)[1])[0]; $b = explode('")', expode('b=toNumbers("', $html)[1])[0]; $c = explode('")', expode('c=toNumbers("', $html)[1])[0]; ```

    把获得的值作为一个名为 __test 的 cookie 的值,添加到请求中,就可以随意获取页面内容了。

    ## 最后

    我将所有关键代码封装成了单个 php 文件以方便使用,有需要的可以去看看:https://github.com/yucho123987/bypass-testcookie-php

    [“\u3010\u5168\u7f51\u9996\u53d1\u3011\u4f7f\u7528 PHP \u7ed5\u8fc7 testcookie \u6a21\u5757\u9632\u62a4\uff0c\u722c\u53d6\u7f51\u9875\u5185\u5bb9”,“\u3010\u5168\u7f51\u9996\u53d1\u3011\u4f7f\u7528 PHP \u7ed5\u8fc7 testcookie \u6a21\u5757\u9632\u62a4\uff0c\u722c\u53d6\u8fd0\u884c\u5728 MyOwnFreeHost \u514d\u8d39\u4e3b\u673a\u4e0a\u7684\u7f51\u9875\u5185\u5bb9”]

    666 :xhj06:

    起点的反爬虫很厉害,可以拿来提高技术。

    https://www.qidian.com

    </s><i> </i>&lt;!DOCTYPEhtml&gt;&lt;html&gt;&lt;head&gt;&lt;metacharset="UTF-8"&gt;&lt;script&gt;varbuid="fffffffffffffffffff"&lt;/script&gt;&lt;scriptsrc="/C2WF946J0/probe.js?v=vc1jasc"&gt;&lt;/script&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/body&gt;&lt;/html&gt;<i> </i><e>

    感谢分享

    做爬虫python比php好用太多啦。 无头浏览器也好用,用php爬的话结合 Flaressolver更方便。

    @“James”#p190211 PHP 的好处是要求不高,到处都能跑:ac01:

    @“Yucho”#p190203

    帮忙看一下,用这个php文件爬下来三张图片名字都对,但是大小错误打不开,哪里有问题啊.:xhj17:


    ```<?php
    // 引入所需的库和文件
    require_once ‘./testcookie-decrypt.php’;

    // 配置
    $targetUrl = ‘http://nihao.rf.gd/’; // 替换为您的目标网站
    $saveDir = ‘./images/’; // 图片保存目录,确保此目录存在且可写
    $userAgent = ‘Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5’; // 模拟 iPad 用户代理

    // 确保保存目录存在
    if (!is_dir($saveDir)) {
    mkdir($saveDir, 0777, true);
    }

    // 函数:发送 HTTP 请求
    function httpRequest($url, $headers = , $cookies = null) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_USERAGENT, $GLOBALS[‘userAgent’]);
    curl_setopt($ch, CURLOPT_HEADER, 1); // 返回 header
    if (!empty($headers)) {
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    }
    if ($cookies !== null) {
    curl_setopt($ch, CURLOPT_COOKIE, $cookies);
    }

    $response = curl_exec($ch);
    $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $header = substr($response, 0, $headerSize);
    $body = substr($response, $headerSize);
    
    if (curl_errno($ch)) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new Exception("cURL error: " . $error);
    }
    curl_close($ch);
    
    return ['header' =&gt; $header, 'body' =&gt; $body];
    

    }

    // 步骤 1: 访问目标网站并获取 HTML
    try {
    $response = httpRequest($targetUrl);
    $html = $response[‘body’];
    } catch (Exception $e){
    die('Error fetching initial page: ’ . $e->getMessage());
    }

    // 步骤 2: 解析 testcookie 参数并解密
    try {
    $abc = parseabc($html);
    $testCookieValue = getTestCookieValue($abc[0], $abc[1], $abc[2]);
    } catch (Exception $e) {
    die('Error during testcookie decryption: ’ . $e->getMessage());
    }

    // 步骤 3: 设置 __test cookie 并再次访问
    try {
    $responseWithCookie = httpRequest($targetUrl, , ‘__test=’ . $testCookieValue);
    $htmlWithCookie = $responseWithCookie[‘body’];
    } catch (Exception $e){
    die('Error fetching page with cookie: ’ . $e->getMessage());
    }

    // 步骤 4: 从 HTML 中提取图片链接
    $imageLinks = ;
    preg_match_all(‘/<img.?src="(.?)"/i’, $htmlWithCookie, $matches);
    if (isset($matches[1])) {
    $imageLinks = $matches[1];
    } else {
    echo ‘No images found on the page.’;
    exit;
    }

    // 步骤 5: 下载图片到本地
    foreach ($imageLinks as $imageLink) {
    // 确保图片链接是完整的 URL
    if (strpos($imageLink, ‘http’) !== 0) {
    $imageLink = rtrim($targetUrl, ‘/’) . ‘/’ . ltrim($imageLink, ‘/’);
    }

    $fileName = basename(parse_url($imageLink, PHP_URL_PATH));
    $savePath = $saveDir . $fileName;
    
    try {
        $imageContent = httpRequest($imageLink);
        file_put_contents($savePath, $imageContent['body']);
        echo 'Image downloaded: ' . $savePath . "\n";
    } catch (Exception $e){
        echo 'Error downloading image ' . $imageLink . ': ' . $e-&gt;getMessage() . "\n";
    }
    

    }

    echo “All images downloaded.\n”;

    ?>
    ```

    @“暗中观察”#p190206 起点的文字压根就是各种加密的图片 哈哈

    最早的研究:http://code.google.com/p/slowaes/

    不过这个ifastnet限制cpu时间,长期占用不行的

    @“kuisa”#p190463 你这个和我引用的那个 Github 仓库其实是一种东西

    @“gogogo”#p190383 发请求之前先打印一下$testCookieValue 的值看看是否成功获取到了,你没写判断网页是否需要验证的逻辑,所以可能会出现无法获取testcookie的情况

    @“kuisa”#p190463 限制 CPU 占用应该指的是虚拟主机运行程序吧,我这属于客户端请求:xhj01:

    @“gogogo”#p190383 等等,我知道怎么回事了,你下载图片的时候,没传入cookie,把 </s>$imageContent = httpRequest($imageLink);<e> 改成 </s>$imageContent = httpRequest($imageLink, [], '__test='.$testCookieValue);<e>,就能正常运作了

    @“gogogo”#p190383 另外你这代码像是 AI .写的。。。

    @“Yucho”#p190484 是的,让ai写给初学者看.:xhj17:

    @“Yucho”#p190203

    再问一下,假设我在http://nihao.rf.gd搭了个短网址生成的php文件index.php,生成了http://nihao.rf.gd/ab123这个短网址,有时候访问http://nihao.rf.gd/ab123会变成http://nihao.rf.gd/ab123?p=1,主机商会在网址后加东西,怎么破,有解么?

    @“Yucho”#p190478 以前搞推流的然后fastcgi太猛了直接supend账户了,还发邮件警告我了

    虽然看不懂,但是看分析过程很享受。

    @“gogogo”#p190489 。。。我在文中也说了,会加这个是因为请求的时候会进行验证,实际上这个i参数并没有什么用。这个过程是由最外层的Nginx进行的,相当于防火墙,而你只能控制Apache的行为。对于浏览器,只能做到在跳转后又马上跳回来。。。

    @“kuisa”#p190492 ?你说的是什么鬼