Tib3rius / AutoRecon

AutoRecon is a multi-threaded network reconnaissance tool which performs automated enumeration of services.
GNU General Public License v3.0
5.17k stars 877 forks source link

Cannot write to terminal after AutoRecon finishes. #1

Closed Tib3rius closed 5 years ago

Tib3rius commented 5 years ago

This is a known issue which appears to be infrequent. Sometimes when AutoRecon finishes running, or the user cancels execution, anything typed into the terminal doesn't get displayed. Commands still appear to get executed.

A workaround is to run the command: reset

digitaloffensive commented 5 years ago

I found by resetting the terminal it corrects it. Just entering reset and enter. Does not seem like you are writing but seemed to fix it for me.

Tib3rius commented 5 years ago

@digitaloffensive yeah that's the workaround. Still not sure what the underlying cause is.

pdelteil commented 5 years ago

Happened to me after the scan finished.

the-c0d3r commented 5 years ago

I think this issue is from colorama, it hooks to the standard input/output/error and it doesn't seem to release it after exit, if you don't call deinit() function. So a solution would be to catch KeyboardInterrupt and do a colorama.deinit().

Source: colorama readme

pdelteil commented 5 years ago

Hey, I just tried deinit() and it didn't work. You can't see what you're typing (different behavior)

What works as a work around is adding:

subprocess.call("reset")

Here

try: loop.run_until_complete(scan_services(loop, semaphore, target)) info('Finished scanning target {byellow}{target.address}{rst}') open(os.path.abspath(os.path.join(basedir, 'completed')), 'a').close() subprocess.call("reset")

I also added a flag/file with name 'completed' to indicate the user that the scan was completed.

I think this issue is from colorama, it hooks to the standard input/output/error and it doesn't seem to release it after exit, if you don't call deinit() function. So a solution would be to catch KeyboardInterrupt and do a colorama.deinit().

Source: colorama readme

the-c0d3r commented 5 years ago

Actually, not seeing what you're typing is a very typical issue with the standard I/O hooking (same thing can happen when you accidentally "cat binary_file"). As I cannot reproduce the problem, I cannot test out my own suggested solution as well. But doing "reset" with subprocesss is just very hacky. I believe it also depends on what terminal emulator you're using, and maybe even with the TERM variable.

pdelteil commented 5 years ago

Actually, not seeing what you're typing is a very typical issue with the standard I/O hooking (same thing can happen when you accidentally "cat binary_file"). As I cannot reproduce the problem, I cannot test out my own suggested solution as well. But doing "reset" with subprocesss is just very hacky. I believe it also depends on what terminal emulator you're using, and maybe even with the TERM variable.

Well..it's a workaround. So far, the only I've found.

Tib3rius commented 5 years ago

@the-c0d3r I tried deinit() but the same issue occurred. I even went as far as removing the entire colorama import and code, but again, same issues. I don't believe colorama is the fault here, or at the very least, it's not entirely the fault.

@pdelteil does the subprocess.call("reset") code blank the entire AutoRecon output though? That's what running reset after AutoRecon finishes does, and I don't think it's a valid workaround because you lose all the output in the scrollback buffer, at least in Kali.

the-c0d3r commented 5 years ago

Let me know what terminal you guys are using, the TERM value, the shell etc, and on what conditions does it trigger. I'll dig around.

the-c0d3r commented 5 years ago

So I just tested the deinit in Kali Linux. Without it, if I send SIGINT, it will cause the issue. But with it, it will not cause any issues.

import colorama
import atexit

def _quit():
    colorama.deinit()

atexit.register(_quit)

Try adding above to between the variables and the import statements. This will solve the issue that occurs when you Ctrl+C. @Tib3rius You are right, the issue does not stem from the colorama itself, but it definitely solve the issue when called. It is not the root cause, but it is the solution. It does not clear the entire scrollback buffer as 'reset' does as well. I can put together a PR if you concur.

I think it's due to the Python's multiprocessing processes does not get killed gracefully, they did not release the STDIN/STDOUT and therefore messes up the parent process's STDIN & STDOUT.


Edit: but you should still try to handle the process that you spawned gracefully. And I think the entire codebase warrants a refactoring. Anything longer than 5 levels of indentation calls for a refactoring, in my opinion.

Tib3rius commented 5 years ago

@the-c0d3r I tested your pull request and it doesn't fix the issue. Still encountering the problem when I Ctrl+C. Also doesn't seem to fix the issue when AutoRecon exits gracefully.

I'm using bash shell, my $TERM is xterm-256color (standard Kali Linux shell).

the-c0d3r commented 5 years ago

I tested the fix on a very new kali linux vm. It seem to have fixed the issue. I didn't had the time to do a very thorough test previously. Now based on your response, I tested again.

It seems the issue will not occur if you "Ctrl+C" before the nmap scanning is completed. But if you do the "Ctrl+C" after seeing the "nmap quick scan completed" message, the same thing will occur.

And this issue does not occur on my linux host, I was wondering why. It was due to my shell being zsh. I changed the shell into zsh inside my kali linux vm, tried to reproduce, it does not occur on Ctrl+C.

