| DetailsAffected Software:Comment-Rating Plugin Fixed in Version:2.9.24 Issue Type:SQL Injection (SQLi) Original Code:Found Here DetailsThis week’s vulnerability was a tricky one. The bug patched in this change list affected the Comment-Rating plugin for WordPress (fixed in 2.9.24). Let’s take the bug step by step. First,the application takes a user/attacker supplied value and runs it through an escaping function here (line 9): $k_id = strip_tags($wpdb->escape($_GET['id'])); So,$k_id is now tainted and contains an escaped value provided by the attacker. A few lines later,we see the following code: if($k_id &&$k_action &&$k_path){ //Check to see if the comment id exists and grab the rating $query = “SELECT * FROM `$table_name` WHERE ck_comment_id = $k_id”; $result = mysql_query($query); The code above checks for a specific condition (which is a condition controllable by the attacker) then proceeds to build and execute a SQL query. On line 22 we see $k_id is used to build a dynamic SQL statement. Variables usage within stings are valid in PHP (http://php.net/manual/en/language.types.string.php –see Variable parsing). $k_id is escaped so we should be ok here…right? Actually,in this case escaping isn’t sufficient to prevent SQL injection. Escaping functions typically work by preventing a variable value from breaking out of quotes,unfortunately in this case there are no quotes to break out of. $k_id is designed to be a numeric value not a string,so there is no need to encapsulate the $k_id value in quotes. Although $k_id is designed to be numeric,there was nothing that would prevent an attacker from providing an arbitrary value for $k_id. For example,an attacker could provide a value like this for $k_id: 99999 union select uname,passwd from users As you can see,there are no special characters (double quotes,single quotes,or database escape characters) in the string above that would have been escaped by a database escaping function. When used to build the $query variable,we end up with: $query = “SELECT * FROM `$table_name` WHERE ck_comment_id = 99999 union select uname,passwd from users“; The developers addressed this vulnerability by validating that $k_id is indeed numeric before using the value to build a dynamic SQL statement. Developers Solution<?phprequire_once('../../../wp-config.php');require_once('../../../wp-includes/functions.php');// CSRF attack protection. Check the Referal field to be the same// domain of the script$k_id = strip_tags($wpdb->escape($_GET['id']));$k_action = strip_tags($wpdb->escape($_GET['action']));$k_path = strip_tags($wpdb->escape($_GET['path']));$k_imgIndex = strip_tags($wpdb->escape($_GET['imgIndex']));+// prevent SQL injection+if (!is_numeric($k_id)) die('error|Query error');$table_name = $wpdb->prefix . 'comment_rating';$comment_table_name = $wpdb->prefix . 'comments';if($k_id &&$k_action &&$k_path){ //Check to see if the comment id exists and grab the rating $query = "SELECT * FROM `$table_name` WHERE ck_comment_id = $k_id"; $result = mysql_query($query);if(!$result){die('error|mysql:'.mysql_error());} if(mysql_num_rows($result)){$duplicated = 0;// used as a counter to off set duplicated votes if($row = @mysql_fetch_assoc($result)){if(strstr($row['ck_ips'],getenv("REMOTE_ADDR"))){ // die('error|You have already voted on this item!'); // Just don't count duplicated votes $duplicated = 1; $ck_ips = $row['ck_ips']; } else{ $ck_ips = $row['ck_ips'] . ',' . getenv("REMOTE_ADDR");// IPs are separated by ',' } } $total = $row['ck_rating_up'] - $row['ck_rating_down'];if($k_action == 'add'){ $rating = $row['ck_rating_up'] + 1 - $duplicated; $direction = 'up'; $total = $total + 1 - $duplicated;} elseif($k_action == 'subtract'){ $rating = $row['ck_rating_down'] + 1 - $duplicated; $direction = 'down'; $total = $total - 1 + $duplicated;} else{ die('error|Try again later');//No action given. } if (!$duplicated){ $query = "UPDATE `$table_name` SET ck_rating_$direction = '$rating',ck_ips = '". $ck_ips . "' WHERE ck_comment_id = $k_id"; $result = mysql_query($query); if(!$result) { // die('error|query '.$query); die('error|Query error'); } // Now duplicated votes will not if(!mysql_affected_rows()) { die('error|affected '. $rating); } $karma_modified = 0; if (get_option('ckrating_karma_type') == 'likes' &&$k_action == 'add'){ $karma_modified = 1;$karma = $rating; } if (get_option('ckrating_karma_type') == 'dislikes' &&$k_action == 'subtract'){ $karma_modified = 1;$karma = $rating; } if (get_option('ckrating_karma_type') == 'both'){ $karma_modified = 1;$karma = $total; } if ($karma_modified){ $query = "UPDATE `$comment_table_name` SET comment_karma = '$karma' WHERE comment_ID = $k_id"; $result = mysql_query($query); if(!$result) die('error|Comment Query error'); } } } else{ die('error|Comment doesnt exist');//Comment id not found in db,something wrong ? }} else{ die('error|Fatal:html format error')}// Add the + sign,if ($total >0){$total = "+$total";}//This sends the data back to the js to process and show on the page// The dummy field will separate out any potential garbage that// WP-superCache may attached to the end of the return.echo("done|$k_id|$rating|$k_path|$direction|$total|$k_imgIndex|dummy");?>DetailsAffected Software:Short URL Plugin Fixed in Version:Changeset 55280 Issue Type:SQL Injection Original Code:Found Here DescriptionThis weeks’ vulnerabilities were a couple of SQL injection bugs in the Short URL Plugin for WordPress. The symptoms for the issues indicate classic SQL injection,let’s have a quick look at the code. First,looking over the code sample,we see a couple of dynamically built SQL statements. It would probably make sense to spend a bit of time and convert these dynamic SQL statements into prepared statements,that way you won’t have to worry about a code change inadvertently re-introducing a SQL injection flaw or an escaping filter bypass. With dynamically built SQL statements we’ll also have to trace each variable until we can determine whether the value can be controlled by an attacker. Lucky for us,the variable assignments are very close to the SQL statements. In the vulnerable sample,we see that the author is taking values directly from a POST request and using those tainted values to build SQL statements. Looking at the check-in,we see that the developer chose to use WordPress’ built-in escaping function for escaping user/attacker controlled data before passing it to a SQL statement. Although the checked-in fixes were straightforward,I was surprised to see that the developers missed an obvious SQL injection on line 56. Same classic SQL injection symptoms,the only difference is the dynamic SQL being built is a DELETE SQL statement as opposed to an INSERT or UPDATE. For those that are wondering… YES,this SQL injection is still present in the latest version of the plug-in! If you happen to be using this plug-in on your website,I would recommend you escape $delete_id before passing it to a SQL statement! I notified the plug-in author,hopefully they’ll be a patch soon. Is this the first Spot-The-Vuln.com 0day? Developers Solution<?php...snip...function kd_admin_options_su(){ global $table_prefix,$wpdb,$user_ID; $table_name = $table_prefix . "short_url"; if($wpdb->get_var("show tables like '$table_name'") != $table_name){ $sql = "CREATE TABLE ".$table_name."( link_id int(11) NOT NULL auto_increment, link_url text NOT NULL, link_desc text NOT NULL, link_count int(11) NOT NULL default '0', PRIMARY KEY (`link_id`) );"; require_once(ABSPATH . 'wp-admin/upgrade-functions.php'); dbDelta($sql); } if(isset($_POST['action'])){$action = $_POST['action'];if($action == "create"){- $add_url = $_POST['form_url'];- $add_desc = $_POST['form_desc'];+ $add_url = $wpdb->escape($_POST['form_url']);+ $add_desc = $wpdb->escape($_POST['form_desc']);if($add_url == "http://"|| (!$add_url)){$ERR = $ERR . "<br>You must enter a URL to redirect to!";} if(!$ERR){$wpdb->query("INSERT INTO $table_name (link_url,link_desc) VALUES ('$add_url','$add_desc')"); $new_url = get_option("siteurl") . "/u/". mysql_insert_id(); $MES = $MES . "<br>The redirect URL has been added. Your new Short URL is:". $new_url; } }if($action == "edit"){- $edit_id = $_POST['id'];- $edit_url = $_POST['form_url'];- $edit_desc = $_POST['form_desc'];+ $edit_id = $wpdb->escape($_POST['id']);+ $edit_url = $wpdb->escape($_POST['form_url']);+ $edit_desc = $wpdb->escape($_POST['form_desc']); if($edit_url == "http://"|| (!$edit_url)){$ERR = $ERR . "<br>You must enter a URL to redirect to!";} if(!$ERR){$wpdb->query("UPDATE $table_name SET link_url='$edit_url',link_desc='$edit_desc' WHERE link_id = $edit_id"); $MES = $MES . "<br>The redirect URL has been modified."; } }if($action == "delete"){ $delete_id = $_POST['id']; $wpdb->query("DELETE FROM $table_name WHERE link_id = '$delete_id'"); $MES = $MES . "<br>Redirect deleted!"; } if($action == "clearall"){ $wpdb->query("UPDATE $table_name SET link_count='0' WHERE link_count >0"); $MES = $MES . "<br>Counts have been reset!"; }} ?> <div class=wrap> <form method="post"><h2>Short URL Admin</h2><?php if($ERR){echo "<p>". $ERR . "</p>";}if($MES){echo "<p>". $MES . "</p>";} ?><p>Short URL allows you to create shorter URL's and keeps track of how manytimes a link has been clicked. It's useful for managing downloads,keeping trackof outbound links and for masking URL's. Clicking the Clear All Clicks buttonwill reset the count for each entry. Visit the <a href="http://www.harleyquine.com/php-scripts/short-url-plugin/">plugin page</a>for more information about this plugin.</p><h2>Current Redirects</h2><table class="widefat"> <thead> <tr> <th scope="col">Short URL (The URL to use)</th> <th scope="col">Real URL (Where it redirects to)</th> <th scope="col">Notes</th> <th scope="col">Amount of Clicks</th> <th scope="col">Manage</th> </tr></thead> <tbody id="the-list"><?php $rowdata = $wpdb->get_results("SELECT * FROM $table_name"); foreach ($rowdata as $row){ $is_editing = $_POST['edit_id']; if($is_editing){if($is_editing == $row->link_id){$EDIT = 1;$EDIT_ID = $row->link_id;$EDIT_URL = $row->link_url;$EDIT_DESC = $row->link_desc;} }?> <tr class='<?php echo $class;?>'> <th scope="row"><a href="<? echo get_option("siteurl") . "/u/". $row->link_id;?>"target="_blank"><? echo get_option("siteurl") . "/u/". $row->link_id;?></a></th> <td><? echo $row->link_url;?></td> <td><? echo $row->link_desc;?></td> <td><? echo $row->link_count;?></td> <td><form method="post"name="delete"><input type="hidden"name="action"value="delete"><input type="hidden"name="id"value="<? echo $row->link_id;?>"><input type="submit"value="Delete"></form><form method="post"name="edit"><input type="hidden"name="edit_id"value="<? echo $row->link_id;?>"><input type="submit"value="Edit"></form></td>...snip...DetailsAffected Software:The Hackers Diet Fixed in Version:9.7b Issue Type:Cross Site Scripting (XSS) Original Code:Found Here DescriptionFirst,some logistics… the code we’re looking at belongs to the “Weight_save.php” file which is part of the “Hackers Diet” WordPress plugin. This plugin was created to “Help you track and predict weight loss using your WordPress blog”. A single change was made to the Weight_save.php for this changelist. The single change simply removes an obvious XSS bug in which POST parameters are printed to HTML markup. This line was likely being used for debugging purposes and was forgotten during release to production. A more robust testing and release process would have caught this. Although the developers prevented an XSS vulnerability,they completely overlooked several other issues. IndigoMann (via blog comments) noticed XSS and SQL Injection in this code… The one issue that jumps out at me is this line: $user_id = $_POST["user"];
I always become very suspicious when an application passes user_id’s back and forth. Ideally,this data should be stored via session state,otherwise an attacker could pass an arbitrary value and access (and possibly update) another user’s data. Looking at the code,it appears this may be the case with the Hacker Diet plugin. Developers Solution<?// get our db settings without loading all of wordpress every save$html = implode('',file("../../../wp-config.php"));$html = str_replace ("require_once","// ",$html);$html = str_replace ("<?php","",$html);$html = str_replace ("?>","",$html);eval($html);if (isset($_POST["id"]) &&isset($_POST["user"]) &&is_numeric($_POST["user"]) &&isset($_POST["content"]) &&is_numeric($_POST["content"])){$date = substr($_POST["id"],7);$user_id = $_POST["user"];$weight = round($_POST["content"],1)} else{-print_r($_POST);+//print_r($_POST);echo "Please enter a valid number for your weight.";exit}mysql_connect(DB_HOST,DB_USER,DB_PASSWORD);mysql_select_db(DB_NAME);$query = "update ".$table_prefix."hackdiet_weightlog set weight = $weight where wp_id = $user_id and date = \"".date("Y-m-d",$date)."\"";mysql_query($query);if (mysql_affected_rows() != 1){// record doesn't exist yet,lets create it$query = "insert into ".$table_prefix."hackdiet_weightlog set date = \"".date("Y-m-d",$date)."\",weight = $weight,wp_id = $user_id";mysql_query($query);if (mysql_affected_rows() != 1){echo "Save failed. - ". mysql_error();exit()} else{echo htmlspecialchars($weight)}} else{echo htmlspecialchars($weight)}$query = "select trend from ".$table_prefix."hackdiet_weightlog where wp_id = $user_id and date <\"".date("Y-m-d",$date)."\"order by date desc limit 1";$result = mysql_query($query);if (mysql_num_rows($result) == 1){$trend = mysql_result($result,0);$use_first_weight_as_trend = false} else{// no trends exist below this entry,we must be first. so in next query,we need to grab today's weight to be trend 1$use_first_weight_as_trend = true}$query = "select date,weight,trend from ".$table_prefix."hackdiet_weightlog where wp_id = $user_id and date >= \"".date("Y-m-d",$date)."\"order by date asc";$result = mysql_query($query);while ($entry = mysql_fetch_assoc($result)){if ($use_first_weight_as_trend){$trend = $entry["weight"];$use_first_weight_as_trend = false} else{// exponentially smoothed moving average with 10% smoothing$trend = $trend + 0.1 * ($entry["weight"] - $trend)}$entry["trend"] = $trend;$weights[] = $entry}foreach ($weights as $entry){$query = "update ".$table_prefix."hackdiet_weightlog set trend = ".round($entry["trend"],1)."where wp_id = $user_id and date = \"".$entry["date"]."\"";mysql_query($query)}// 0 will always be the edited date,since the list contains the edited entry + all the ones after it,sorted asc.$dif = round($weights[0]["weight"] - $weights[0]["trend"],1);echo "<span class=\"trend_dif ".(($dif <0)?"good_trend":"bad_trend")."\">$dif</span>";?>DetailsAffected Software:The Hacker’s Diet (WordPress Plugin) Fixed in Version:0.9.7b Issue Type:SQL Injection Original Code: Found Here DescriptionThis week’s vulnerability was a SQL injection vulnerability affecting the Hacker’s Diet WordPress plugin. In the vulnerable version,the plugin assigns several variables using values obtained directly from the querystring. The variable assignments are shown below: $weeks = $_GET["weeks"]; $start_date = $_GET["start_date"]; $end_date = $_GET["end_date"]; $goal = $_GET["goal"]; $user_id = $_GET["user"]; $maint_mode = $_GET["maint_mode"];
No sanitization or validation is done before assigning the values. Once the assignments are made,the attacker controlled values are then passed to a dynamic SQL string here resulting in SQL Injection: $query = “select date,weight,trend from “.$table_prefix.”hackdiet_weightlog where wp_id = $user_id and date >\”".date(“Y-m-d”,strtotime(“$weeks weeks ago”)).”\”order by date asc”; $query = “select date,weight,trend from “.$table_prefix.”hackdiet_weightlog where wp_id = $user_id and date >= \”$start_date\”and date <= \”$end_date\”order by date asc”;
The plugin authors patched this vulnerability by validating that the $_GET[“user”] and $_GET[“weeks”] parameters contains only numeric characters. An interesting exercise would be to trace through the code and find where the following variables are being used: $start_date = $_GET["start_date"]; $end_date = $_GET["end_date"]; $goal = $_GET["goal"]; $maint_mode = $_GET["maint_mode"];
Developers Solution1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| <?php include (dirname(__FILE__)."/jpgraph/jpgraph.php"); include (dirname(__FILE__)."/jpgraph/jpgraph_line.php"); include (dirname(__FILE__)."/jpgraph/jpgraph_scatter.php");
// get our db settings without loading all of wordpress every save $html = implode('',file("../../../wp-config.php")); $html = str_replace ("require_once","// ",$html); $html = str_replace ("<?php","",$html); $html = str_replace ("?>","",$html); eval($html);
mysql_connect(DB_HOST,DB_USER,DB_PASSWORD); mysql_select_db(DB_NAME);
+if (!is_numeric($_GET["user"]) || !is_numeric($_GET["weeks"])) { + exit; +}
$weeks = $_GET["weeks"]; $start_date = $_GET["start_date"]; $end_date = $_GET["end_date"]; $goal = $_GET["goal"]; $user_id = $_GET["user"]; $maint_mode = $_GET["maint_mode"];
if ($weeks) { - $query = "select date,weight,trend from ".$table_prefix."hackdiet_weightlog where wp_id = $user_id and date >\"".date("Y-m-d",strtotime("$weeks weeks ago"))."\"order by date asc"; + $query = "select date,weight,trend from ".$table_prefix."hackdiet_weightlog where wp_id = \"". $user_id . "\"and date >\"".date("Y-m-d",strtotime("$weeks weeks ago"))."\"order by date asc"; } else if ($start_date and $end_date) { - $query = "select date,weight,trend from ".$table_prefix."hackdiet_weightlog where wp_id = $user_id and date >= \"$start_date\"and date <= \"$end_date\"order by date asc"; + $query = "select date,weight,trend from ".$table_prefix."hackdiet_weightlog where wp_id = \"". $user_id . "\"and date >= \"$start_date\"and date <= \"$end_date\"order by date asc"; }
result = mysql_query($query); if (mysql_num_rows($result)) { if (mysql_num_rows($result) == 1) { // only one day,gotta finagle the display
$row = mysql_fetch_assoc($result);
// fake day before $weight_data[] = 0; if ($goal >0) { $goal_data[] = $goal; } $x_data[] = date("n/j",strtotime("yesterday",strtotime($row["date"])));
// data $weight_data[] = $row["weight"]; if ($goal >0) { $goal_data[] = $goal; } $x_data[] = date("n/j",strtotime($row["date"]));
// fake day after $weight_data[] = 0; if ($goal >0) { $goal_data[] = $goal; } $x_data[] = date("n/j",strtotime("tomorrow",strtotime($row["date"]))); } else { $num_rows = mysql_num_rows($result); if ($num_rows <= 7 * 2) { // 0-2 weeks $ticks = "daily"; } else if ($num_rows <= 31 * 4) { // 2 weeks - 4 months $ticks = "weekly"; } else { // 4 months + $ticks = "monthly"; }
$count = 1; while ($row = mysql_fetch_assoc($result)) { $weight_data[] = $row["weight"]; $trend_data[] = $row["trend"]; if ($goal >0) { $goal_data[] = $goal; } switch ($ticks) { case "weekly": if ($count == 1) { $x_data[] = date("n/j",strtotime($row["date"])); } else { $x_data[] = ""; if ($count == 7) { $count = 0; } } break; case "monthly": if (date("j",strtotime($row["date"])) == "1") { $x_data[] = date("n/j",strtotime($row["date"])); } else { $x_data[] = ""; } break; case "daily": default: $x_data[] = date("n/j",strtotime($row["date"])); break; }
$count++; } } |
|