如何解决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 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;
}

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

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

  //
            x";
        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="x";      //添加子元素
 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 cookies) {
        List 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 loadForRequest(HttpUrl url) {
        List 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 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

你可能感兴趣的:(如何解决Cloudflare的5秒DDoS防御)