如何解决Cloudflare的5秒DDoS防御

实例:磁力链接转种子

网址:https://itorrents.org/
需求:通过该网站,根据磁力链接下种子文件

一般的下载格式:
请求地址:http://itorrents.org/torrent/INFO_HASH_IN_HEX.torrent
请求实例:http://itorrents.org/torrent/B415C913643E5FF49FE37D304BBB5E6E11AD5101.torrent

更新

【20200406】itorrents网站的Cloudflare升级之后,脚本出现

+Function("return escape")()(("")["italics"]())[2]+"o"+(undefined+"")[2]+(t

等等,可读性降低,在简化JS脚本时需要更多措施

必要Cookie

  • __cfduid:这个cookie在返回503时候从请求头得到,用于之后的认证过程
  • cf_clearance: 访问网站真正使用的cookie,请求头加入它,可以认证通过并正常访问到网站,该cookie的存活时间为2小时

代码获取cookie

使用的平台和工具:

  • 平台:JRE 8 或 Android 6.0
  • 软件:Postman,模拟http请求

第三方jar包:

  • jsoup: 爬取网页源代码,包括html和Js
  • rhino: 让安卓能够执行js脚本
  • okhttp: 用于发网络请求,获取cookie,下种子文件等操作

具体过程:

​ 首先代码发送请求,让服务器返回503。

//创建OkHttpClient对象
OkHttpClient client = new OkHttpClient.Builder().build();

//创建Request对象,设置一个url地址, 设置请求方式。
Request request = new Request.Builder()
                    .addHeader("User-Agent", USER_AGENT)
                    .url("https://itorrents.org").method("GET", null)
                    .build();

//创建一个call对象,参数就是Request请求对象
Call call = client.newCall(request);

//同步调用,返回Response,会抛出IO异常
Response response = call.execute();

​ 出现503错误,首先从返回头部中获取第一个cookie( __cfduid)的值

Headers headers = response.headers();
String setCookieStr = headers.get("Set-Cookie");
String __cfduid = setCookieStr.split(";")[0].trim();

​ 使用Jsoup爬取返回的网页数据

//  调用该句获取返回输入流
InputStream is = response.body().byteStream();
    
private String getErrorHtml(InputStream is){
    String resultHtml = "";
    try{
        BufferedReader reader = new BufferedReader(new InputStreamReader(is, "utf-8"));
        String line = null;
        while ((line = reader.readLine()) != null) {
        resultHtml += line;
        }
    } catch (IOException e){
        EvLog.e(TAG, "IOException: " + e.getMessage());
    }
    return resultHtml;
}

//获取表单所有参数
private Map<String, String> getNewHttpParams(String resultHtml) throws Exception {
    Document doc = Jsoup.parse(resultHtml);
    
    //取得表单
    Element loginForm = doc.getElementById("challenge-form");
    
    //取得script下面的JS变量
    Element js = doc.getElementsByTag("script").get(0);

    //过滤JS内容,只保留有效内容
    String jsData = js.data();
    jsData = jsData.substring(jsData.indexOf("setTimeout"), jsData.indexOf("'; 121'"));
    jsData = "var " + jsData.substring(jsData.indexOf("f,") + 2).trim();
    String tiStr = jsData.substring(jsData.indexOf("t = document.createElement('div');"), jsData.indexOf("challenge-form") + 18);
    jsData = jsData.replace(tiStr, "").replace("a.value", "var a").replace("t.length", "13");

    //...执行JS代码
    
    return params;
}

​ 观察返回结果:只有第四个参数的值没有体现

<form id="challenge-form" action="/cdn-cgi/l/chk_jschl" method="get">
 <input type="hidden" name="s" value="79e2e9582b532dc8c1803b475404232b947dd65e-1552293281-1800-AaPJ3gbam22HO+RRAxJmBJLQP/wyX5Gxit+iGz77w0hI8Ph74sfEKxz5xGm3RWVJzpTVdlM93S4QTzpDHf6HgUeyLd3E1kiC0B0d7rUOEKmd"></input>
<input type="hidden" name="jschl_vc" value="17505afb059e796938098575225488f4"/>
<input type="hidden" name="pass" value="1552293285.342-v/1sNwvefM"/>
 <input type="hidden" id="jschl-answer" name="jschl_answer"/>
</form>

​ 第四个参数需要分析返回网页的JS,代码如下

  //
            <![CDATA[
  (function(){
    var a = function() {try{return !!window.addEventListener} catch(e) {return !1} },
    b = function(b, c) {a() ? document.addEventListener("DOMContentLoaded", b, c) : document.attachEvent("onreadystatechange", b)};
    b(function(){
      var a = document.getElementById('cf-content');a.style.display = 'block';
      setTimeout(function(){
        var s,t,o,p,b,r,e,a,k,i,n,g,f, XcSJxuC={"sKGOcaDtX":+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(!+[]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(+[]))};
        t = document.createElement('div');
        t.innerHTML="<a href='/'>x</a>";
        t = t.firstChild.href;r = t.match(/https?:\/\//)[0];
        t = t.substr(r.length); t = t.substr(0,t.length-1);
        a = document.getElementById('jschl-answer');
        f = document.getElementById('challenge-form');
        ;XcSJxuC.sKGOcaDtX-=+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![])+(+!![])+(+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]));a.value = +XcSJxuC.sKGOcaDtX.toFixed(10) + t.length; '; 121'
        f.action += location.hash;
        f.submit();
      }, 4000);
    }, false);
  })();
  //]]>

​ 分析可以发现,变量a存储第四个参数值,f是提交的表单。因此,如何得到a变量的值是关键

a.value = +XcSJxuC.sKGOcaDtX.toFixed(10) + t.length;

​ 由两个部分组成,一个是XcSJxuC.sKGOcaDtX,另一个是t.length,首先解决t.length,t的值经过下面几个操作

 t = document.createElement('div');    //创建div标签
 t.innerHTML="<a href='/'>x</a>";      //添加子元素
 t = t.firstChild.href;  //获得a标签的href属性,这里是网站根路径https://itorrents.org/
 r = t.match(/https?:\/\//)[0];  //获取t的https://前缀
 t = t.substr(r.length);   //获取子串 itorrents.org/ 
 t = t.substr(0,t.length-1);     //去掉子串最后一个字符'/',结果 itorrents.org

​ 因此t.length值为13。每次返回的这一结果固定,因此直接替换成13即可

​ 解决XcSJxuC.sKGOcaDtX的问题,将js简化,跟其有关的只有如下操作

XcSJxuC={"sKGOcaDtX":+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(!+[]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(+[]))};
XcSJxuC.sKGOcaDtX-=+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![])+(+!![])+(+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]));
/*
[],数组符号,默认值为0,但不能单独使用
!, 取反符号,本身不具备数值意义,不能单独使用,需要和[]配合使用
!+[],取反加数组,值为1
(+[]), 加数组,值为0
+![],取反数组,值为0
+!![],二次取反数组,值为1
var b = +((!+[]+!![]+![])+(![]+![]+!![]+!+[]));  值为4
*/

