## 前言
[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 ,看都看不懂。相比之下,第二种方法似乎更可行。
于是接下来就有三种办法了:
使用 Selenium 、Puppeteer 等库操作无头浏览器,获取网页内容。需要占用一定的资源,并且需要安装浏览器。
使用 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