Bottom line is, this issue seem to be somewhere between python's multiprocessing and bash's way of handling the stdin/stdout. Would you mind trying out the same thing in zsh shell?

I'll dig around more inside those async and concurrent processes when I have the time.

Tib3rius commented 5 years ago

@the-c0d3r having run AutoRecon a few times in zsh, I'm inclined to agree with your assessment. This seems like there's some shell-specific weirdness going on. Using zsh seems to be a good workaround for now.

Tib3rius commented 5 years ago

@the-c0d3r just wondering if you had played around with this anymore?

the-c0d3r commented 5 years ago

@Tib3rius I have tested a few things, like adding colorama init, deinit, restoring the sys.stdout, sys.stdin, sys.stdout none of them were successful. It appears that your try catch for KeyboardInterrupt did not stop the event loop. I have been trying out things from this article, but it wasn't fruitful. It is quite hard to track down all the possible places where KeyboardInterrupt can occur.

I have never used asyncio or corroutines before. May I ask what's your design consideration for using them? Maybe I can help you write it in another way. I'm more familiar with spawning threads and doing multiprocesses. IMHO, using threads and processes are much more simpler than the asyncio. I normally add try and catch to all child threads/processes, or make it ignore SIGINT and accept it from main thread. Anyways, let me know if you wish to rewrite it, I think I should be able to help out with it.

Tib3rius commented 5 years ago

@the-c0d3r AutoRecon actually uses both multiprocessing and asyncio. Multiple processes are used (one per host) to ensure that multiple cores are used (which threading does not do). Asyncio was chosen because it's a new way of doing "threads" in Python and was recommended. I also found it very simple to handle stdout/stderr compared with regular threads. One of the tools that AutoRecon is based on (bscan) uses asyncio.

My main concern with switching to the old way of doing threads is still being able to handle the stdout/stderr. For example, AutoRecon by default will not show outputs from any of the commands, but will still parse the outputs (line by line) to check for pattern matching (it saves the matches in a file). With -vv, AutoRecon prints every line of output (from every command) to the screen. Any switch to threads must preserve this functionality.

I intend for version 2 of AutoRecon to go in a separate direction though, where instead of a complicated config file, I want a plugins directory where people can write python scripts that run instead (while still having scripts which handle running actual system commands, etc.) Plugins would have access to functions from the main program for doing things like pattern matching, printing, etc. So it may be that version 2 of AutoRecon works better with threads, and we fix this issue then. I also want to switch from a "one host per process" model to a "one process per CPU" model, with a better scheduler that assigns threads to processes regardless of host. This would speed up AutoRecon in a situation where you only have 5 processes, but want to scan 6 hosts. Currently, if the first 5 hosts being scanned have web servers, then nikto ends up being the only task being run on each process, while the 6th host hasn't even been scanned yet, even when you've said "ok, each process can run 10 things concurrently".

VortixDev commented 5 years ago

When caused by Ctrl+C, this seems to be because because the nmap process not terminating correctly. Nmap unsets certain TTY control flags when starting, one of which is responsible for causing input to not be echoed out. When AutoRecon's execution is stopped with SIGINT, it doesn't quit fully and the nmap child processes remain active but become defunct, which it seems isn't sufficient to trigger nmap's function to restore the flags.

I believe killing the processes as part of the interrupt handling will resolve this.

EDIT: Perhaps the backing up and restoration of these flags by nmap could also cause this issue when autorecon finishes successfully. If multiple instances of nmap run and thus one process backs up the flags after they've already been modified by another process, it might also terminate (and thus restore the flags in their modified form) after the TTY flags have already been restored by that other process.

the-c0d3r commented 5 years ago

@VortixDev It seems like your findings sound logical. I never suspected nmap to be overwriting the tty settings. It's impressive how you managed to find the code which does that. What I'm curious is how come zsh can handle this perfectly fine, but bash cannot. And why nmap needs to mess with the terminal from the first place.

If this is the case, how do we solve it from python side on SIGTERM signal? What kind of TTY flag do we need to set?

VortixDev commented 5 years ago

@the-c0d3r Regarding zsh, see this Stack Exchange answer. It seems to be a feature of a module called "zsh line editor" (ZLE), which supposedly resets the ECHO flag. If you turn this off as described in the answer, with unsetopt ZLE, then the problem can be replicated in zsh too.

Regarding nmap, it sets two flags: ICANON and ECHO. I don't know much about the ICANON flag, but the ECHO one controls whether or not input is echoed back out. Presumably, that is disabled to avoid user input from muddling the displayed nmap output. For more detail on the flags, see here.

The best way to handle this is likely to back up the flags before any tools are ran, and to set them back afterwards. The termios library can be used to do this, and example usage can be found on the linked doc page.

the-c0d3r commented 5 years ago

@VortixDev Thanks for the comment. I have tested your approach and it seem to be working fine in #16 I tried unsetopt ZLE as well, it seem to mess up my zsh, autocompletion, ctrl+l clear screen, all those stopped working.

I made it backup the terminal flags on startup, and added exit hook to restore them on exit.