​ 因此,在理论上是可以通过执行JS脚本把值算出来的,使用Jsoup获取全部脚本后,采取字符串操作将js过滤成如下代码:

var XcSJxuC={"sKGOcaDtX":+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(!+[]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(+[]))};
XcSJxuC.sKGOcaDtX-=+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![])+(+!![])+(+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]));
var a = +XcSJxuC.sKGOcaDtX.toFixed(10) + 13;

​ 最后返回的 a 就是第四个参数的值,因此,使用rhino库 ,写一个方法,让android能执行js代码,并获取结果

// 执行JS脚本,获得所需参数的值
private String getJsAnswer(String jsString) {
    //Enter a Context
    org.mozilla.javascript.Context context = org.mozilla.javascript.Context.enter();
    context.setOptimizationLevel(-1);
    try{
        //Initializing standard objects
        Scriptable scope = context.initStandardObjects();

        ScriptableObject.putProperty(scope, "javaContext",
                org.mozilla.javascript.Context.javaToJS(org.mozilla.javascript.Context.getCurrentContext(), scope));
        
        ScriptableObject.putProperty(scope, "javaLoader",
                org.mozilla.javascript.Context.javaToJS(getClass().getClassLoader(), scope));

        //Evaluating a script
        context.evaluateString(scope, jsString, HOST, 1, null);

        Object a = scope.get("a", scope);
        if (a == Scriptable.NOT_FOUND) {
            EvLog.e(TAG, "answer not found.");
        } else {
            return org.mozilla.javascript.Context.toString(a);
        }

    } catch (Exception e){
        EvLog.e(TAG, "Exception: " + e.getMessage());
    } finally {
        org.mozilla.javascript.Context.exit();
    }
    return null;
}

