async def get_all_files_meta(request, user): try: files = await db_objects.execute(FileMeta.select()) except Exception as e: return json({'status': 'error', 'error': 'failed to get files'}) return json( [f.to_json() for f in files if f.operation.name in user['operations']])
async def list_all_screencaptures_per_callback(request, user, id): try: callback = await db_objects.get(Callback, id=id) except Exception as e: print(e) return json({'status': 'error', 'error': 'failed to find callback'}) screencapture_paths = [] if callback.operation.name in user['operations']: screencaptures = await db_objects.execute(FileMeta.select().where( FileMeta.path.regexp(".*{}/downloads/.*/screenshots/".format( callback.operation.name)))) for s in screencaptures: if s.task.callback == callback: screencapture_paths.append(s.to_json()) return json({ 'status': 'success', 'callback': callback.id, 'files': screencapture_paths }) else: return json({ 'status': 'error', 'error': 'must be part of that callback\'s operation to see its screenshots' })
async def ws_screenshots(request, ws, user): try: async with aiopg.create_pool(apfell.config['DB_POOL_CONNECT_STRING']) as pool: async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute('LISTEN "newfilemeta";') # BEFORE WE START GETTING NEW THINGS, UPDATE WITH ALL OF THE OLD DATA operation = await db_objects.get(Operation, name=user['current_operation']) files = await db_objects.execute(FileMeta.select().where(FileMeta.operation == operation).order_by(FileMeta.id)) for f in files: if "{}/downloads/".format(user['current_operation']) in f.path and "/screenshots/" in f.path: await ws.send(js.dumps({**f.to_json(), 'callback_id': f.task.callback.id, 'operator': f.task.operator.username})) await ws.send("") # now pull off any new payloads we got queued up while processing old data while True: try: msg = conn.notifies.get_nowait() blob = js.loads(msg.payload) if "{}/downloads/".format(user['current_operation']) in blob['path'] and "/screenshots" in blob['path']: f = await db_objects.get(FileMeta, id=blob['id']) callback_id = f.task.callback.id await ws.send(js.dumps({**f.to_json(), 'callback_id': callback_id, 'operator': f.task.operator.username})) except asyncio.QueueEmpty as e: await asyncio.sleep(2) await ws.send("") # this is our test to see if the client is still there continue except Exception as e: print(e) return finally: pool.close()
async def create_filemeta_in_database(request, user, id): try: operation = await db_objects.get(Operation, name=user['current_operation']) filemeta = await db_objects.get(FileMeta, id=id, operation=operation) except Exception as e: print(e) return json({ 'status': 'error', 'error': 'file does not exist or not part of your current operation' }) status = {'status': 'success'} filemeta.deleted = True try: await db_objects.update(filemeta) except Exception as e: status = {'status': 'error', 'error': str(e)} try: # only remove the file if there's nothing else pointing to it # this could be a payload and the user is just asking to remove the hosted aspect file_count = await db_objects.count( FileMeta.select().where((FileMeta.path == filemeta.path) & (FileMeta.deleted == False))) file_count += await db_objects.count( Payload.select().where((Payload.location == filemeta.path) & (Payload.deleted == False))) if file_count == 0: os.remove(filemeta.path) except Exception as e: pass return json({**status, **filemeta.to_json()})
async def remove_payload(request, puuid, user, from_disk): try: payload = await db_objects.get(Payload, uuid=puuid) except Exception as e: print(e) return json({ 'status': 'error', 'error': 'specified payload does not exist' }) try: payload.deleted = True await db_objects.update(payload) if from_disk == 1 and os.path.exists(payload.location): try: os.remove(payload.location) except Exception as e: print(e) # if we started hosting this payload as a file in our database, we need to remove that as well file_metas = await db_objects.execute( FileMeta.select().where(FileMeta.path == payload.location)) for fm in file_metas: await db_objects.delete(fm) success = {'status': 'success'} return json({**success, **payload.to_json()}) except Exception as e: print(e) return json({'status': 'error', 'error': 'failed to delete payload'})
async def clear_tasks_for_callback_func(data, cid, user): try: callback = await db_objects.get(Callback, id=cid) operation = await db_objects.get(Operation, id=callback.operation) except Exception as e: print(e) return { 'status': 'error', 'error': 'failed to get callback or operation' } tasks_removed = [] if "all" == data['task']: tasks = await db_objects.execute(Task.select().join(Callback).where( (Task.callback == callback) & (Task.status == "submitted")).order_by(Task.timestamp)) elif len(data['task']) > 0: # if the user specifies a task, make sure that it's not being processed tasks = await db_objects.execute( Task.select().where((Task.id == data['task']) & (Task.status == "submitted"))) else: # if you don't actually specify a task, remove the the last task that was entered tasks = await db_objects.execute(Task.select().where( (Task.status == "submitted") & (Task.callback == callback)).order_by(-Task.timestamp).limit(1)) for t in tasks: if operation.name in user['operations']: try: t_removed = t.to_json() # don't actually delete it, just mark it as completed with a response of "CLEARED TASK" t.status = "processed" await db_objects.update(t) # we need to adjust all of the things associated with this task now since it didn't actually happen # find/remove ATTACKTask, TaskArtifact, FileMeta attack_tasks = await db_objects.execute( ATTACKTask.select().where(ATTACKTask.task == t)) for at in attack_tasks: await db_objects.delete(at, recursive=True) task_artifacts = await db_objects.execute( TaskArtifact.select().where(TaskArtifact.task == t)) for ta in task_artifacts: await db_objects.delete(ta, recursive=True) file_metas = await db_objects.execute( FileMeta.select().where(FileMeta.task == t)) for fm in file_metas: os.remove(fm.path) await db_objects.delete(fm, recursive=True) # now create the response so it's easy to track what happened with it response = "CLEARED TASK by " + user['username'] await db_objects.create(Response, task=t, response=response) tasks_removed.append(t_removed) except Exception as e: print(e) return { 'status': 'error', 'error': 'failed to delete task: ' + t.command.cmd } return {'status': 'success', 'tasks_removed': tasks_removed}
async def list_all_screencaptures_per_operation(request, user): if user['current_operation'] != "": operation = await db_objects.get(Operation, name=user['current_operation']) screencaptures = await db_objects.execute(FileMeta.select().where(FileMeta.path.regexp(".*{}/downloads/.*/screenshots/".format(operation.name)))) screencapture_paths = [] for s in screencaptures: screencapture_paths.append(s.to_json()) return json({'status': 'success', 'files': screencapture_paths}) else: return json({"status": 'error', 'error': 'must be part of a current operation to see an operation\'s screencaptures'})
async def get_current_operations_files_meta(request, user): if user['current_operation'] != "": try: operation = await db_objects.get(Operation, name=user['current_operation']) files = await db_objects.execute(FileMeta.select().where(FileMeta.operation == operation)) except Exception as e: return json({'status': 'error', 'error': 'failed to get files'}) return json([f.to_json() for f in files if not "screenshots" in f.path ]) else: return json({"status": 'error', 'error': 'must be part of an active operation'})
async def ws_files_current_operation(request, ws, user): try: async with aiopg.create_pool(apfell.config['DB_POOL_CONNECT_STRING']) as pool: async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute('LISTEN "newfilemeta";') # BEFORE WE START GETTING NEW THINGS, UPDATE WITH ALL OF THE OLD DATA operation = await db_objects.get(Operation, name=user['current_operation']) files = await db_objects.execute(FileMeta.select().where( (FileMeta.operation == operation) & (FileMeta.deleted == False)).order_by(FileMeta.id)) for f in files: if "/screenshots/" not in f.path: if "/{}/downloads/".format(user['current_operation']) not in f.path: # this means it's an upload, so supply additional information as well # two kinds of uploads: via task or manual if f.task is not None: # this is an upload via agent tasking await ws.send(js.dumps( {**f.to_json(), 'host': f.task.callback.host, "upload": f.task.params})) else: # this is a manual upload await ws.send(js.dumps({**f.to_json(), 'host': 'MANUAL FILE UPLOAD', "upload": "{\"remote_path\": \"Apfell\", \"file_id\": " + str(f.id) + "}", "task": "null"})) else: await ws.send(js.dumps({**f.to_json(), 'host': f.task.callback.host, 'params': f.task.params})) await ws.send("") # now pull off any new payloads we got queued up while processing old data while True: try: msg = conn.notifies.get_nowait() id = (msg.payload) f = await db_objects.get(FileMeta, id=id, operation=operation, deleted=False) if "/screenshots" not in f.path: try: if "/{}/downloads/".format(user['current_operation']) not in f.path: # this means it's an upload, so supply additional information as well # could be upload via task or manual if f.task is not None: # this is an upload via gent tasking await ws.send(js.dumps( {**f.to_json(), 'host': f.task.callback.host, "upload": f.task.params})) else: # this is a manual upload await ws.send(js.dumps({**f.to_json(), 'host': 'MANUAL FILE UPLOAD', "upload": "{\"remote_path\": \"Apfell\", \"file_id\": " + str(f.id) + "}", "task": "null"})) else: await ws.send(js.dumps({**f.to_json(), 'host': f.task.callback.host, 'params': f.task.params})) except Exception as e: pass # we got a file that's just not part of our current operation, so move on except asyncio.QueueEmpty as e: await asyncio.sleep(1) await ws.send("") # this is our test to see if the client is still there continue except Exception as e: print(e) continue finally: pool.close()
async def add_task_to_callback_func(data, cid, user): try: # first see if the operator and callback exists op = await db_objects.get(Operator, username=user['username']) cb = await db_objects.get(Callback, id=cid) operation = await db_objects.get(Operation, name=user['current_operation']) original_params = None task = None # now check the task and add it if it's valid and valid for this callback's payload type try: cmd = await db_objects.get( Command, cmd=data['command'], payload_type=cb.registered_payload.payload_type) except Exception as e: # it's not registered, so check the default tasks/clear if data['command'] == "tasks": # this means we're just listing out the not-completed tasks, so nothing actually goes to the agent task = await db_objects.create(Task, callback=cb, operator=op, params=data['command'], status="processed", original_params=data['command']) raw_rsp = await get_all_not_completed_tasks_for_callback_func( cb.id, user) if raw_rsp['status'] == 'success': rsp = "" for t in raw_rsp['tasks']: rsp += "\nOperator: " + t['operator'] + "\nTask " + str(t['id']) + ": " + t['command'] + " " + \ t['params'] + "\nStatus: " + t['status'] await db_objects.create(Response, task=task, response=rsp) return { 'status': 'success', **task.to_json(), 'command': 'tasks' } else: return { 'status': 'error', 'error': 'failed to get tasks', 'cmd': data['command'], 'params': data['params'] } elif data['command'] == "clear": # this means we're going to be clearing out some tasks depending on our access levels task = await db_objects.create( Task, callback=cb, operator=op, params="clear " + data['params'], status="processed", original_params="clear " + data['params']) raw_rsp = await clear_tasks_for_callback_func( {"task": data['params']}, cb.id, user) if raw_rsp['status'] == 'success': rsp = "Removed the following:" for t in raw_rsp['tasks_removed']: rsp += "\nOperator: " + t['operator'] + "\nTask " + str( t['id']) + ": " + t['command'] + " " + t['params'] await db_objects.create(Response, task=task, response=rsp) return {'status': 'success', **task.to_json()} else: return { 'status': 'error', 'error': raw_rsp['error'], 'cmd': data['command'], 'params': data['params'] } # it's not tasks/clear, so return an error return { 'status': 'error', 'error': data['command'] + ' is not a registered command', 'cmd': data['command'], 'params': data['params'] } file_meta = "" # some tasks require a bit more processing, so we'll handle that here so it's easier for the implant if cmd.cmd == "upload": upload_config = js.loads(data['params']) # we need to get the file into the database before we can signal for the callback to pull it down try: # see if we actually submitted "file_id /remote/path/here" if 'file_id' in upload_config and upload_config['file_id'] > 0: f = await db_objects.get(FileMeta, id=upload_config['file_id']) # we don't want to lose our tracking on this file, so we'll create a new database entry file_meta = await db_objects.create( FileMeta, total_chunks=f.total_chunks, chunks_received=f.chunks_received, complete=f.complete, path=f.path, operation=f.operation, operator=op) data['file_updates_with_task'].append(file_meta) elif 'file' in upload_config: # we just made the file for this instance, so just use it as the file_meta # in this case it's already added to data['file_updates_with_task'] file_meta = await db_objects.get(FileMeta, id=upload_config['file']) # now normalize the data for the agent since it doesn't care if it was an old or new file_id to upload data['params'] = js.dumps({ 'remote_path': upload_config['remote_path'], 'file_id': file_meta.id }) except Exception as e: print(e) return { 'status': 'error', 'error': 'failed to get file info from the database: ' + str(e), 'cmd': data['command'], 'params': data['params'] } elif cmd.cmd == "download": if '"' in data['params']: data['params'] = data['params'][ 1: -1] # remove "" around the string at this point if they are there elif cmd.cmd == "screencapture": data['params'] = datetime.datetime.utcnow().strftime( '%Y-%m-%d-%H:%M:%S') + ".png" elif cmd.cmd == "load": try: status = await perform_load_transforms(data, cb, operation, op) if status['status'] == 'error': return { **status, 'cmd': data['command'], 'params': data['params'] } # now create a corresponding file_meta file_meta = await db_objects.create(FileMeta, total_chunks=1, chunks_received=1, complete=True, path=status['path'], operation=cb.operation) data['file_updates_with_task'].append(file_meta) data['params'] = js.dumps({ "cmds": data['params'], "file_id": file_meta.id }) except Exception as e: print(e) return { 'status': 'error', 'error': 'failed to open and encode new function', 'cmd': data['command'], 'params': data['params'] } # now actually run through all of the command transforms original_params = data['params'] step_output = {} # keep track of output at each stage step_output["0 - initial params"] = data['params'] cmd_transforms = await get_commandtransforms_func( cmd.id, operation.name) if cmd_transforms['status'] == 'success': # reload our transforms right before use if we are actually going to do some if len(cmd_transforms['transforms']) > 0: try: import app.api.transforms.utils importlib.reload(sys.modules['app.api.transforms.utils']) except Exception as e: print(e) from app.api.transforms.utils import CommandTransformOperation commandTransforms = CommandTransformOperation() for t in cmd_transforms['transforms']: if data['transform_status'][str( t['order'])]: # if this is set to active, do it try: data['params'] = await getattr( commandTransforms, t['name'])(data['params'], t['parameter']) step_output[str(t['order']) + " - " + t['name']] = data['params'] except Exception as e: print(e) return { 'status': 'error', 'error': 'failed to apply transform {}, with message: {}'. format(t['name'], str(e)), 'cmd': data['command'], 'params': original_params } else: return { 'status': 'error', 'error': 'failed to get command transforms with message: {}'.format( str(cmd_transforms['error'])), 'cmd': data['command'], 'params': original_params } if "test_command" in data and data['test_command']: # we just wanted to test out how things would end up looking, but don't actually create a Task for this # remove all of the fileMeta objects we created in prep for this since it's not a real issuing for update_file in data['file_updates_with_task']: await db_objects.delete(update_file) # we only want to delete the file from disk if there are no other db objects pointing to it # so we need to check other FileMeta.paths and Payload.locations file_count = await db_objects.count( FileMeta.select().where((FileMeta.path == update_file.path) & (FileMeta.deleted == False))) file_count += await db_objects.count(Payload.select().where( (Payload.location == update_file.path) & (Payload.deleted == False))) try: if file_count == 0: os.remove(update_file.path) except Exception as e: pass try: await db_objects.delete(file_meta) file_count = await db_objects.count( FileMeta.select().where((FileMeta.path == file_meta.path) & (FileMeta.deleted == False))) file_count += await db_objects.count( Payload.select().where((Payload.location == file_meta.path) & (Payload.deleted == False))) if file_count == 0: os.remove(file_meta.path) except Exception as e: pass return { 'status': 'success', 'cmd': data['command'], 'params': original_params, 'test_output': step_output } if original_params is None: original_params = data['params'] if task is None: task = await db_objects.create(Task, callback=cb, operator=op, command=cmd, params=data['params'], original_params=original_params) await add_command_attack_to_task(task, cmd) for update_file in data['file_updates_with_task']: # now we can associate the task with the filemeta object update_file.task = task await db_objects.update(update_file) status = {'status': 'success'} task_json = task.to_json() task_json['task_status'] = task_json[ 'status'] # we don't want the two status keys to conflict task_json.pop('status') return {**status, **task_json} except Exception as e: print("failed to get something in add_task_to_callback_func " + str(e)) return { 'status': 'error', 'error': 'Failed to create task: ' + str(e), 'cmd': data['command'], 'params': data['params'] }
async def database_clears(request, user): try: operator = await db_objects.get(Operator, username=user['username']) operation = await db_objects.get(Operation, name=user['current_operation']) if operation.name not in user['admin_operations']: return json({'status': 'error', 'error': "you must be the admin of the operation to clear the database"}) except Exception as e: return json({'status': 'error', 'error': "failed to get the operation and operation: " + str(e)}) data = request.json if 'object' not in data: return json({'status': 'error', 'error': '"object" is a required parameter'}) deleted_obj_info = {'dbnumber': 0} if data['object'] == "payloads": payloads = await db_objects.execute(Payload.select().where(Payload.operation == operation)) for p in payloads: try: os.remove(p.location) # delete it from disk first except Exception as e: print(e) await db_objects.delete(p, recursive=True) # then delete it and everything it relies on from the db deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "callbacks": callbacks = await db_objects.execute(Callback.select().where(Callback.operation == operation)) for c in callbacks: await db_objects.delete(c, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "screencaptures": screencaptures = await db_objects.execute(FileMeta.select().where( (FileMeta.operation == operation) & (FileMeta.path.contains("/screenshots/")) )) for s in screencaptures: try: os.remove(s.path) except Exception as e: print(e) await db_objects.delete(s, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "downloads": downloads = await db_objects.execute(FileMeta.select().where( (FileMeta.operation == operation) & (FileMeta.path.contains("/downloads/")) )) for d in downloads: try: os.remove(d.path) except Exception as e: print(e) await db_objects.delete(d, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 shutil.rmtree("./app/files/{}/downloads".format(operation.name)) # remove the downloads folder from disk elif data['object'] == "uploads": uploads = await db_objects.execute(FileMeta.select().where( (FileMeta.operation == operation) & (FileMeta.path.contains("/{}/".format(operation.name))) & ~(FileMeta.path.contains("/downloads")) )) for u in uploads: try: os.remove(u.path) except Exception as e: print(e) await db_objects.delete(u, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "keylogs": keylogs = await db_objects.execute(Keylog.select().where(Keylog.operation == operation)) for k in keylogs: await db_objects.delete(k, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "credentials": credentials = await db_objects.execute(Credential.select().where(Credential.operation == operation)) for c in credentials: await db_objects.delete(c, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "tasks": callbacks = Callback.select().where(Callback.operation == operation) tasks = await db_objects.prefetch(Task.select(), callbacks) for t in tasks: await db_objects.delete(t, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 elif data['object'] == "responses": callbacks = Callback.select().where(Callback.operation == operation) tasks = Task.select() responses = await db_objects.prefetch(Response.select(), tasks, callbacks) for r in responses: await db_objects.delete(r, recursive=True) deleted_obj_info['dbnumber'] = deleted_obj_info['dbnumber'] + 1 return json({"status": "success", 'stats': deleted_obj_info})