A Python module to crack Cloudflare's JavaScript challenge (aka "I'm Under Attack" mode)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
9.2 KiB

  1. import enum
  2. import functools
  3. import operator
  4. import re
  5. class ParsingError(Exception):
  6. pass
  7. class EvaluationError(Exception):
  8. pass
  9. class StrEnum(enum.Enum):
  10. def __repr__(self):
  11. return str(self)
  12. class Type(StrEnum):
  13. group = 1
  14. brackets = 2
  15. class Modifier(StrEnum):
  16. negate = 1
  17. plus = 2
  18. class JSBool:
  19. def __init__(self, value):
  20. self.value = bool(value)
  21. def __add__(self, other):
  22. if isinstance(other, (JSBool, JSInt)): # bool + bool/int -> int (addition)
  23. return JSInt(self) + JSInt(other)
  24. elif isinstance(other, JSString): # bool + string -> string (concatenation)
  25. return JSString(self) + other
  26. else:
  27. return NotImplemented
  28. def __sub__(self, other):
  29. if isinstance(other, (JSBool, JSInt, JSString)): # bool - bool/int/string -> int (subtraction)
  30. return JSInt(self) - JSInt(other)
  31. else:
  32. return NotImplemented
  33. def __mul__(self, other):
  34. if isinstance(other, (JSBool, JSInt, JSString)): # bool * bool/int/string -> int (multiplication)
  35. return JSInt(self) * JSInt(other)
  36. else:
  37. return NotImplemented
  38. def __eq__(self, other):
  39. return isinstance(other, JSBool) and self.value == other.value
  40. def __bool__(self):
  41. return self.value
  42. def __int__(self):
  43. return int(self.value)
  44. def __str__(self):
  45. return str(self.value).lower() # 'True' in Python is 'true' in JS
  46. def __repr__(self):
  47. return 'JSBool({!r})'.format(self.value)
  48. class JSInt:
  49. def __init__(self, value):
  50. self.value = int(value)
  51. def __add__(self, other):
  52. if isinstance(other, JSInt): # int + int -> int (addition)
  53. return JSInt(self.value + other.value)
  54. elif isinstance(other, JSBool): # int + bool -> int (addition)
  55. return self + JSInt(other)
  56. elif isinstance(other, JSString): # int + string -> string (concatenation)
  57. return JSString(self) + other
  58. else:
  59. return NotImplemented
  60. def __sub__(self, other):
  61. if isinstance(other, JSInt): # int - int -> int (subtraction)
  62. return JSInt(self.value - other.value)
  63. elif isinstance(other, (JSBool, JSString)): # int - bool/string -> int (subtraction)
  64. return self - JSInt(other)
  65. else:
  66. return NotImplemented
  67. def __mul__(self, other):
  68. if isinstance(other, JSInt): # int * int -> int (multiplication)
  69. return JSInt(self.value * other.value)
  70. elif isinstance(other, (JSBool, JSString)): # int * bool/string -> int (multiplication)
  71. return self * JSInt(other)
  72. else:
  73. return NotImplemented
  74. def __eq__(self, other):
  75. return isinstance(other, JSInt) and self.value == other.value
  76. def __bool__(self):
  77. return self.value != 0 # Any value other than zero is considered 'true'
  78. def __int__(self):
  79. return self.value
  80. def __str__(self):
  81. return str(self.value)
  82. def __repr__(self):
  83. return 'JSInt({!r})'.format(self.value)
  84. class JSString:
  85. def __init__(self, value):
  86. self.value = str(value)
  87. def __add__(self, other):
  88. if isinstance(other, JSString): # string + string -> string (concatenation)
  89. return JSString(self.value + other.value)
  90. elif isinstance(other, (JSInt, JSBool)): # string + int/bool -> string (concatenation)
  91. return self + JSString(other)
  92. else:
  93. return NotImplemented
  94. def __sub__(self, other):
  95. if isinstance(other, (JSBool, JSInt, JSString)): # string - bool/int/string -> int (subtraction)
  96. return JSInt(self) - JSInt(other)
  97. else:
  98. return NotImplemented
  99. def __mul__(self, other):
  100. if isinstance(other, (JSBool, JSInt, JSString)): # string * bool/int/string -> int (multiplication)
  101. return JSInt(self) * JSInt(other)
  102. else:
  103. return NotImplemented
  104. def __eq__(self, other):
  105. return isinstance(other, JSString) and self.value == other.value
  106. def __bool__(self):
  107. return self.value != '' # Any non-empty string is considered 'true'
  108. def __int__(self):
  109. if self.value == '':
  110. return 0
  111. return int(self.value)
  112. def __str__(self):
  113. return self.value
  114. def __repr__(self):
  115. return 'JSString({!r})'.format(self.value)
  116. _itemModifierToResultMapping = {
  117. (): JSString(''),
  118. (Modifier.plus,): JSInt(0),
  119. (Modifier.negate,): JSBool(False),
  120. #(Modifier.plus, Modifier.plus): syntax error
  121. (Modifier.plus, Modifier.negate): JSInt(0),
  122. (Modifier.negate, Modifier.plus): JSBool(True),
  123. (Modifier.negate, Modifier.negate): JSBool(True),
  124. #(Modifier.plus, Modifier.plus, Modifier.plus): syntax error
  125. #(Modifier.plus, Modifier.plus, Modifier.negate): syntax error
  126. (Modifier.plus, Modifier.negate, Modifier.plus): JSInt(1),
  127. (Modifier.plus, Modifier.negate, Modifier.negate): JSInt(1),
  128. #(Modifier.negate, Modifier.plus, Modifier.plus): syntax error
  129. (Modifier.negate, Modifier.plus, Modifier.negate): JSBool(True),
  130. (Modifier.negate, Modifier.negate, Modifier.plus): JSBool(False),
  131. (Modifier.negate, Modifier.negate, Modifier.negate): JSBool(False),
  132. }
  133. class Item:
  134. def __init__(self, type, modifiers, values = None):
  135. if type not in (Type.group, Type.brackets):
  136. raise ValueError('type must be Type.group or Type.brackets')
  137. iter(modifiers) # Test whether modifiers is an iterable, and let the potential TypeError bubble up
  138. if not all(x in (Modifier.negate, Modifier.plus) for x in modifiers):
  139. raise ValueError('modifiers must be an iterable that can only contain Modifier.negate or Modifier.plus')
  140. if values is not None and type != Type.group:
  141. raise ValueError('values can only be specified for group items')
  142. if type == Type.group and values is None:
  143. raise ValueError('values are required for group items')
  144. self.type = type
  145. self.modifiers = modifiers
  146. self.values = values
  147. def evaluate(self, evaluateFunction = None):
  148. if self.type == Type.group:
  149. if evaluateFunction is None or not callable(evaluateFunction):
  150. raise ValueError('must specify a callable evaluateFunction when evaluating a group item')
  151. return evaluateFunction(self.values, self.modifiers)
  152. else:
  153. try:
  154. return _itemModifierToResultMapping[tuple(self.modifiers)]
  155. except KeyError:
  156. raise EvaluationError('Unrecognised modifier pattern {!r}'.format(self.modifiers))
  157. def __eq__(self, other):
  158. return isinstance(other, Item) and self.type == other.type and self.modifiers == other.modifiers and self.values == other.values
  159. def __repr__(self):
  160. return 'Item({!r}, {!r}{})'.format(self.type, self.modifiers, ', values = {!r}'.format(self.values) if self.values is not None else '')
  161. def parse(s):
  162. '''
  163. Parse expression s into a tree of Items.
  164. Argument: s (string), the expression to evaluate
  165. Returns: tree (list of Items)
  166. '''
  167. itemStack = {0: []}
  168. modifierStack = {0: []}
  169. currentItemStack = itemStack[0]
  170. currentModifierStack = modifierStack[0]
  171. stackLevel = 0
  172. finishedItem = False
  173. pos = 0
  174. length = len(s)
  175. while pos < length:
  176. char = s[pos]
  177. if char == '+':
  178. if pos == 0 or not finishedItem:
  179. finishedItem = False
  180. currentModifierStack.append(Modifier.plus)
  181. # else: addition, nothing to do
  182. elif char == '!':
  183. finishedItem = False
  184. currentModifierStack.append(Modifier.negate)
  185. elif char == '(':
  186. finishedItem = False
  187. stackLevel += 1
  188. currentItemStack = itemStack[stackLevel] = []
  189. currentModifierStack = modifierStack[stackLevel] = []
  190. elif char == ')':
  191. if stackLevel == 0:
  192. raise ParsingError('Encountered ) without matching (')
  193. stackLevel -= 1
  194. currentItemStack = itemStack[stackLevel]
  195. currentItemStack.append(Item(type = Type.group, modifiers = modifierStack[stackLevel], values = itemStack[stackLevel + 1]))
  196. currentModifierStack = modifierStack[stackLevel] = []
  197. finishedItem = True
  198. elif char == '[':
  199. if s[pos + 1] != ']':
  200. raise ParsingError('Invalid byte found at position {}; expected ] but got {}'.format(pos + 1, s[pos + 1]))
  201. # End of modifier sequence
  202. currentItemStack.append(Item(type = Type.brackets, modifiers = currentModifierStack))
  203. currentModifierStack = []
  204. pos += 1 # Skip over closing bracket
  205. finishedItem = True
  206. pos += 1
  207. return itemStack[0]
  208. def evaluate(tree, modifiers = None):
  209. t = map(lambda x: x.evaluate(evaluate), tree)
  210. if len(tree) > 1:
  211. result = functools.reduce(operator.add, t) # Concatenation or addition, but this is all handled in the JS* classes
  212. else:
  213. result = next(t)
  214. if modifiers == [Modifier.plus]:
  215. return JSInt(result)
  216. return result
  217. def crack(url, html):
  218. m = re.search(r'setTimeout\(function\(\)\{\s+var\s+s,t,o,p,b,r,e,a,k,i,n,g,f,\s*(?P<parent>[a-zA-Z]+)=\{"(?P<child>[a-zA-Z]+)":(?P<initialExpression>[^}]+)\};' +
  219. r'\s*t\s*=\s*document\.createElement\(\'div\'\);' +
  220. r'\s*t\.innerHTML="<a href=\'/\'>x</a>";' +
  221. r'\s*t\s*=\s*t\.firstChild\.href;\s*r\s*=\s*t\.match\(/https\?:\\/\\//\)\[0\];' +
  222. r'\s*t\s*=\s*t\.substr\(r\.length\);\s*t\s*=\s*t\.substr\(0,t\.length-1\);' +
  223. r'\s*a\s*=\s*document\.getElementById\(\'jschl-answer\'\);' +
  224. r'\s*f\s*=\s*document\.getElementById\(\'challenge-form\'\);' +
  225. r'\s*;((?P=parent)\.(?P=child)\s*[*+-]=\s*[^;]+\s*;\s*)+a\.value\s*=\s*parseInt\((?P=parent)\.(?P=child),\s*10\)\s*\+\s*t\.length;\s*\';\s*121\'' +
  226. r'\s*f\.action\s*\+=\s*location\.hash;' +
  227. r'\s*f\.submit\(\);' +
  228. r'\s*\},\s*4000\);', html)
  229. if not m:
  230. return None
  231. d = m.groupdict()
  232. operators = {'*': operator.mul, '+': operator.add, '-': operator.sub}
  233. result = evaluate(parse(d['initialExpression']))
  234. for m in re.finditer(d['parent'] + r'\.' + d['child'] + r'\s*(?P<operator>[*+-])=\s*(?P<expression>[^;]+)', html):
  235. result = operators[m.group('operator')](result, evaluate(parse(m.group('expression'))))
  236. domain = re.search(r'^https?://([^/]+)/', url).group(1)
  237. return result + JSInt(len(domain))