⚠️ Urgent! Active Attacks on MODX Revolution Sites Below Revolution 2.6.5
Subscribe: RSS
  • Okay,

    I’ve been playing with making it possible to edit, update and manage blogs (and other documents later) using the IXR_Class for XML-RPC.

    This is the most common xmlrpc server for blogs including Wordpress.

    I had started playing with files from this thread: http://modxcms.com/forums/index.php/topic,5827.msg42532.html#msg42532

    The problem was that it wasn’t complete nor could it utilize the modx API and DBAPI from within modx.

    What I have worked out is the following:

    1. Create a folder in /assets/snippets/ called xmlrpc
    2. Upload files attached below to the folder.
    3. Create a snippet called xmlrpc with the following code:
    <?php
    /* -------------------------------------------------------------
    :: Snippet: xmlrpc
    ----------------------------------------------------------------
      Short Description: 
            XML-RPC Server based on Incutio XML-RPC Server
    ---------------------------------------------------------------- */
    
    // xmlrpc version being executed
    define('XR_VERSION', '0.0.1');
    
    // Path where xmlrpc is installed
    define('XR_SPATH', 'assets/snippets/xmlrpc/');
    
    //==============================================================================
    //include snippet file
    define ('XR_PATH', $modx->config['base_path'].XR_SPATH);
    
    $output = "";
    include(XR_PATH.'xmlrpc.inc.php');
    
    return $output;
    ?>


    4. Create a document in modx, insert the following snippet call: [!xmlrpc!], change the template to blank and publish the document
    5. Using Scribefire, MarsEdit or Qumana try to connect to the blog.

    It doesn’t work--yet. I need to complete cleaning the sql queries out to use the modx api since it will be cleaner and be easier to migrate for new versions.

    I need help making sure the logic is working.

    I need help to troubleshoot the logging mechanism.

    Here is the xmlrpc.inc.php code (included in the zip):
    <?php
    # fix for mozBlog and other cases where '<?xml' isn't on the very first line
    $HTTP_RAW_POST_DATA = trim($HTTP_RAW_POST_DATA);
    
    
    
    //Get our XML-RPC bits going
    require ("class/class-IXR.php"); //In Same Dir as XMLRPC - namely "/" or  $_SERVER["DOCUMENT_ROOT"]
    
    // Turn off all warnings and errors.
    // error_reporting(0);
    
    $post_default_title = ''; // posts submitted via the xmlrpc interface get that title
    $post_default_category = 1; // posts submitted via the xmlrpc interface go into that category
    
    $xmlrpc_logging = 1;
    
    function logIO($io,$msg) {
    	global $xmlrpc_logging;
    	if ($xmlrpc_logging) {
    		$fp = fopen("xmlrpc.log","a+");
    		$date = gmdate("Y-m-d H:i:s ");
    		$iot = ($io == "I") ? " Input: " : " Output: ";
    		fwrite($fp, "\n\n".$date.$iot.$msg);
    		fclose($fp);
    	}
    	return true;
    	}
    
    function starify($string) {
    	$i = strlen($string);
    	return str_repeat('*', $i);
    }
    
    logIO("I", $HTTP_RAW_POST_DATA);
    
    
    function mkdir_p($target) {
    	// from php.net/mkdir user contributed notes 
    	if (file_exists($target)) {
    	  if (!is_dir($target)) {
    	    return false;
    	  } else {
    	    return true;
    	  }
    	}
    
    	// Attempting to create the directory may clutter up our display.
    	if (@mkdir($target)) {
    	  return true;
    	}
    
    	// If the above failed, attempt to create the parent node, then try again.
    	if (mkdir_p(dirname($target))) {
    	  return mkdir_p($target);
    	}
    
    	return false;
    }
    
    
    class modx_xmlrpc_server extends IXR_Server {
    
    	function modx_xmlrpc_server() {
    		$this->methods = array(
    		  // Blogger API
    		  'blogger.getUsersBlogs' => 'this:blogger_getUsersBlogs',
    		  'blogger.getUserInfo' => 'this:blogger_getUserInfo',
    		  'blogger.getPost' => 'this:fake',//'this:blogger_getPost',
    		  'blogger.getRecentPosts' =>  'this:blogger_getRecentPosts',
    		  'blogger.getTemplate' =>  'this:fake',//'this:blogger_getTemplate',
    		  'blogger.setTemplate' =>  'this:fake',//'this:blogger_setTemplate',
    		  'blogger.newPost' =>  'this:fake',//'this:blogger_newPost',
    		  'blogger.editPost' =>  'this:fake',//'this:blogger_editPost',
    		  'blogger.deletePost' =>  'this:fake',//'this:blogger_deletePost',
    
    		 // MetaWeblog API (with MT extensions to structs)
    		  'metaWeblog.newPost' => 'this:mw_newPost',
    		  'metaWeblog.editPost' => 'this:mw_editPost',
    		  'metaWeblog.getPost' => 'this:mw_getPost',
    		  'metaWeblog.getRecentPosts' => 'this:blogger_getRecentPosts',
    		  'metaWeblog.getCategories' => 'this:mw_getCategories',
    		  'metaWeblog.newMediaObject' => 'this:mw_newMediaObject',
    
    		  // MetaWeblog API aliases for Blogger API
    		  // see http://www.xmlrpc.com/stories/storyReader$2460
    		  'metaWeblog.deletePost' => 'this:blogger_deletePost',
    		  'metaWeblog.getTemplate' => 'this:blogger_getTemplate',
    		  'metaWeblog.setTemplate' => 'this:blogger_setTemplate',
    		  'metaWeblog.getUsersBlogs' => 'this:blogger_getUsersBlogs',
    
    		  // MovableType API
    		  'mt.getCategoryList' => 'this:blogger_getUsersBlogs',//'this:mt_getCategoryList',
    		  'mt.getRecentPostTitles' => 'this:blogger_getRecentPosts',//'this:mt_getRecentPostTitles',
    		  'mt.getPostCategories' => 'this:mt_getPostCategories',
    		  'mt.setPostCategories' => 'this:mt_setPostCategories',
    		  'mt.supportedMethods' => 'this:mt_supportedMethods',
    		  'mt.supportedTextFilters' => 'this:mt_supportedTextFilters',
    		  'mt.getTrackbackPings' => 'this:mt_getTrackbackPings',
    		  'mt.publishPost' => 'this:mt_publishPost',
    
    		  // PingBack
    		  'pingback.ping' => 'this:pingback_ping',
    		  'pingback.extensions.getPingbacks' => 'this:pingback_extensions_getPingbacks',
    
    		  'demo.sayHello' => 'this:sayHello',
    		  'demo.addTwoNumbers' => 'this:addTwoNumbers'
    		);
    		//$this->methods = apply_filters('xmlrpc_methods', $this->methods);
    		$this->IXR_Server($this->methods);
    	}
    
    	function sayHello($args) {
    		return 'Hello!';
    	}
    
    	function addTwoNumbers($args) {
    		$number1 = $args[0];
    		$number2 = $args[1];
    		return $number1 + $number2;
    	}
    
    //Check login user/pass against modx 
    
    function fake($user_login, $user_pass) {
    	return;
    }
    
    
    function user_pass_ok($user_login, $user_pass) {
    	logIO("I", "in user_pass_ok\n");
    
    	global $dbase,$table_prefix, $modx;
    
    	//Safely escape our user/pass so no evil stuff happens.
    	$user_login = $modx->db->escape($user_login);
    	$user_pass = $modx->db->escape($user_pass);
    
    	logIO("I", "after escape logins\n");
    
    	//Somewhere in here we want to get the user id of the user of the person attempting to login
    	//and if the username exists check the password
    	//and if the password is a match get the records for the user
    	
    	//Get check admin exists based on user_login and get id and password
    	
    	$ufields = "*";
    	$ufrom = $modx->db->config['table_prefix']."manager_users";
    	$uwhere = "`username` = '$user_login'";
    
    	$fetch = $modx->db->getRow($modx->db->select($ufields, $ufrom, $uwhere));
    	$fuser = $fetch['username'];
    	$fpass = $fetch['password'];
    	$fid = $fetch['id'];
    	
        $ua_fields = "*" ;
    	$ua_from = $modx->db->config['table_prefix']."user_attributes";
    	$ua_where = "`id` = '$fid'";
        $user_atribs = $modx->db->getRow($modx->db->select($ua_fields, $ua_from, $ua_where));
    
    	logIO("I", "after db\n");
    
    
    	//U-oh, either no such user or >1 user.  Shouldn't happen...
    	if($user_login != $fuser) {
    			logIO("I","Bad User:".$user_login);
    			$this->error = new IXR_Error(403, 'Bad login/pass combination.');
    			return (FALSE);
    	}
    
    	$row = mysql_fetch_assoc($rs);
    	$id			= $row['id'];
    	$dbasePassword 			= $fetch['password'];
    	$failedlogins 			= $row['failedlogincount'];
    	$blocked 				= $row['blocked'];
    	$blockeduntildate		= $row['blockeduntil'];
    	$blockedafterdate		= $row['blockedafter'];
    
    	logIO("I","Good User:".$user_login."\n");
    
    	if($dbasePassword != md5($user_pass)) {
    				//Bad Password
    				logIO("I","Bad Pass:".md5($user_pass));
    				$this->error = new IXR_Error(403, 'Bad login/pass combination.');
    				return (FALSE);
    		}
    
    	//Lazy - ignore bad login and failed login for now
    
    		logIO("I","ID is:".$id);
    		return $id;
    	}
    
    
    	function login_pass_ok($user_login, $user_pass) {
    	  $result=$this->user_pass_ok($user_login, $user_pass);
    	  if (!$result) {
    	    $this->error = new IXR_Error(403, 'Bad login/pass combination.');
    		logIO("I","Bad Pass:".$md5($user_pass));
    	    return false;
    	  }
    	  return $result;
    	}
    
    
    
    
    	/* Blogger API functions
    	 * specs on http://plant.blogger.com/api and http://groups.yahoo.com/group/bloggerDev/
    	 */
    
    
    	/* blogger.getUsersBlogs will make more sense once we support multiple blogs */
    	function blogger_getUsersBlogs($args) {
    		
    	global $dbase,$table_prefix;
    	  $user_login = $args[1];
    	  $user_pass  = $args[2];
    
    	  $result=$this->user_pass_ok($user_login, $user_pass);
    
    	  if (!$result) {
    		
    	    return $this->error;
    	  }
    	 logIO("I","in getUsersBlogs");
    	  //Get site_name
    	  $sql = "SELECT setting_value from ". $dbase. "." .$table_prefix."system_settings where setting_name = 'site_name'";
    	  $rs = mysql_query($sql);
    	  $limit = mysql_num_rows($rs);
    	  logIO("I","after db query:".$sql."\n");
    	  if ($limit==0) {
    		$site_name = "[Unknown Site Name";
    	  }
    	  else {
    		  $row = mysql_fetch_assoc($rs);
    		  $site_name = $row['setting_value'];
    	  }
    	  logIO("I","SiteName:".$site_name."\n");
    	  //Is admin user?
    	 /* $user_data = $modx->getUserInfo($result);
    	  if ($user_data['usertype']=="manager") {
    		  $isAdmin=1;
    	  } 
    	  else {
    		  $isAdmin=0;
    	  }
    	 logIO("I","site name:".$site_name."\n");
    	 logIO("I","Is Admin:".$isAdmin."\n");*/
    
    	  //Get the users current folder list for xxx
    	  $struct = array(
    	            'isAdmin'  => $isAdmin, //is admin?
    	            'url'      => $site_name,
    	            'blogid'   => '1', //NA ...
                    'blogName' => 'blogs name' //(id home page name)
     	  );
    	
    	  return array($struct);
    	}
    
    
    	/* blogger.getUsersInfo gives your client some info about you, so you don't have to */
    	function blogger_getUserInfo($args) {
    	  global $modx;
    
    	   logIO("I","in getUserInfo\n");
    	  $user_login = $args[1];
    	  $user_pass  = $args[2];
    
    	   $result=$this->user_pass_ok($user_login, $user_pass);
    
    	  if (!$result) {
    	    return $this->error;
    	  }
    
    
    	  $user_data = $modx->getUserInfo($result);
    
    	  $struct = array(
    	    'nickname'  => $user_data->user_nickname,
    	    'userid'    => $user_data->ID,
    	    'url'       => $user_data->user_url,
    	    'email'     => $user_data->user_email,
    	    'lastname'  => $user_data->user_lastname,
    	    'firstname' => $user_data->user_firstname
    	  );
     
    	  return $struct;
    	}
    
    function mysql2date($dateformatstring, $mysqlstring, $translate = true) {
           $m = $mysqlstring;
          if ( empty($m) ) {
               return false;
            }
            $i = mktime(substr($m,11,2),substr($m,14,2),substr($m,17,2),substr($m,5,2),substr($m,8,2),substr($m,0,4));
        
            if( 'U' == $dateformatstring )
                return $i;
            
            if ( -1 == $i || false == $i )
                $i = 0;
    
            $j = @date($dateformatstring, $i);
            if ( !$j ) {
            // for debug purposes
            //    echo $i." ".$mysqlstring;
            }
            return $j;
    }
    
    function modx_get_recent_posts($num = 10) {
       global $dbase,$table_prefix, $modx;
    
        // Set the limit clause, if we got a limit
        if ($num) {
            $limit = "LIMIT $num";
        }
    	
        $sql = "SELECT id as ID, pagetitle as post_title, createdon as post_date, content as post_content, createdby as post_author FROM $dbase.".$table_prefix."site_content WHERE published = '1' ORDER BY post_date DESC $limit";
        
    	logIO("I","$sql");
    	
    	$count=0;
    	$rs = mysql_query($sql);
    	if(@mysql_num_rows($rs)) {
    		while($row=mysql_fetch_array($rs)) {
    				$result[]=$row;$count++;
    		}
    	}
    	logIO("I","modx_grp:$count.");
    	return $result;
        //return $result?$result:array();
    }
    
    
    
    	function blogger_getRecentPosts($args) {
    
    		  $blog_ID    = $args[1]; /* though we don't use it yet */
    		  $user_login = $args[2];
    		  $user_pass  = $args[3];
    		  $num_posts  = $args[4];
    
    		  $result=$this->user_pass_ok($user_login, $user_pass);
    
    		  if (!$result) {
    		    return $this->error;
    		  }
    		 logIO("I","in getRecentPosts");
    
    		 $posts_list = $this->modx_get_recent_posts($num_posts);
    
    		  if (!$posts_list) {
    			$this->error = new IXR_Error(500, 'Either there are no posts, or something went wrong.');
    			return $this->error;
    		  }
    		  $count=0;
    		  logIO("I","blogger_grp:pre posts list.");
    		  foreach ($posts_list as $entry) {
    				$count++;  logIO("I",$count);
    
    				$post_date = $this->mysql2date('Ymd\TH:i:s', $entry['post_date']);
    				//$categories = implode(',', modx_get_post_categories($entry['ID']));
    
    				$content  = '<title>'.stripslashes($entry['post_title']).'</title>';
    				$content .=  '<category>0</category>'; //  '<category>'.$categories.'</category>';
    				$content .= stripslashes($entry['post_content']);
    
    				$struct[] = array(
    					  'userid' => $entry['post_author'],
    					  'dateCreated' => new IXR_Date($post_date),
    					  'content' => $content,
    					  'postid' => $entry['ID'],
    				);
    				logIO("I","inforeach$count");
    		  }
    		  
    
    		  $recent_posts = array();
    		  for ($j=0; $j<count($struct); $j++) {
    			array_push($recent_posts, $struct[$j]);
    		  }
    		 logIO("I","here");
    		  return $recent_posts;
    		}
    
    }
    
    $modx_xmlrpc_server = new modx_xmlrpc_server();
    
    ?>
    


    If anyone wants to help bring this along my PHP skills are pretty sad but this is a very important piece of modx that could have huge applications for all end users. Think of not having to use the admin panel at all to edit an article?

      Author of zero books. Formerly of many strange things. Pairs well with meats. Conversations are magical experiences. He's dangerous around code but a markup magician. BlogTwitterLinkedInGitHub
    • Personally, I have no problem with using the Manager, nor have any of my clients to date. Actually, it seems to make them more aware of what they’re doing and pay more attention if they have to use the manager.

      Then there’s QuickEdit, as well as FDM and NewsManager.

      What, exactly, is the advantage of this?
        Studying MODX in the desert - http://sottwell.com
        Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
        Join the Slack Community - http://modx.org
      • Susan, Most blog software apps including, Wordpress, Blogger, MoveableType, MetaWeblog and others allow for the remote editing of documents from a desktop app. Personally, I’ve used these apps vs the web interface as they offer additional features specific to blogging as well as many apps like Qumana, DeskEditor, Mars Edit and scribefire all run as a native desktop app.

        One of the biggest benefits for the end user is if they use modx on multiple sites they can manage blogs from on interface instead of having to go to website1.com, website2.com and website3.com you just go to your editor and choose the site and write or edit and done.

        The XML RPC is also has the potential for connection from other sites such as social media update sites and the like but all using a single common interface.

        There are other applications for XMLRPC but this is one specific set.

        It (not the IXR Class Server) is natively supported in Revolution including a JSON flavour as well.
          Author of zero books. Formerly of many strange things. Pairs well with meats. Conversations are magical experiences. He's dangerous around code but a markup magician. BlogTwitterLinkedInGitHub
        • Ah, OK, I didn’t catch that this was for using from a desktop application.
            Studying MODX in the desert - http://sottwell.com
            Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
            Join the Slack Community - http://modx.org
          • Yeah,

            It isn’t to replace the manager. This is all part of me "evil" plan to build out a complete blogging solution to compare with WP that offers every main feature from editing, management to pings (although this is less relevant), pingbacks, and comment antispam as well as db independent version control and author workflow.

            For 2 years now I have just integrated modx designs with a WP install but I there is no reason modx can’t do everything WP can. To that end the modx BlogKit will be developed.

            This is part and parcel of that.

            There are also other reasons to implement XMLRPC as well as it allows a simple mechanism for people to create social networking sites where people can cross connect blogs and sites in creative ways.
              Author of zero books. Formerly of many strange things. Pairs well with meats. Conversations are magical experiences. He's dangerous around code but a markup magician. BlogTwitterLinkedInGitHub
            • Jay, first lets distinguish XMLRPC from the various XMLRPC-based blogging API’s because they are not one and the same. XMLRPC is a protocol for remote procedure calls using XML. To implement a Blogger API with XMLRPC is what I think you are trying to do here, correct? I just want to make sure everyone understands that the protocol used to interact with the MODx API is an XMLRPC implementation of the Blogger API, not XMLRPC itself. You can implement any API via XMLRPC. That’s what it is for.

              However, this is going to be a critical interaction point with all kinds of potential security vulnerabilities waiting to get exploited. IOW, this is not a trivial part of code that sends a few queries to MODx and returns a response. It’s like a gateway into the guts of it and as such, is very critical in terms of security. This is one of the most exploited parts of WordPress and one of the more common reasons why you have to upgrade WP every couple of weeks it seems like to avoid hackers wreaking havoc on your blog.

              I’m not saying this to discourage your implementation, just so you are aware that a) XMLRPC is a protocol in and of itself that could be useful in MODx for any number of purposes, not just implementing the Blogger API and that b) we should be using an XMLRPC protocol library that can be reused, not writing the implementation of the Blogger API into the XMLRPC code.
              • Quote from: smashingred at Sep 23, 2008, 12:22 PM

                pingbacks

                This sounds quite interesting because most blog’s and forum’s have pingbacks these days!
                  with regards,

                  Ronald Lokers
                  'Front-end developer' @ h2o Media

                • I agree with your points Jason. I don’t want to blur the line between XMLRPC and implementing various blog APIs via XMLRPC.

                  Due to my non programmer mindset, I find it hard to express what it is I want to accomplish.

                  Here it is in plain english:

                  I want to build out a complete blogging solution for modx.
                  I personally prefer to use desktop apps to write my blog posts as well as store them locally for editing.
                  I don’t want to open modx to major vulnerabilities but I do want to open it to use.
                  I’m not limiting it to just accessing the Blogger API but WP, MT and others as well to enable people to post and edit to modx in the ways they want.

                  If that can be accomplished using the XMLPRC library that you are using for modx for easier migration and more security by all means point me in the right direction and I’ll do that.

                  I’d much rather do it right than quick.

                  Any help anyone can offer in my endeavour would be appreciated.
                    Author of zero books. Formerly of many strange things. Pairs well with meats. Conversations are magical experiences. He's dangerous around code but a markup magician. BlogTwitterLinkedInGitHub
                  • Thanks for trying to implement this, Jay!

                    I for one am drawn to use WP more now because it allows management through the iPhone (due to XMLRPC). It would be incredible if this was available for MODx, as well.
                    • ER, thanks. I need support though. I am no programmer and so making this work and troubleshooting makes my head hurt and takes 10 times as long.

                      I am hoping I can get a volunteer or two to help who has php on the brain. I may be able to get it to work but the big fail for me is that I may leave modx open to security holes without proper assistance.

                      So again, I implore the community for a little help on this. I can even set up a google project for this if that would be helpful.

                      Cheers,

                      Jay
                        Author of zero books. Formerly of many strange things. Pairs well with meats. Conversations are magical experiences. He's dangerous around code but a markup magician. BlogTwitterLinkedInGitHub