Das Problem mit CloudFlare ist, dass man Seiten im I’m Under Attack Mode schlecht curlen kann.

Beim ersten Aufruf sendet CF eine Seite mit Code 503 und einem __cfduid-Cookie die testen soll, ob JavaScript aktiv ist. Wenn ja, wird ein Formular gesendet:

<form id="challenge-form" action="/cdn-cgi/l/chk_jschl" method="get">
    <input type="hidden" name="jschl_vc" value="58d1a6f5ae5e8370f0ad39a57e24d4f9"/>
    <input type="hidden" name="pass" value="1446611501.924-p1DZPAloDm"/>
    <input type="hidden" id="jschl-answer" name="jschl_answer"/>
</form>

Interessanter ist aber der Weg zur jschl_answer. Im Header der Seite steht (hier leicht verschobenes) JavaScript:

var t, r, a, f, awWJlmK = {
    "Jn": +((!+[] + !![] + []) + (!+[] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]))
};
a = document.getElementById('jschl-answer');
f = document.getElementById('challenge-form');

awWJlmK.Jn += +((+!![] + []) + (!+[] + !![] + !![] + !![] + !![] + !![]));
awWJlmK.Jn -= +((!+[] + !![] + !![] + []) + (!+[] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]));

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.value = parseInt(awWJlmK.Jn, 10) + t.length;
f.submit();

Ein paar Dinge sind hier dynamisch: Der Name des Objekts, das die Jschl-Answer hält sowie der String in diesem (awWJlmK.Jn) und die Anzahl der Berechnungen, die damit passieren. Die Syntax aus +, ! , [ und ] lässt sich recht einfach beschreiben: ! castet und negiert zu Bool, ein + macht daraus eine Int (und das übrigens richtig schnell) und ein leerer Array [] ist zwar true, wenn zu einer Zahl gecastet aber 0. Witzig. Und sicher irgendwie erklärbar.

So ergeben sich bspw. Kombinationen wie +![]0, weil []true, !truefalse und +false0. Andersherum ergibt !+[] true, da +[]0 und !0true. 0 + true1.

Interessant sind auch Operationslose und leere Arrays am Ende einzelner Abschnitte: +!![] + [] zum Beispiel addiert 1 zu []. Number + Array ergibt einen String aus der Zahl und dem CSV-Array. Hier also den String 1, welcher sich, addiert mit einer Zahl, natürlich ebenso verhält. Aus (+!![] + []) + (!+[] + !![] + !![] + !![] + !![] + !![]) ergibt sich also nicht 1 + "6" = 7, sondern = 16. Das + am Anfang jeder Zeile sorgt dafür, dass im folgenden wieder mit einer Zahl gerechnet werden kann.

Damit ist der größere Teil der Challenge zu Ende und es fehlt bloß noch der String - bzw. dessen length - t. t ist die Adresse der Ressource ohne Protokoll und Pfad - zum Beispiel 192k.pw. Hierzu wird ein Link mit href="/" erstellt und dieser gleich wieder abgerufen - was ein relativ kreativer Weg ist an https://192k.pw/ zu gelangen. Protokoll und Slashes werden anschließend mit substr() entfernt.

Die jschl_answer ist also die berechnete Zahl + Länge der Domain. CloudFlare scheint den Mechanismus immer mal wieder zu ändern, allerdings hat der aktuelle noch starke Ähnlichkeit mit dem vor beinahe drei Jahren.

Ein bisschen schummeln und das ganze lösen kann man mit JavaScirpt recht einfach. Irgendwie logisch.

solveJschlChallange(script, url) {
    let match
    let jschl = /\:([\+\(\)\[\]!]+)/.exec(script)[1] // initial value
    let regex = /[a-z]+\.[a-z]+([\+\-\*\/])=([\+\(\)\[\]!]+);/gi // all the other lines

    // don't try this at home
    while(match = regex.exec(script))
        jschl = eval(`(${jschl}) ${match[1]} (${match[2]})`)

    jschl = + jschl + url.split('/')[2].length

    return jschl
}

Hat man nun alle drei Parts zusammen und die aktuelle minimale Wartezeit von 3 Sekunden zwischen den Requests überwunden, kann das Formular abgeschickt werden und im besten Fall kommt ein cf_clearance-Cookie zurück. Mit diesem, der __cfduid und dem selben User-Agent ist der eigentliche Content ist ab dem nächsten Request verfügbar. Das Script exportiert alles in ./headers.json (und versucht diese beim nächsten Request zu laden):

$ cat ./headers.json | jq '.'
{
    "User-Agent": "crystal/gems",
    "Cookie": [
        "__cfduid=d2831a76cc453ed9aa19f9093cf5196461449268686",
        "cf_clearance=19e692289859b5167995c7f199262d6a13cc6942-1449268690-2592000"
    ]
}

$ curl "https://192k.pw/" \
    -H "User-Agent: crystal/gems" \
    -H "Cookie: __cfduid=d2831a76cc453ed9aa19f9093cf5196461449268686; cf_clearance=19e692289859b5167995c7f199262d6a13cc6942-1449268690-2592000"

Ein fertiges Script liegt auf GitHub.

Zu erwähnen ist der cloudflare-scrape, der das ganze irgendwie mit Python und einer installierten JS-Runtime (V8/Node) löst. Dabei parst er die Challenge aber nicht selbst, sondern führt einfach das gesamte Script-Tag aus.