Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing multiple bugs in credential generation + refactoring #19653

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

Mathiou04
Copy link
Contributor

@Mathiou04 Mathiou04 commented Nov 15, 2024

Fixes #19652

Summary

In this PR, I added various tests on the CredentialCollection class, all of them currently failing and illustrating what I think are bugs.

Issues can be grouped in two categories:

  • those linked to the additional_publics array
  • those linked to the password_spray option

I added some comments on each test explaining what it is currently yielding (instead of the result I think should be expected - let me know if I misunderstood anything).

Refactoring opportunity

You will see that there are many bugs linked to the password_spray option.

It seems that this branch of the code has been copy/pasted from the original branch (that didn't handle password spraying) and adapted.
There was not a lot of tests on this part of the code + the structure is difficult to maintain which probably explains those issues.

The original code branch (without spraying: #each_unfiltered_username_first) is also a bit complex with a lot of duplicated code.

I think there are some opportunities to simplify and clarify the code a lot with some refactoring.

Non-regression tests

For this, I created 2 additional "non-regression" tests that activate all options and show how the credentials should be yielded.
There is one test with password spraying, one without.

I don't know if we want to keep them after the refactoring, but they will surely help.

Also, even if a part of the order in which the credentials are yielded should be "fixed", there are questions around others.
For example:

  • when do we want to yield the credentials generated by the user_as_pass option in case of password_spraying?
  • when do we want to yield credentials generated by the "userpass" file?

Additional question

As a side note, the nil_passwords, blank_passwords and user_as_pass options do not apply to the userpass file.
Is it expected behaviour or should we extend this behaviour to users yielded in the userpass file?

Next steps

  • 1. I first plan to fix the easiest bugs (wrong var names for example) to make more tests pass
  • 2. Then I would tackle the cleaning of the password spraying code branch, so as to make all tests pass.
  • 3. Then in a third commit, I would refactor both branches to simplify the code and reduce the probability to have such bugs in the future (also facilitating maintenance)

let(:password) { nil }
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: username, private: nil),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fails because the collection yields nothing in case of "password spraying".

# REMOVE BEFORE COMMIT currently failing
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "test_public", private: "test_public"),
Copy link
Contributor Author

@Mathiou04 Mathiou04 Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crashes because of a non-existent variable.

# REMOVE BEFORE COMMIT currently failing
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "test_public", private: "test_public"),
Copy link
Contributor Author

@Mathiou04 Mathiou04 Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crashes because of a non-existent variable.

# REMOVE BEFORE COMMIT: this option is ignored currently for additional_publics
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "test_public", private: nil),
Copy link
Contributor Author

@Mathiou04 Mathiou04 Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case yields nothing: nil passwords are ignored in additional_public credential generation. (is there a reason for this?)

Metasploit::Framework::Credential.new(public: "test_public1", private: ""),
Metasploit::Framework::Credential.new(public: "test_public2", private: ""),
Metasploit::Framework::Credential.new(public: "test_public1", private: nil),
Metasploit::Framework::Credential.new(public: "test_public2", private: nil),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails because the password spraying option does not work with additional_publics.
It generates the correct list of credentials, but not in the correct order.

expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "adsf", private: "password"),
Metasploit::Framework::Credential.new(public: "jkl", private: "password"),
Metasploit::Framework::Credential.new(public: "test_public", private: "password"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails because for some reason, in addition to those 3 credentials, it also uses the additional_publics ("test_public") as a password.


# REMOVE BEFORE COMMIT: this yields empty creds (no username, no pass)
specify do
expect { |b| collection.each(&b) }.to yield_successive_args()
Copy link
Contributor Author

@Mathiou04 Mathiou04 Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails because in this case (no username defined!) it generates a set of empty credentials.


specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: username, private: ''),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails because it generates no credential.
But in this case we have a username and are asking for a blank password so it should generate this. (this is the case I chose to describe in the issue)

