Skip to content

Commit cb403b8

Browse files
author
Brent Cook
committed
Land rapid7#6077, initial python meterpreter module support
2 parents d7af7c3 + 9adb2ee commit cb403b8

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/post/meterpreter/extensions/python/tlv'
4+
require 'set'
5+
6+
module Rex
7+
module Post
8+
module Meterpreter
9+
module Extensions
10+
module Python
11+
12+
###
13+
#
14+
# Python extension - gives remote python scripting capabilities on the target.
15+
#
16+
###
17+
18+
class Python < Extension
19+
20+
PY_CODE_TYPE_STRING = 0
21+
PY_CODE_TYPE_PY = 1
22+
PY_CODE_TYPE_PYC = 2
23+
24+
PY_CODE_FILE_TYPES = [ '.py', '.pyc' ]
25+
26+
PY_CODE_FILE_TYPE_MAP = {
27+
'.py' => PY_CODE_TYPE_PY,
28+
'.pyc' => PY_CODE_TYPE_PYC
29+
}
30+
31+
#
32+
# Typical extension initialization routine.
33+
#
34+
# @param client (see Extension#initialize)
35+
def initialize(client)
36+
super(client, 'python')
37+
38+
client.register_extension_aliases(
39+
[
40+
{
41+
'name' => 'python',
42+
'ext' => self
43+
}
44+
])
45+
end
46+
47+
def reset
48+
request = Packet.create_request('python_reset')
49+
client.send_request(request)
50+
51+
return true
52+
end
53+
54+
def import(file, mod_name, result_var)
55+
unless ::File.file?(file)
56+
raise ArgumentError, "File not found: #{file}"
57+
end
58+
59+
ext = ::File.extname(file).downcase
60+
unless PY_CODE_FILE_TYPES.include?(ext)
61+
raise ArgumentError, "File not a valid type: #{file}"
62+
end
63+
64+
code = ::File.read(file)
65+
66+
request = Packet.create_request('python_execute')
67+
request.add_tlv(TLV_TYPE_PYTHON_CODE, code)
68+
request.add_tlv(TLV_TYPE_PYTHON_CODE_LEN, code.length)
69+
request.add_tlv(TLV_TYPE_PYTHON_CODE_TYPE, PY_CODE_FILE_TYPE_MAP[ext])
70+
request.add_tlv(TLV_TYPE_PYTHON_NAME, mod_name) if mod_name
71+
request.add_tlv(TLV_TYPE_PYTHON_RESULT_VAR, result_var) if result_var
72+
73+
run_exec_request(request)
74+
end
75+
76+
#
77+
# Dump the LSA secrets from the target machine.
78+
#
79+
# @return [Hash<Symbol,Object>]
80+
def execute_string(code, result_var)
81+
request = Packet.create_request('python_execute')
82+
request.add_tlv(TLV_TYPE_PYTHON_CODE, code)
83+
request.add_tlv(TLV_TYPE_PYTHON_CODE_TYPE, PY_CODE_TYPE_STRING)
84+
request.add_tlv(TLV_TYPE_PYTHON_RESULT_VAR, result_var) if result_var
85+
86+
run_exec_request(request)
87+
end
88+
89+
private
90+
91+
def run_exec_request(request)
92+
response = client.send_request(request)
93+
94+
result = {
95+
result: response.get_tlv_value(TLV_TYPE_PYTHON_RESULT),
96+
stdout: "",
97+
stderr: ""
98+
}
99+
100+
response.each(TLV_TYPE_PYTHON_STDOUT) do |o|
101+
result[:stdout] << o.value
102+
end
103+
104+
response.each(TLV_TYPE_PYTHON_STDERR) do |e|
105+
result[:stderr] << e.value
106+
end
107+
108+
result
109+
end
110+
111+
end
112+
113+
end; end; end; end; end
114+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: binary -*-
2+
module Rex
3+
module Post
4+
module Meterpreter
5+
module Extensions
6+
module Python
7+
8+
TLV_TYPE_PYTHON_STDOUT = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 1)
9+
TLV_TYPE_PYTHON_STDERR = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 2)
10+
TLV_TYPE_PYTHON_CODE = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 3)
11+
TLV_TYPE_PYTHON_CODE_LEN = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 4)
12+
TLV_TYPE_PYTHON_CODE_TYPE = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 5)
13+
TLV_TYPE_PYTHON_NAME = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 6)
14+
TLV_TYPE_PYTHON_RESULT_VAR = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 7)
15+
TLV_TYPE_PYTHON_RESULT = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 8)
16+
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# -*- coding: binary -*-
2+
require 'rex/post/meterpreter'
3+
4+
module Rex
5+
module Post
6+
module Meterpreter
7+
module Ui
8+
9+
###
10+
#
11+
# Python extension - interact with a python interpreter
12+
#
13+
###
14+
class Console::CommandDispatcher::Python
15+
16+
Klass = Console::CommandDispatcher::Python
17+
18+
include Console::CommandDispatcher
19+
20+
#
21+
# Name for this dispatcher
22+
#
23+
def name
24+
'Python'
25+
end
26+
27+
#
28+
# List of supported commands.
29+
#
30+
def commands
31+
{
32+
'python_reset' => 'Resets/restarts the Python interpreter',
33+
'python_execute' => 'Execute a python command string',
34+
'python_import' => 'Import/run a python file or module'
35+
}
36+
end
37+
38+
def cmd_python_reset(*args)
39+
client.python.reset
40+
print_good('Python interpreter successfully reset')
41+
end
42+
43+
@@python_import_opts = Rex::Parser::Arguments.new(
44+
'-h' => [false, 'Help banner'],
45+
'-f' => [true, 'Path to the file (.py, .pyc), or module directory to import'],
46+
'-n' => [true, 'Name of the module (optional, for single files only)'],
47+
'-r' => [true, 'Name of the variable containing the result (optional, single files only)']
48+
)
49+
50+
def python_import_usage
51+
print_line('Usage: python_import <-f file path> [-n mod name] [-r result var name]')
52+
print_line
53+
print_line('Loads a python code file or module from disk into memory on the target.')
54+
print_line('The module loader requires a path to a folder that contains the module,')
55+
print_line('and the folder name will be used as the module name. Only .py files will')
56+
print_line('work with modules.')
57+
print_line(@@python_import_opts.usage)
58+
end
59+
60+
#
61+
# Import/run a python file
62+
#
63+
def cmd_python_import(*args)
64+
if args.length == 0 || args.include?('-h')
65+
python_import_usage
66+
return false
67+
end
68+
69+
result_var = nil
70+
source = nil
71+
mod_name = nil
72+
73+
@@python_import_opts.parse(args) { |opt, idx, val|
74+
case opt
75+
when '-f'
76+
source = val
77+
when '-n'
78+
mod_name = val
79+
when '-r'
80+
result_var = val
81+
end
82+
}
83+
84+
unless source
85+
print_error("The -f parameter must be specified")
86+
return false
87+
end
88+
89+
if ::File.directory?(source)
90+
files = ::Find.find(source).select { |p| /.*\.py$/ =~ p }
91+
if files.length == 0
92+
fail_with("No .py files found in #{source}")
93+
end
94+
95+
base_name = ::File.basename(source)
96+
unless source.end_with?('/')
97+
source << '/'
98+
end
99+
100+
print_status("Importing #{source} with base module name #{base_name} ...")
101+
102+
files.each do |file|
103+
rel_path = file[source.length, file.length - source.length]
104+
parts = rel_path.split('/')
105+
106+
mod_parts = [base_name] + parts[0, parts.length - 1]
107+
108+
if parts[-1] != '__init__.py'
109+
mod_parts << ::File.basename(parts[-1], '.*')
110+
end
111+
112+
mod_name = mod_parts.join('.')
113+
print_status("Importing #{file} as #{mod_name} ...")
114+
result = client.python.import(file, mod_name, nil)
115+
handle_exec_result(result, nil)
116+
end
117+
else
118+
print_status("Importing #{source} ...")
119+
result = client.python.import(source, mod_name, result_var)
120+
handle_exec_result(result, result_var)
121+
end
122+
123+
end
124+
125+
@@python_execute_opts = Rex::Parser::Arguments.new(
126+
'-h' => [false, 'Help banner'],
127+
'-r' => [true, 'Name of the variable containing the result (optional)']
128+
)
129+
130+
def python_execute_usage
131+
print_line('Usage: python_execute <python code> [-r result var name]')
132+
print_line
133+
print_line('Runs the given python string on the target. If a result is required,')
134+
print_line('it should be stored in a python variable, and that variable should')
135+
print_line('passed using the -r parameter.')
136+
print_line(@@python_execute_opts.usage)
137+
end
138+
139+
#
140+
# Execute a simple python command string
141+
#
142+
def cmd_python_execute(*args)
143+
if args.length == 0 || args.include?('-h')
144+
python_execute_usage
145+
return false
146+
end
147+
148+
code = args.shift
149+
result_var = nil
150+
151+
@@python_execute_opts.parse(args) { |opt, idx, val|
152+
case opt
153+
when '-r'
154+
result_var = val
155+
end
156+
}
157+
158+
result = client.python.execute_string(code, result_var)
159+
160+
handle_exec_result(result, result_var)
161+
end
162+
163+
private
164+
165+
def handle_exec_result(result, result_var)
166+
if result[:result]
167+
print_good("#{result_var} = #{result[:result]}")
168+
elsif result[:stdout].length == 0 and result[:stderr].length == 0
169+
print_good("Command executed without returning a result")
170+
end
171+
172+
if result[:stdout].length > 0
173+
print_good("Content written to stdout:\n#{result[:stdout]}")
174+
end
175+
176+
if result[:stderr].length > 0
177+
print_error("Content written to stderr:\n#{result[:stderr]}")
178+
end
179+
end
180+
181+
end
182+
183+
end
184+
end
185+
end
186+
end
187+

0 commit comments

Comments
 (0)