2 # RuCTFe 2014 Exploit Helpers
6 FLAG_SERVER_TIMEOUT = 5
7 TARGET_CONNECT_TIMEOUT = 5
8 TARGET_READ_TIMEOUT = 5
27 HUGE_TIMEOUT = 3600 # workaround for Queue bug
29 def indent(amount, string):
31 return '\n'.join([prefix + line for line in string.split('\n')])
34 class ExploitBase(object):
35 FLAG_REGEX = '^\w{31}=$'
37 class Adapter(logging.LoggerAdapter):
38 def process(self, msg, kwargs):
39 return '[%s] %s' % (self.extra['host'], msg), kwargs
41 def __init__(self, host, port, flag_service=None, **kwargs):
44 self.logger = logging.getLogger('Exploit')
45 self.logger = self.Adapter(self.logger, extra={'host': host, 'port': port})
46 self.flag_pattern = re.compile(self.FLAG_REGEX)
47 self.flag_service = flag_service
49 def submit_flag(self, flag):
50 '''Submit a flag to the flag server.'''
51 if not self.flag_pattern.match(flag):
52 raise Exception('doesn\'t look like a flag: %s' % (repr(flag)))
54 self.flag_service.submit_flag(flag)
56 self.logger.info('flag: %s (not submitted in test mode)', flag)
59 self.logger.debug('starting attack')
62 except socket.timeout:
63 self.logger.warn('socket timeout')
65 except socket.error as e:
66 self.logger.error('socket error (%s.%s): %s',
71 except requests.exceptions.RequestException as e:
72 self.logger.error('request exception: %s', str(e))
74 except Exception as e:
75 self.logger.exception('caught exception: %s.%s', type(e).__module__, type(e).__name__)
81 '''To be implemented by subclasses.'''
85 class ExploitBaseTcp(ExploitBase):
89 except socket.timeout:
90 self.logger.info('connect timeout')
92 except ConnectionRefusedError:
93 self.logger.info('connection refused')
105 '''Establish a connection to the target.'''
106 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
107 self.socket.settimeout(TARGET_CONNECT_TIMEOUT)
108 self.socket.connect((self.host, self.port))
109 self.logger.debug('connected')
110 self.socket.settimeout(TARGET_READ_TIMEOUT)
113 '''Close the target connection.'''
116 def recv_fix(self, length):
117 '''Receive fixed-length data.'''
118 buf = self.socket.recv(length, socket.MSG_WAITALL)
119 if len(buf) != length:
120 raise Exception('preliminary eof')
123 def recv_until(self, magic_str):
124 '''Receive data until a magic string (e.g. prompt) is encountered.'''
126 if not isinstance(magic_str, bytes):
127 magic_str = magic_str.encode('ascii')
129 while matchpos < len(magic_str):
132 if c[0] == magic_str[matchpos]:
139 '''Receive data until eof.'''
142 chunk = self.socket.recv(1024, socket.MSG_WAITALL)
150 self.socket.sendall(buf)
153 '''To be implemented by subclasses.'''
157 class RateLimit(object):
158 def __init__(self, delta=1, initial=False):
159 self.last_update = None
161 self.initial = initial
164 if not self.last_update:
165 self.last_update = time.time()
167 if time.time() > self.last_update + self.delta:
168 self.last_update = time.time()
174 class FlagService(object):
175 class Thread(threading.Thread):
176 def __init__(self, service, thread_no):
177 threading.Thread.__init__(self)
179 self.service = service
180 self.thread_no = thread_no
183 if self.thread_no == 0:
184 self.service.progress()
186 def submit_flag(self, flag):
187 self.service.logger.debug('submitting flag: %s', flag)
188 service_name = 'ctfutil-%s' % (sys.argv[0])
190 r = requests.get('http://10.0.1.10/submit.php',
191 params={'flag': flag,
192 'service': service_name},
193 timeout=FLAG_SERVER_TIMEOUT,
195 if r.status_code == 200:
196 self.service.record_submitted()
198 self.service.logger.error('error %d', r.status_code)
199 except requests.exceptions.RequestException as e:
200 self.service.logger.error('caught exception: %s', str(e))
207 item = self.service.flag_queue.get(timeout=1)
215 self.submit_flag(item)
216 self.service.flag_queue.task_done()
218 self.service.flag_queue.task_done()
221 self.logger = logging.getLogger('FlagService')
222 self.flag_queue = queue.Queue(500)
223 self.threads = [self.Thread(self, i) for i in range(FLAG_THREADS)]
224 self.periodic_update = RateLimit(1, True)
225 self.lock = threading.Lock()
226 self.flags_submitted = 0
229 for thread in self.threads:
232 def submit_flag(self, flag):
233 self.flag_queue.put(flag, HUGE_TIMEOUT)
235 def record_submitted(self):
237 self.flags_submitted += 1
240 if not self.periodic_update.step():
244 '%d flags submitted, %d pending' % (
245 self.flags_submitted,
246 self.flag_queue.qsize()))
247 self.flags_submitted = 0
250 class ParallelAttack(object):
251 class Thread(threading.Thread):
252 def __init__(self, attack):
253 threading.Thread.__init__(self)
259 item = self.attack.queue.get()
262 kwargs = self.attack.kwargs.copy()
264 exploit = self.attack.exploit_class(**kwargs)
265 status = exploit.run_catch()
266 self.attack.record_status(item, status)
267 self.attack.queue.task_done()
269 self.attack.queue.task_done()
271 def __init__(self, exploit_class, nthreads, subservice, **kwargs):
272 self.logger = logging.getLogger('ParallelAttack')
273 self.queue = queue.Queue()
274 self.flag_service = FlagService()
275 self.exploit_class = exploit_class
276 self.nthreads = nthreads
277 self.subservice = subservice
280 self.kwargs['flag_service'] = self.flag_service
282 self.lock = threading.Lock()
287 self.logger.info('waiting for next tick')
290 def record_status(self, workitem, status):
291 host = workitem['host']
293 oldstatus = self.hoststatus.get(host)
294 self.hoststatus[host] = status
297 self.statushist[oldstatus] -= 1
298 self.statushist[status] = self.statushist.get(status, 0) + 1
300 def status_summary(self):
302 hist = self.statushist
303 order = ['success', 'timeout', 'down', 'error', 'pending']
304 for key in hist.keys():
307 elems = ["%d %s" % (hist.get(key, 0), key) for key in order]
308 self.logger.info('status: %s', ', '.join(elems))
310 def wait_for_queue(self):
311 rl = RateLimit(1, True)
314 self.status_summary()
316 n_pending = self.statushist['pending']
321 self.status_summary()
324 self.logger.info('starting parallel attack on %d.{0..%d}.attack with ' +
325 '%d threads', self.subservice, ATTACK_NHOSTS-1, self.nthreads)
327 self.flag_service.start()
330 for i in range(self.nthreads):
331 thread = self.Thread(self)
332 self.threads.append(thread)
337 self.round_start = time.time()
339 for i in range(0, ATTACK_NHOSTS):
340 item = {'host': '%d.%d.attack' % (self.subservice, i)}
341 self.record_status(item, 'pending')
342 self.queue.put(item, timeout=HUGE_TIMEOUT)
344 self.wait_for_queue()
347 except KeyboardInterrupt:
348 self.logger.warn('interrupted, shutting down')
350 for i in range(len(self.threads)):
351 self.queue.put(None, timeout=HUGE_TIMEOUT)
355 for thread in self.threads:
359 class Formatter(logging.Formatter):
360 def format(self, record):
361 return logging.Formatter.format(self, record)
363 def formatException(self, exc_info):
364 exc_str = '\n'.join([' '*4 + line for line in traceback.format_exception(*exc_info)])
368 class AttackTool(object):
369 def __init__(self, exploit_class, **kwargs):
370 self.exploit_class = exploit_class
371 self.logger = logging.getLogger('AttackTool')
374 def init_logging(self):
375 streamHandler = logging.StreamHandler()
376 streamHandler.setFormatter(Formatter(
377 # '%(asctime)s [%(levelname)-5s %(filename)s:%(lineno)-3d] %(message)s',
378 '%(asctime)s %(levelname)-7s %(name)-14s %(message)s',
380 rootLogger = logging.getLogger()
381 rootLogger.addHandler(streamHandler)
382 rootLogger.setLevel(logging.INFO)
384 logging.getLogger('urllib3.connectionpool').setLevel(logging.WARN)
386 def parse_args(self):
387 parser = argparse.ArgumentParser(usage='%(prog)s [options...] [-a|-t HOST]')
388 parser.add_argument('-p', '--port', metavar='PORT', type=int, required=True)
389 parser.add_argument('-v', '--verbose',
390 help='increase logging level to DEBUG',
391 action='store_true', dest='debug')
392 parser.add_argument('--threads', metavar='N', type=int,
393 default=ATTACK_THREADS,
394 help='number of threads for --attack-all')
395 g = parser.add_mutually_exclusive_group(required=True)
396 g.add_argument('-a', '--attack-all',
397 help='attack all hosts',
398 action='store_true', dest='attack_all')
399 g.add_argument('-t', '--test', metavar='HOST', type=str,
400 help='attack a single host',
401 action='append', dest='targets')
403 return parser.parse_args()
408 opts = self.parse_args()
410 self.kwargs['port'] = opts.port
413 rootLogger = logging.getLogger()
414 rootLogger.setLevel(logging.DEBUG)
417 attack = ParallelAttack(exploit_class=self.exploit_class,
418 nthreads=opts.threads,
422 for target in opts.targets:
423 self.logger.info('test-attacking %s', target)
424 kwargs = self.kwargs.copy()
425 kwargs['host'] = target
426 exploit = self.exploit_class(**kwargs)
427 status = exploit.run()
428 self.logger.info('status: %s', status)