前言
[链接登录后可见](MOFH)是 iFastNet 旗下的一个免费虚拟主机分销提供商,允许用户创建并管理自己的免费虚拟主机分销网站,国外著名的 [链接登录后可见] 和 [链接登录后可见] 就是它的分销。
大量实验表明, MOFH 给所有分销的免费虚拟主机都配置了反爬虫系统,其行为与请求所携带的 User-Agent(UA)标头和 IP 有关:
请求 UA 为 curl 的 UA 时,返回空响应,如果是 curl 会提示:curl: (52) Empty reply from server
。
UA 中包含 Googlebot
(不区分大小写)或者 IP 在白名单中时,返回页面真正的 HTML 内容,此时可直接获得页面内容。
UA 为其他情况时,返回一段内容与用户 UA 和 IP 有关的 HTML 代码,检测当前环境是否为启用了 JavaScript(JS) 的浏览器。
如果客户端的 IP 地址处于黑名单中,那么此时无论用户的 UA 是什么,都会触发以上第三种情况。
因此要获取运行在 MOFH 分销的免费虚拟主机上的网站的内容,有两种方案:
第一种方案没什么可说的,这里主要研究第二种方案。
探索
我们以 [链接登录后可见] 这个网站为例,对返回的那段用来验证当前环境的 HTML 进行格式化,不难发现其包含一个引用 /aes.js
的 script 标签和一个直接包含 JS 代码的 script 标签,对后者的 JS 代码进行格式化处理后得到:
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 是[链接登录后可见]设置的,而这个模块的作用正是反爬。
所以接下来又有两条路:
第一条路在 2016 年的时候就已经有人发文探讨过了:[链接登录后可见] ,但是已经过去 8 年了,其可行性无法保证。重新走一次吗?我是做不到的,2451 行代码光是想想就觉得头疼,更何况我没学过 C ,看都看不懂。相比之下,第二种方法似乎更可行。
于是接下来就有三种办法了:
使用 [链接登录后可见] 、[链接登录后可见] 等库操作无头浏览器,获取网页内容。需要占用一定的资源,并且需要安装浏览器。
使用 JavaScript 解释器(如各大编程语言的 v8 库)运行验证网页中的 JavaScript 代码,获得 cookie 值。需要有安装合适的 JavaScript 解释器。
根据验证页面 JS 的解密过程进行解密,而不运行 JS。
作为一个 PHPer ,我选择用 PHP 搞。前两种方案需要安装第三方拓展,本着能省事就省事的原则,我决定尝试第三种方案。
首先我需要把那段 JavaScript 代码变成 PHP ,借助于 GPT 的力量,这非常简单, 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 实现。最终我在 [链接登录后可见]中找到了 PHP 版本的实现。同时,里面还有 JS 、Python 和 Ruby 的实现。经过比对, 这个仓库的 JS 代码和 aes.js
的代码几乎一模一样!
然后,我兴奋地导入了 PHP 文件,但是调用时,提示我 decrypt
函数的参数数量不足。于是我简单浏览了一下代码,确认官方的 PHP 实现和 JS 实现并非完全等效。好在多余的参数修补并不复杂,只需要把这个函数开头的代码改成:
public static function decrypt($cipherIn,$mode,$key,$iv) {
$size=count($key);
$originalsize=count($cipherIn);
然后保存,重新调用 getTestCookieValue
函数,传入 a, b, c 三个参数,可以发现成功返回了一个字符串,把这个字符串作为 __test
的值进行请求,成功返回网页原始内容!说明,这条路是行得通的!
上面我提到了 a, b, c 三个参数,这三个参数都在那段 JS 代码里明文显示,可以用下面的 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 文件以方便使用,有需要的可以去看看:[链接登录后可见]