@Mathiou04 Mathiou04 force-pushed the fix_bugs_in_credentials_collection_enumerator_and_refacto branch from 64aeccc to 4e9d771 Compare November 15, 2024 17:21
@Mathiou04 Mathiou04 force-pushed the fix_bugs_in_credentials_collection_enumerator_and_refacto branch from 4e9d771 to 7cab903 Compare November 15, 2024 17:26
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "asdf", private: ''),
Metasploit::Framework::Credential.new(public: "jkl", private: ''),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fails also because it yields nothing. The given file is completely ignored even if we would like to have the usernames with the blank passwords generated.

Metasploit::Framework::Credential.new(public: "test_public", private: ""),
Metasploit::Framework::Credential.new(public: "test_public", private: "passfile"),
Metasploit::Framework::Credential.new(public: "test_public", private: "test_private")
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently failing because of the various bugs that make the other tests fail.

Metasploit::Framework::Credential.new(public: "userpass_user", private: "userpass_pass"),
Metasploit::Framework::Credential.new(public: "user", private: "test_private"),
Metasploit::Framework::Credential.new(public: "userfile", private: "test_private"),
Metasploit::Framework::Credential.new(public: "test_public", private: "test_private"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently failing because of the various bugs that make the other tests fail.

end
end

context "when every possible option is used" do
Copy link
Contributor Author

@Mathiou04 Mathiou04 Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to create some generic non-regression tests to demonstrate how the credentials should be yielded in case of various user/password data source (in fact in case of all them being active).

The idea is mainly to check that all credentials are present, but also to have the proper order.
I am not sure they should be kept after the fixes/refacto have been made (as there may be some question regarding the order, many are probably valid?) but here they will help demonstrate what we expect.


additional_publics.each do |add_public|
yield Metasploit::Framework::Credential.new(public: add_public, private: add_public, realm: realm, private_type: :password)
end
Copy link
Contributor Author

@Mathiou04 Mathiou04 Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can easily see the potential for a refactoring here as many blocks are almost complete c/p of previous blocks.

@cgranleese-r7 cgranleese-r7 self-assigned this Dec 4, 2024
Copy link
Contributor

@cgranleese-r7 cgranleese-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's still in draft but I thought I'd give it a once over anyway. Looks great and thanks for following up on the last pull request 🚀

Comment on lines 317 to 321
context "when using password spraying" do
let(:password_spray) { true }

# REMOVE BEFORE COMMIT: yields nothings, fails because of bug in method
context "without password" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would your thoughts be on not nesting the contexts here. My thinking is it would make searching for failing tests much harder, as the sting would be broken across multiple lines whereas if it was a single context it would be much easier to find.

Here is where I usually check "best practices" when writing tests, just in case it's useful:
https://www.betterspecs.org/#contexts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok with me 👍
I struggled a bit to find a proper organization of the cases, given the already existing specs.

Do you want to merge only those two, or do you think I should also do the same for the other cases?
(meaning having only 1-level deep contexts)

Copy link
Contributor

@cgranleese-r7 cgranleese-r7 Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to merge only those two, or do you think I should also do the same for the other cases? (meaning having only 1-level deep contexts)

I think having only one context for each test would be better when combined with this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fine with me. I'll change that!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bit painful to do but I agree, once it is done, it is much clearer to read and to debug 🎉

end
end

context "when given a user_file" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming from the comment below you want to have blank_passwords set here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually set up on line 434, but with the nested contexts it is not easy to follow along...
I am wondering if it is better to duplicate setup code so as to have 1-level context where you can properly see all the setup, or be a bit more dry and nest.

Happy to follow what you feel is best.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to duplicate setup code so as to have 1-level context where you can properly see all the setup

I would personally be in favor of that as it is much easier to see the configuration for each test.

@cgranleese-r7 cgranleese-r7 added enhancement rn-enhancement release notes enhancement labels Dec 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement rn-enhancement release notes enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Various bugs when using the PASSWORD_SPRAY option
2 participants