diff --git a/n98-magerun2.yaml b/n98-magerun2.yaml
index e004ceb..7867321 100644
--- a/n98-magerun2.yaml
+++ b/n98-magerun2.yaml
@@ -3,7 +3,9 @@ autoloaders:
commands:
customCommands:
- - Hypernode\Magento\Command\Hypernode\Performance\PerformanceCommand
+ - Hypernode\Magento2\Command\Hypernode\Crack\AdminPasswordCommand
+ - Hypernode\Magento2\Command\Hypernode\Log\ParseLogCommand
+ - Hypernode\Magento2\Command\Hypernode\Performance\PerformanceCommand
passwordCracker:
rulesDirs:
diff --git a/src/Hypernode/Magento2/Command/Hypernode/Crack/AbstractCrackCommand.php b/src/Hypernode/Magento2/Command/Hypernode/Crack/AbstractCrackCommand.php
new file mode 100644
index 0000000..ecff725
--- /dev/null
+++ b/src/Hypernode/Magento2/Command/Hypernode/Crack/AbstractCrackCommand.php
@@ -0,0 +1,469 @@
+userModel = $userModel;
+ $this->storeManager = $storeManager;
+ $this->encryptor = $encryptor;
+ $this->filesystem = $filesystem;
+ }
+
+ protected function configure()
+ {
+ $this
+ ->addArgument('wordlists', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Word list files to use as the base passwords.')
+ ->addOption('active', null, InputOption::VALUE_NONE, 'Include active users in output')
+ ->addOption('inactive', null, InputOption::VALUE_NONE, 'Include inactive users in output')
+ ->addOption('cracked', null, InputOption::VALUE_NONE, 'Return rows successfully cracked')
+ ->addOption('uncracked', null, InputOption::VALUE_NONE, 'Return rows not cracked')
+ ->addOption('engine', null, InputOption::VALUE_REQUIRED, 'Force using specific cracking engine')
+ ->addOption('rulesets', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Mutator rulesets to run against wordlists')
+ ->addOption('usernames', 'u', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Comma seperated list of usernames to filter by')
+ ->addOption('force', 'f', InputOption::VALUE_NONE, 'Skip details confirmation')
+ ->addOption('format', null, InputOption::VALUE_REQUIRED, 'Output format table|csv|json')
+ ;
+ }
+
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+
+ $this->input = $input;
+ $this->output = $output;
+ $this->config = $this->getPluginConfig();
+
+ if ($format = $input->getOption('format')) {
+ if (!in_array($format, array('csv', 'table', 'json'))) {
+ throw new \InvalidArgumentException(
+ sprintf('Invalid format specified [%s].', $format)
+ );
+ }
+ }
+
+ if (extension_loaded('xdebug')) {
+ $output->writeln('The xdebug extension was detected, this will significantly slow down the cracking rate.');
+ }
+
+ foreach ($this->config['wordlistDirs'] as $dir) {
+ $path = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'special.txt';
+ if (file_exists($path)) {
+ $output->writeln(
+ sprintf('The file %s will never be loaded due to "special" being a reserved list name.', $path)
+ );
+ break;
+ }
+ }
+ }
+
+ protected function getInstalledWordlists()
+ {
+ return $this->getInstalledFiles('wordlistDirs', 'txt');
+ }
+
+ protected function getInstalledRules()
+ {
+ return $this->getInstalledFiles('rulesDirs', 'rule');
+ }
+
+ protected function getInstalledFiles($configField, $extension)
+ {
+ $config = $this->getApplication()
+ ->getConfig();
+
+ $files = array();
+ foreach ($config['passwordCracker'][$configField] as $dir) {
+ foreach (glob(rtrim($dir, '/') . '/*.' . $extension) as $ruleFile) {
+ $files[] = basename($ruleFile, '.' . $extension);
+ }
+ }
+
+ return $files;
+ }
+
+ public function getHelp()
+ {
+ $wordlists = implode(', ', $this->getInstalledWordlists());
+ $rules = implode(', ', $this->getInstalledRules());
+
+ return <<Filtering:
+ It is possible to filter which passwords to crack by either status or username. Using the --active
+ and --inactive flags allows filtering on the status. Including both flags is the same as including
+ neither. All admin credentials will be checked. The -u flag allows you to specify one or more
+ usernames to be attempted. Both filter types can be used in conjunction with each other and are
+ additive. Therefore if you use a username for an inactive user in conjunction with the --active flag,
+ the user will not be checked.
+
+ Engine:
+ This command supports two different engines. Hashcat and PHP. By default it will attempt to determine
+ if shell_exec is enabled and if hashcat is installed on the server, if it finds hashcat it will leverage
+ this as the engine as it is many orders of magnitude faster than PHP processing. If hashcat isn't found
+ then it will fall back to pure PHP. Alternatively you can force PHP processing using the --engine=php flag.
+
+ The hashcat engine currently only supports cracking CE passwords. Whilst it's slower, one major advantage
+ of the PHP engine is that it uses the encryption model configured in Magento. This means if you are using
+ EE or a 3rd party replacement due to legacy support or greater security, it can still attempt to brute force
+ your passwords. The speed at which it can achieve this will vary based on the hashing mechanism in use.
+
+ Output:
+ It is possible to filter which values get included in the output by using the --cracked and --uncracked
+ flags. If either is used independantly then only the passwords that are cracked or uncracked will be
+ included in the output. Using both flags is the same as including neither, all row will be output.
+
+ When the --json flag is used, the only information output to stdout will be a JSON object
+ containing information about the cracked password.
+
+ Rulesets:
+ When specifying a ruleset you can either use file system paths or the name of an installed ruleset.
+
+ Installed Rulesets: $rules
+
+ Wordlists:
+ When specifying word lists you can either use file system paths or the name of an installed word list. The
+ word list special represents an automatically generated word list based on the domain names and information
+ about the admin users in the installation.
+
+ Installed Wordlists: $wordlists
+
+HTML;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->detectMagento($output, true);
+ if (!$this->initMagento()) {
+ return;
+ }
+
+ $wordFiles = $this->getWordFiles();
+ $ruleFiles = $this->getRuleFiles();
+ $credentials = $this->getCredentials();
+
+ if (!$this->confirmDetails()) {
+ return;
+ }
+
+ $start = microtime(true);
+
+ $engine = $this->getEngine($wordFiles, $ruleFiles);
+ $results = $engine->crack($credentials);
+ $results = $this->filterOutput($results);
+
+ $this->cleanup();
+
+ $data = array(
+ 'results' => $results,
+ 'time_taken' => (int)(microtime(true) - $start),
+ );
+
+ if ($input->getOption('format') === 'json') {
+ $this->outputJson($data);
+ } elseif ($input->getOption('format') === 'csv') {
+ $this->outputCsv($data);
+ } else {
+ $this->outputTable($data);
+ }
+ }
+
+ protected function getEngine($wordFiles, $ruleFiles)
+ {
+ $input = $this->input;
+ $output = $this->output
+ ->getErrorOutput();
+
+ $factory = new EngineFactory();
+ if ($engineType = $input->getOption('engine')) {
+ $factory->setEngineType($engineType);
+ }
+ $factory->setEncryptor($this->encryptor);
+ $factory->setWordFiles($wordFiles);
+ $factory->setRuleFiles($ruleFiles);
+ $factory->setOutput($output);
+
+ return $factory->getEngine();
+ }
+
+ protected function getPluginConfig()
+ {
+ $config = $this->getApplication()
+ ->getConfig();
+
+ return $config['passwordCracker'];
+ }
+
+ protected function confirmDetails()
+ {
+ if ($this->input->getOption('force')) {
+ return true;
+ }
+
+ $this->output->writeLn('Are you sure?');
+ $this->output->writeLn(' This command can be slow.'.PHP_EOL);
+
+ // @todo - confirm details such as number of users, words and rules.
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion(' Enter y to proceed: ', false);
+
+ return $helper->ask($this->input, $this->output, $question);
+ }
+
+ protected function outputJson($data)
+ {
+ $output = $this->output;
+ $results = array();
+ foreach ($data['results'] as $credential) {
+ $results[] = array(
+ 'user' => $credential->getId(),
+ 'hash' => $credential->getHash(),
+ 'cracked' => $credential->isCracked() ? true : false,
+ 'password' => $credential->getPassword(),
+ );
+ }
+ $data['results'] = $results;
+ $output->write(json_encode($data));
+ }
+
+ protected function outputTable($data)
+ {
+ $output = $this->output;
+ $results = array();
+ foreach ($data['results'] as $credential) {
+ $results[] = array(
+ 'user' => $credential->getId(),
+ 'hash' => $credential->getHash(),
+ 'cracked' => $credential->isCracked() ? 'Yes' : 'No',
+ 'password' => $credential->getPassword(),
+ );
+ }
+ $headings = array('User', 'Hash', 'Cracked', 'Password');
+ $t = $this->getHelper('table');
+ $t->setHeaders($headings)->renderByFormat($output, $results);
+
+ $output->writeln(
+ sprintf(
+ 'Cracking Completed in %s.',
+ $this->humanizeTime($data['time_taken'])
+ )
+ );
+ }
+
+ protected function outputCsv($data)
+ {
+ $output = $this->output;
+ $results = array();
+ foreach ($data['results'] as $credential) {
+ $results[] = array(
+ 'user' => $credential->getId(),
+ 'hash' => $credential->getHash(),
+ 'cracked' => $credential->isCracked() ? '1' : '0',
+ 'password' => $credential->getPassword(),
+ );
+ }
+ $headings = array('User', 'Hash', 'Cracked', 'Password');
+ $t = $this->getHelper('table');
+ $t->setHeaders($headings)->renderByFormat($output, $results, 'csv');
+ }
+
+ protected function getWordFiles()
+ {
+ $wordlists = $this->input
+ ->getArgument('wordlists');
+
+ $special = array_search('special', $wordlists);
+ if ($special !== false) {
+ $filePath = $this->generateSpecialWordlist();
+ $wordlists = array_replace($wordlists, array($special => $filePath));
+ }
+
+ $resolver = new FileResolver($wordlists, $this->config['wordlistDirs'], 'txt');
+ $invalid = $resolver->getInvalidFiles();
+ if (!empty($invalid)) {
+ throw new \InvalidArgumentException(
+ sprintf('Wordlist file(s) not found [%s].', implode(', ', $invalid))
+ );
+ }
+
+ return $resolver->getValidFiles();
+ }
+
+ protected function getRuleFiles()
+ {
+ $usedRuleSets = $this->input->getOption('rulesets');
+
+ $resolver = new FileResolver($usedRuleSets, $this->config['rulesDirs'], 'rule');
+
+ $invalid = $resolver->getInvalidFiles();
+ if (!empty($invalid)) {
+ throw new \InvalidArgumentException(
+ sprintf('Ruleset file(s) not found [%s].', implode(', ', $invalid))
+ );
+ }
+
+ return $resolver->getValidFiles();
+ }
+
+ protected function filterCracked($results, $cracked)
+ {
+ $filteredResults = array();
+ foreach ($results as $result) {
+ if ($result->isCracked() === $cracked) {
+ $filteredResults[] = $result;
+ }
+ }
+
+ return $filteredResults;
+ }
+
+ protected function filterOutput($results)
+ {
+ $input = $this->input;
+ $cracked = $input->getOption('cracked');
+ $uncracked = $input->getOption('uncracked');
+ if ($cracked && ! $uncracked) {
+ return $this->filterCracked($results, true);
+ } elseif ($uncracked && ! $cracked) {
+ return $this->filterCracked($results, false);
+ }
+
+ return $results;
+ }
+
+ protected function generateSpecialWordlist()
+ {
+ $admins = $this->userModel->getCollection();
+ $stores = $this->storeManager->getStores();
+ $generator = new WordlistGenerator();
+ $generator->setAdmins($admins);
+ $generator->setStores($stores);
+ $words = $generator->generate();
+
+ $directoryRead = $this->filesystem->getDirectoryRead('tmp');
+ $dir = $directoryRead->getAbsolutePath('password-cracker');
+
+ $io = new \Varien_Io_File;
+ $io->checkAndCreateFolder($dir);
+
+ $id = uniqid('run-', true);
+ $this->specialPath = $dir . sprintf('special-words-%s.txt', $id);
+ $fh = fopen($this->specialPath, 'w');
+ foreach ($words as $word) {
+ fwrite($fh, $word . PHP_EOL);
+ }
+
+ fclose($fh);
+
+ return $this->specialPath;
+ }
+
+ protected function cleanup()
+ {
+ $path = realpath($this->specialPath);
+ if (file_exists($path)
+ && strpos($path, $this->filesystem->getDirectoryRead('tmp')->getAbsolutePath()) === 0) {
+ unlink($path);
+ }
+ }
+
+ public function humanizeTime($secs)
+ {
+ $secs = (int) $secs;
+ $units = array(
+ 'week' => 7*24*3600,
+ 'day' => 24*3600,
+ 'hour' => 3600,
+ 'minute' => 60,
+ 'second' => 1,
+ );
+
+ // specifically handle zero
+ if ($secs === 0) {
+ return '0 seconds';
+ }
+ if ($secs === 1) {
+ return '1 second';
+ }
+
+ $s = '';
+ foreach ($units as $name => $divisor) {
+ if ($quot = (int)($secs / $divisor)) {
+ $s .= "$quot $name";
+ $s .= (abs($quot) > 1 ? 's' : '') . ', ';
+ $secs -= $quot * $divisor;
+ }
+ }
+
+ return substr($s, 0, -2);
+ }
+
+ protected function applyStatusFilter($collection)
+ {
+ $input = $this->input;
+ $active = $input->getOption('active');
+ $inactive = $input->getOption('inactive');
+ if ($active || $inactive) {
+ $states = array();
+ if ($active) {
+ $states[] = 1;
+ }
+ if ($inactive) {
+ $states[] = 0;
+ }
+ $collection->addFieldToFilter('is_active', array('in' => $states));
+ }
+
+ return $collection;
+ }
+
+ protected function applyUserFilter($collection, $field = 'username')
+ {
+ $input = $this->input;
+ $usernames = $input->getOption('usernames');
+ if (!empty($usernames)) {
+ $usernames = array_map('trim', array_filter($usernames));
+ $collection->addFieldToFilter($field, array('in' => $usernames));
+ }
+ }
+
+ abstract protected function getCredentials();
+}
diff --git a/src/Hypernode/Magento2/Command/Hypernode/Crack/AdminPasswordCommand.php b/src/Hypernode/Magento2/Command/Hypernode/Crack/AdminPasswordCommand.php
new file mode 100644
index 0000000..8809f06
--- /dev/null
+++ b/src/Hypernode/Magento2/Command/Hypernode/Crack/AdminPasswordCommand.php
@@ -0,0 +1,55 @@
+setName('hypernode:crack:admin-passwords')
+ ->setDescription('Attempt to crack admin credentials');
+
+ parent::configure();
+ }
+
+ /**
+ * @return array
+ */
+ protected function getCredentials()
+ {
+ $admins = $this->getAdmins();
+ $credentials = array();
+ foreach ($admins as $admin) {
+ $credentials[] = new Credential($admin->getPassword(), $admin->getUsername());
+ }
+
+ return $credentials;
+ }
+
+ /**
+ * @return \Mage_Admin_Model_Resource_User_Collection
+ */
+ protected function getAdmins()
+ {
+ $admins = $this->userModel->getCollection();
+ $this->applyUserFilter($admins);
+ $this->applyStatusFilter($admins);
+
+ return $admins;
+ }
+}
diff --git a/src/Hypernode/Magento2/Command/Hypernode/Log/ParseLogCommand.php b/src/Hypernode/Magento2/Command/Hypernode/Log/ParseLogCommand.php
new file mode 100644
index 0000000..9b83792
--- /dev/null
+++ b/src/Hypernode/Magento2/Command/Hypernode/Log/ParseLogCommand.php
@@ -0,0 +1,22 @@
+