​ 如果是纯javaSE或者javaEE项目,支持javax,其自带js引擎可以帮助你执行js代码,更加方便,如下:

// 执行JS脚本,获得所需参数的值
private String getJsAnswer(String jsString) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    try {
        engine.eval(jsString);
        return engine.get("a") + "";
    } catch (ScriptException e) {
        e.printStackTrace();
    }
    return null;
}

​ 最后算出来结果,比如最后算出来jschl_answer为22.6738726886,结合前三个参数,使用okhttp包,GET方式提交表单到指定地址,这里需要注意延迟4秒再发送请求。

https://itorrents.org/cdn-cgi/l/chk_jschl?pass=1552293285.342-v/1sNwvefM&jschl_answer=22.6738726886&jschl_vc=17505afb059e796938098575225488f4&s=79e2e9582b532dc8c1803b475404232b947dd65e-1552293281-1800-AaPJ3gbam22HO+RRAxJmBJLQP/wyX5Gxit+iGz77w0hI8Ph74sfEKxz5xGm3RWVJzpTVdlM93S4QTzpDHf6HgUeyLd3E1kiC0B0d7rUOEKmd

​ 请求头要带上第一个获取的cookie __cfduid,代码如下

//模拟cloudflare延时4秒,以便产生新的cookie,也标志着此为耗时操作,只能在子线程中完成
Thread.sleep(4 * 1000);

OkHttpClient client = new OkHttpClient.Builder().cookieJar(new CookieJar() {
    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        List<Cookie> list = cookieStore.get(HOST);
        if(list.addAll(cookies)){
            EvLog.i(TAG, "saveFromResponse -- 添加进cookiestore");
            cookieStore.put(HOST, list);
        } else {
            EvLog.i(TAG, "saveFromResponse -- 重置cookiestore");
            cookieStore.put(HOST, cookies);
        }
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        List<Cookie> cookies = cookieStore.get(HOST);
        return cookies != null ? cookies : new ArrayList<>();
    }
}).build();

//创建Request对象,设置一个url地址, 设置请求方式。
Request request = new Request.Builder()
        .addHeader("User-Agent", USER_AGENT)
        .addHeader("Cookie", __cfduid)
        .addHeader("Referer", "https://itorrent.org")
        .addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
        .addHeader("Connection", "keep-alive")
        .addHeader("Host", "itorrent.org")
        .url(jsChl).method("GET", null)
        .build();

//创建一个call对象,参数就是Request请求对象
Call call = client.newCall(request);

//同步调用,返回Response,会抛出IO异常
Response response = call.execute();
if(response.code() == HttpURLConnection.HTTP_OK) {
    // 获得新的 Cookie
    List<Cookie> cookies = cookieStore.get(HOST);
    for(Cookie c : cookies) {
        if(c.name().equals("cf_clearance")) {
            return c;
        }
    }
} else {
    EvLog.e(TAG, "请求失败,错误码: " + response.code());
}

​ 如果返回200,则能拿到真正需要的cf_clearance的值,使用偏好设置将cookie持久化
​ 拿到第二个cookie之后,加入到请求头当中,就可以成功访问并下载种子文件

String downloadUrl = "http://itorrents.org/torrent/B415C913643E5FF49FE37D304BBB5E6E11AD5101.torrent";

//创建OkHttpClient对象
OkHttpClient client = new OkHttpClient.Builder().build();

//创建Request对象,设置一个url地址, 设置请求方式。
Request request = new Request.Builder()
      .addHeader("User-Agent", USER_AGENT)
      .addHeader("Cookie", cookie)
      .url(downloadUrl).method("GET", null)
      .build();

//创建一个call对象,参数就是Request请求对象
Call call = client.newCall(request);

//同步调用,返回Response,会抛出IO异常
Response response = call.execute();

//获取返回数据
if(response.code() == HttpURLConnection.HTTP_OK) {
      FileUtils.copyInputStreamToFile(response.body().byteStream(), saveTo);
      EvLog.i(TAG, "Download torrent success!!!");
} 

附:一些其他的种子缓存站

torrentinfo
btcache
thetorrent

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350