Popular Vulnerable Code

Tougher –SQL Injection

Details

Affected Software:PunBB

Fixed in Version:1.3

Issue Type:SQL Injection (SQLi)

Original Code:Found Here

Description

This week’s bug was an old SQL injection bug that affected PunBB versions <1.3. In short,a value is taken from an attacker/user controlled POST request and is used to build a SQL statement. This bug actually requires a small amount of tracing,so here we go! First,we see that PunBB takes the attacker/user supplied content here (line 11)

$form = array_map('trim',$_POST['form']);

The line above uses the value passed via $_POST[‘form’] to populate the $form variable. The value goes through a trim() function,but is (for the most part) un-sanitized. It’s interesting that PHP allows for the submission of arrays through POST parameters. This behavior is mentioned in the comments on this page

Next,the $form variable (which contains our attacker supplied values from $_POST[‘form’]) is used in a foreach statement and each index of the $form variable value is used in some application logic. You can see this in the following line (line 19)

foreach ($form as $key =>$input)

The foreach extracts the various values from the $form variable,does a quick comparison to a configuration value of some sort. If the comparison returns the correct value,the application uses the tainted value to populate a $query array variable. The tainted value is used here (line 26)

'SET'=>'conf_value='.$input,

The name of the variable ($query),along with the names of the indexes in the array (UPDATE,SET,WHERE),and finally the names of variables/functions close-by ($forum_db,query_build) are dead giveaways that the untainted value will eventually be used in a SQL query. Use of a tainted value in this manner leads to SQL injection.
The developers addressed this issue by casting the tainted $input value to an int before using it to build a SQL statement.

Developers Solution

...<snip>...// Load the admin.php language filerequire FORUM_ROOT.'lang/'.$forum_user['language'].'/admin_common.php';require FORUM_ROOT.'lang/'.$forum_user['language'].'/admin_settings.php';$section = isset($_GET['section']) ? $_GET['section']:null;if (isset($_POST['form_sent'])){$form = array_map('trim',$_POST['form']);($hook = get_hook('aop_form_submitted')) ? eval($hook):null;...<snip>...($hook = get_hook('aop_pre_update_configuration')) ? eval($hook):null;foreach ($form as $key =>$input){// Only update permission values that have changedif (array_key_exists('p_'.$key,$forum_config) &&$forum_config['p_'.$key] != $input){$query = array('UPDATE'=>'config',-'SET'=>'conf_value='.$input,+'SET'  =>'conf_value='.intval($input),'WHERE'=>'conf_name=\'p_'.$forum_db->escape($key).'\'');($hook = get_hook('aop_qr_update_permission_conf')) ? eval($hook):null;$forum_db->query_build($query) or error(__FILE__,__LINE__)}// Only update option values that have changedif (array_key_exists('o_'.$key,$forum_config) &&$forum_config['o_'.$key] != $input){if ($input != '' || is_int($input))$value = '\''.$forum_db->escape($input).'\'';else$value = 'NULL';$query = array('UPDATE'=>'config','SET'=>'conf_value='.$value,'WHERE'=>'conf_name=\'o_'.$forum_db->escape($key).'\'');($hook = get_hook('aop_qr_update_permission_option')) ? eval($hook):null;$forum_db->query_build($query) or error(__FILE__,__LINE__)}}// Regenerate the config cacheif (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))require FORUM_ROOT.'include/cache.php';generate_config_cache();($hook = get_hook('aop_pre_redirect')) ? eval($hook):null;redirect(forum_link($forum_url['admin_settings_'.$section]),$lang_admin_settings['Settings updated'].' '.$lang_admin_common['Redirect'])}if (!$section || $section == 'setup'){// Setup the form$forum_page['group_count'] = $forum_page['item_count'] = $forum_page['fld_count'] = 0;// Setup breadcrumbs$forum_page['crumbs'] = array(array($forum_config['o_board_title'],forum_link($forum_url['index'])),array($lang_admin_common['Forum administration'],forum_link($forum_url['admin_index'])),array($lang_admin_common['Settings'],forum_link($forum_url['admin_settings_setup'])),array($lang_admin_common['Setup'],forum_link($forum_url['admin_settings_setup'])));($hook = get_hook('aop_setup_pre_header_load')) ? eval($hook):null;define('FORUM_PAGE_SECTION','settings');define('FORUM_PAGE','admin-settings-setup');require FORUM_ROOT.'header.php';// START SUBST - <!-- forum_main -->ob_start();($hook = get_hook('aop_setup_output_start')) ? eval($hook):null;?><div class="main-content main-frm"><form class="frm-form"method="post"accept-charset="utf-8"action="<?php echo forum_link($forum_url['admin_settings_setup']) ?>"><div class="hidden"><input type="hidden"name="csrf_token"value="<?php echo generate_form_token(forum_link($forum_url['admin_settings_setup'])) ?>"/><input type="hidden"name="form_sent"value="1"/></div><div class="content-head"><h2 class="hn"><span><?php echo $lang_admin_settings['Setup personal'] ?></span></h2></div>...<snip>...

Fish –SQL Injection

Details

Affected Software:Members-only WordPress plugin

Fixed in Version:0.6.7

Issue Type:SQL Injection

Original Code: Found Here

Description

This week’s vuln was an easy one :)   The vulnerability affected the “members-only” plug-in for WordPress and was patched in version 0.6.7.  There are a couple changes in the diff,but the most important change (from a security standpoint) is the transition from a string-built,dynamic SQL statement to a prepared statement.  The following lines show the SQL injection bug:

$feedkey = $_GET['feedkey'];
… <snip>…
$find_feedkey = $wpdb->get_results(“SELECT umeta_id FROM $wpdb->usermeta WHERE meta_value = ‘$feedkey’”);

The members-only developers assigned a value for the $feedkey variable directly from the querystring.  They then used the attacker controlled value to build a SQL statement,using the $feedkey value to complete the SQL WHERE clause.  The symptoms presented above are classic SQL injection.  I’m happy to see that the members-only developers chose to use prepared statements to fix the SQL injection.

There are a few other fixes here,but the SQL injection was the most important security fix.

Developers Solution

1
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
-if (empty($userdata->ID) &&$members_only_opt['feed_access'] != 'feednone')  //Check if user is logged in or Feed Keys is required
+if (empty($userdata->ID) || $members_only_opt['feed_access'] != 'feednone')  //Check if user is logged in or Feed Keys is required
{
$feedkey = $_GET['feedkey'];
if (!empty($feedkey))
{
// Check if Feed Key is in the Database
-$find_feedkey = $wpdb->get_results("SELECT umeta_id FROM $wpdb->usermeta WHERE meta_value = '$feedkey'");
+$find_feedkey = $wpdb->get_results( $wpdb->prepare(
+          "SELECT umeta_id FROM $wpdb->usermeta WHERE meta_value = %s",
+          $feedkey
+) );

if (!empty($find_feedkey) &&$members_only_opt['feed_access'] == 'feedkeys') //If Feed Key is found and using Feed Keys
{
$feedkey_valid = TRUE;
if (empty($feedkey) &&$members_only_opt['feed_access'] == 'feedkeys')
{
$feed = members_only_create_feed('No Feed Key Found',$errormsg['feedkey_missing']);
-header("Content-Type:application/xml;charset=ISO-8859-1");
+header("Content-Type:application/xml;charset=". get_bloginfo( 'charset' ),true);
echo $feed;
exit;
}
elseif ($feedkey_valid == FALSE &&$members_only_opt['feed_access'] == 'feedkeys')
{
$feed = members_only_create_feed('Feed Key is Invalid',$errormsg['feedkey_invalid']);
-header("Content-Type:application/xml;charset=ISO-8859-1");
+header("Content-Type:application/xml;charset=". get_bloginfo( 'charset' ),true);
echo $feed;
exit;
}

if (empty($feedkey) &&$members_only_opt['feed_access'] == 'feedkeys')
{
$feed = members_only_create_feed('No Feed Key Found',$errormsg['feedkey_missing']);
-header("Content-Type:application/xml;charset=ISO-8859-1");
+header("Content-Type:application/xml;charset=". get_bloginfo( 'charset' ),true);
echo $feed;
exit;
}

Burnout –SQL Injection

Details

Affected Software:TimesToCome Stop Bot Registration (WordPress Plugin)

Fixed in Version:1.9

Issue Type:SQL Injection

Original Code: Found Here

Description

Straight up SQL Injection vulnerability.  The following attacker controlled values are taken via the following lines:

$request_time = $_SERVER['REQUEST_TIME'];

$http_accept = $_SERVER['HTTP_ACCEPT'];

$http_user_agent = $_SERVER['HTTP_USER_AGENT'];

$http_remote_addr = $_SERVER['REMOTE_ADDR'];

Then,a few lines down the attacker controlled values are used as part of a dynamic INSERT SQL statement.

$sql = “INSERT INTO ”. $registration_log_table_name . ”( ip,email,problem,accept,agent,day )

VALUES ( ‘$http_remote_addr’,‘$user’,‘$error’,‘$http_accept’,‘$http_user_agent’,NOW() )”;

$result = $wpdb->query( $sql );

Understanding that taking attacker controlled data and using it as part of a dynamic SQL statement is a bad idea,the plug-in developers checked in a patch to sanitize the data before using in the SQL statement.  The path is shown below:

$http_accept = htmlentities($http_accept);

$http_user_agent = htmlentities($http_user_agent);

$http_remote_addr = htmlentities($http_remote_addr);

$http_request_uri = htmlentities($html_request_uri);

There are a couple curious things about this patch.  First,instead of transitioning away from dynamic SQL building (which can be difficult to get just right),the authors decided to sanitize the input before passing it to the dynamic SQL.  Second (and more interesting),is the authors used htmlentities() to escape the data.  Htmlentities() is typically used to escape data to prevent XSS attacks,not SQL Injection.  Htmlentities() takes a few parameters,one of which is the optional $quote_style parameter.  The $quote_style parameter defines how strings with double and single quotes will be escaped.  According to the PHP documentation the three options are:

ENT_COMPAT  - Will convert double-quotes and leave single-quotes alone.

ENT_QUOTES  - Will convert both double and single quotes.

ENT_NOQUOTES –Will leave both double and single quotes unconverted

If the $quote_style is not specified,PHP will default to ENT_COMPAT.  Do you think this patch will hold up to the test of time?

Developers Solution

1
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
// log all requests to register on our blog
function ttc_add_to_log( $user,$error)
{

global $wpdb;
$registration_log_table_name = $wpdb->prefix . "ttc_user_registration_log";
$request_time = $_SERVER['REQUEST_TIME'];
$http_accept = $_SERVER['HTTP_ACCEPT'];
$http_user_agent = $_SERVER['HTTP_USER_AGENT'];
$http_remote_addr = $_SERVER['REMOTE_ADDR'];

if($wpdb->get_var("show tables like  '$registration_log_table_name'") != $registration_log_table_name)  {ttc_wp_user_registration_install();
}

// wtf? accept statements coming in at over 255 chars?  Prevent sql errors and any funny business
// by shortening anything from user to 200 chars if over 255
if ( strlen($email) >200 ){ $email = substr ($email,0,200 );}
if ( strlen($http_accept ) >200 ) { $http_accept = substr ( $http_accept,0,200 );}
if ( strlen($http_user_agent ) >200 ) { $http_user_agent = substr ( $http_user_agent,0,200 );}

+// clean input for database
+$http_accept = htmlentities($http_accept);
+$http_user_agent = htmlentities($http_user_agent);
+$http_remote_addr = htmlentities($http_remote_addr);
+$http_request_uri = htmlentities($html_request_uri);

$sql = "INSERT INTO ". $registration_log_table_name . "( ip,email,problem,accept,agent,day )
VALUES ( '$http_remote_addr','$user','$error','$http_accept','$http_user_agent',NOW() )";
$result = $wpdb->query( $sql );
}

// add an email to our email blacklist if we decide it is an bot
function ttc_add_to_blacklist( $email )
{
global $wpdb;
$blacklist_table_name = $wpdb->prefix . "ttc_user_registration_blacklist";

if($wpdb->get_var("show tables like '$blacklist_table_name'") != $blacklist_table_name) {
ttc_wp_user_registration_install();
}

if ( strlen($email) >200 ){ $email = substr ($email,0,200 );}

$sql = "INSERT INTO ". $blacklist_table_name . "( blacklisted ) VALUES ( '$email' )";
$result = $wpdb->query( $sql );

}

// add an ip to our ip blacklist if we decide it is a bot
function ttc_add_to_ip_blacklist( $ip )
{
global $wpdb;
$ip_table_name = $wpdb->prefix . "ttc_ip_blacklist";

if($wpdb->get_var("show tables like '$ip_table_name'") != $ip_table_name) {
ttc_wp_user_registration_install();
}

$sql = "INSERT INTO ". $ip_table_name . "( ip ) VALUES ( '$ip' )";
$result = $wpdb->query( $sql );
}

Reboot – SQL Injection

Details

Affected Software:The Hacker’s Diet (WordPress Plugin)

Fixed in Version:0.9.7b

Issue Type:SQL Injection

Original Code: Found Here

Description

This 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 Solution

1
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++;
}
}

Will - Sql InjectionWill – Sql Injection

Details

Affected Software:WP Category Manager plugin

Fixed in Version:1.3.1.0

Issue Type:Sql Injection

Original Code: Found Here

Description

This week’s vulnerability affected the WP Category Manager plugin.  There were two interesting characteristics I noticed with this code fix.  First,while there were a number of SQL injection vulnerabilities fixed with this change list,there were also a large number of non security fixes included in this change list as well.  It’s generally a good idea to keep security change lists separate from other change lists.  The number of non security fixes included in this particular list was so distracting,I removed them from the post.  The SQL injection fixes are pretty straight forward,changing dynamically built SQL statements into WordPress’ built-in $wpdb->prepare() function.

The second characteristic that caught my attention was usage of numeric IDs at the end of SQL statements.  For example:

where object_id = $postId and term_taxonomy_id= $categoryId”;

This syntax creates a condition in which the typical addslashes() used to protect against SQL injection can be bypassed.  For example,an attacker could craft a SQL injection string like:

Sqli.php?categoryId=-1 union select 1,2,3,4,5–

As you can see,the injection string above contains no special characters that would be escaped by addslashes().  Fortunately,the Category Manager plugin developers chose to utilze $wpdb->prepare() instead of addslashes().

Developers Solution

1
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<?php

include_once('wpcm-options.php');

if( ! class_exists('wpcm_functions')):

class wpcm_functions
{
public static function remove_category($postId,$categoryId)
{
-               global $wpdb;
-               $wpdb->show_errors();
-               $queryStr = "DELETE FROM $wpdb->term_relationships
-                         where object_id = $postId and term_taxonomy_id= $categoryId";
+        echo $postId;

-               $wpdb->query($queryStr);
+        if(is_int(intval($postId)))
+        {
+            global $wpdb;
+
+            $wpdb->show_errors();
+
+            $queryStr = $wpdb->prepare("DELETE FROM $wpdb->term_relationships where object_id = %d and term_taxonomy_id= %s",$postId,$categoryId);
+            $wpdb->query($queryStr);
+        }
}


public static function get_posts($category,$page)
{
global $wpdb;
$wpdb->show_errors();

// First figure out how many posts to show per page. This will be your limit
$pageSize = wpcm_options::get_option('postsperpage');

$finalQueryLine = '';

if($pageSize != -1)
{
// Next figure out how many posts to skip. This will be your offset
$offset = $pageSize * $page;

$finalQueryLine = "limit ". $pageSize . "offset ". $offset;

+                    }
+
+            $querystr = $wpdb->prepare("select wposts.*,wp_term_taxonomy.term_taxonomy_id
+                                    from $wpdb->posts wposts
+                                    LEFT JOIN $wpdb->term_relationships wp_term_relationships ON wposts.ID = wp_term_relationships.object_id
+                                    LEFT JOIN $wpdb->term_taxonomy wp_term_taxonomy ON wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id
+                                    LEFT JOIN $wpdb->terms wp_terms ON wp_terms.term_id = wp_term_taxonomy.term_id
+                                        WHERE wp_term_taxonomy.taxonomy = 'category'
+                                            and wp_terms.name = '%s'
+                                            and wposts.post_status='publish'
+                                        ORDER BY wposts.ID ". $finalQueryLine ,$category);
+
+            $postlist = $wpdb->get_results($querystr);
+            return $postlist;
}
-               $querystr = "select wposts.*,wp_term_taxonomy.term_taxonomy_id
-                                from $wpdb->posts wposts
-                                 LEFT JOIN $wpdb->term_relationships wp_term_relationships ON wposts.ID = wp_term_relationships.object_id
-                                 LEFT JOIN $wpdb->term_taxonomy wp_term_taxonomy ON wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id
-                                 LEFT JOIN $wpdb->terms wp_terms ON wp_terms.term_id = wp_term_taxonomy.term_id
-                                               WHERE wp_term_taxonomy.taxonomy = 'category'
-                                                               and wp_terms.name = '". $category . "'
-                                                               and wposts.post_status='publish'
-                                       ORDER BY wposts.ID ". $finalQueryLine;
-
-                $postlist = $wpdb->get_results($querystr);
-                return $postlist;
}

public static function get_postCount($category)
{
global $wpdb;
$wpdb->show_errors();

-               $querystr = "select count(*)
-                                from $wpdb->posts wposts
-                                 LEFT JOIN $wpdb->term_relationships wp_term_relationships ON wposts.ID = wp_term_relationships.object_id
-                                 LEFT JOIN $wpdb->term_taxonomy wp_term_taxonomy ON wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id
-                                 LEFT JOIN $wpdb->terms wp_terms ON wp_terms.term_id = wp_term_taxonomy.term_id
-                                               WHERE wp_term_taxonomy.taxonomy = 'category'
-                                                               and wp_terms.name = '". $category . "'
-                                                               and wposts.post_status='publish'";
+        $querystr = $wpdb->prepare("select count(*)
+                                from $wpdb->posts wposts
+                                    LEFT JOIN $wpdb->term_relationships wp_term_relationships ON wposts.ID = wp_term_relationships.object_id
+                                    LEFT JOIN $wpdb->term_taxonomy wp_term_taxonomy ON wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id
+                                    LEFT JOIN $wpdb->terms wp_terms ON wp_terms.term_id = wp_term_taxonomy.term_id
+                                        WHERE wp_term_taxonomy.taxonomy = 'category'
+                                            and wp_terms.name = '%s'
+                                            and wposts.post_status='publish'",$category);

$result = $wpdb->get_var($querystr,0,0);
return $result;

}

public static function render_posts($postlist)
{
if($postlist)
{
foreach($postlist as $post)
{
echo '<div id="catmanagerpost'. $post->ID .'">';
echo '<span ><a href="'. get_permalink($post->ID) .'"title="'.$post->post_title . '">' . $post->post_title . '</a></span><span >' . date_format(date_create($post->post_date),"F j,Y") . '</span>';
echo '<p ><a href="javascript:void(0);"postID="'.$post->ID.'"catID="'. $post->term_taxonomy_id  .'"id="catmanremovepost'. $post->ID .'"title="Remove post from this category">Remove</a>| ';
echo edit_post_link('Edit Post','','',$post->ID);
echo '</p></div>';
}
}
else
{
echo '<strong>No posts found</strong>';
}
}

public static function render_postcount($category)
{
$count = wpcm_functions::get_postCount($category);

echo '<span id="wpcmpostcount">'.$count.'</span>';
}

public static function get_categories()
{
global $wpdb;

$wpdb->show_errors();

$querystr = "select wt.name,wt.term_id
from $wpdb->terms wt
join $wpdb->term_taxonomy wtt on wtt.term_id = wt.term_id
where wtt.taxonomy = 'category'
order by wt.name";

$catlist = $wpdb->get_results($querystr);
return $catlist;
}
}
endif;

?>

Widths – SQL Injection

Details

Affected Software:Login LockDown for WordPress

Fixed in Version: 1.5

Issue Type:SQL Injection

Original Code: Found Here

Description

This week’s code sample comes from the “Login Lockdown” plug-in for WordPress.  It’s always interesting when “security” software ends up having serious security flaws….

This patch contained several bug fixes.  The first bug fix we see in the patch is the inclusion of nonce checking to prevent CSRF.  It’s difficult to detect CSRF vulnerabilities by looking at individual function logic and it’s ok if the reader missed these bugs.  CSRF token validation should be done at the framework level and including CSRF nonce validation in the logic of every function can quickly become unwieldy.  If an application wide CSRF solution cannot be implemented at the framework level,then auditing for CSRF must be done at a function by function level.  Personally,I prefer to check for CSRF vulnerabilities by identifying any function that performs a Create,Update,or Delete operation,mapping those functions back to the HTML markup and checking the markup to see if a nonce is passed as part of the POST or GET request.  This is of course is done after an extensive audit of the nonce validation code.

The bugs that should have been spotted by the spotthevuln reader are the SQL injection and the XSS vulnerabilities in the code.  The SQL Injection is pretty straight forward.  The “releaseme” POST parameter is taken and is eventually passed directly to a dynamically built SQL statement without any sanitization.  The developers fixed the vulnerability by utilizing the WordPress escape logic.

Finally,the last line of the code snippet actually contained an XSS vulnerability,echoing a $_SERVER variable without any sanitization.

Developers Solution

1
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
function print_loginlockdownAdminPage() {
        global $wpdb;
        $table_name = $wpdb->prefix . "lockdowns";
        $loginlockdownAdminOptions = get_loginlockdownOptions();

        if (isset($_POST['update_loginlockdownSettings'])) {

+  //wp_nonce check
+  check_admin_referer('login-lockdown_update-options');

                if (isset($_POST['ll_max_login_retries'])) {
                        $loginlockdownAdminOptions['max_login_retries'] = $_POST['ll_max_login_retries'];
                }
                if (isset($_POST['ll_retries_within'])) {
                        $loginlockdownAdminOptions['retries_within'] = $_POST['ll_retries_within'];
                }
                if (isset($_POST['ll_lockout_length'])) {
                        $loginlockdownAdminOptions['lockout_length'] = $_POST['ll_lockout_length'];
                }
                if (isset($_POST['ll_lockout_invalid_usernames'])) {
                        $loginlockdownAdminOptions['lockout_invalid_usernames'] = $_POST['ll_lockout_invalid_usernames'];
                }
                if (isset($_POST['ll_mask_login_errors'])) {
                        $loginlockdownAdminOptions['mask_login_errors'] = $_POST['ll_mask_login_errors'];
                }
                update_option("loginlockdownAdminOptions",$loginlockdownAdminOptions);
                ?>
<div><p><strong><?php _e("Settings Updated.","loginlockdown");?></strong></p></div>
                <?php
        }
        if (isset($_POST['release_lockdowns'])) {

+  //wp_nonce check
+  check_admin_referer('login-lockdown_release-lockdowns');
               
  if (isset($_POST['releaseme'])) {
                        $released = $_POST['releaseme'];
                        foreach ( $released as $release_id ) {
                                $results = $wpdb->query("UPDATE $table_name SET release_date = now() ".
-                                                       "WHERE lockdown_ID = $release_id");
+                                                      "WHERE lockdown_ID = ". $wpdb->escape($release_id) . "");
                        }
                }
                update_option("loginlockdownAdminOptions",$loginlockdownAdminOptions);
                ?>
<div><p><strong><?php _e("Lockdowns Released.","loginlockdown");?></strong></p></div>
                <?php
        }
        $dalist = listLockedDown();
?>
<div>
-<form method="post"action="<?php echo $_SERVER["REQUEST_URI"];?>">
+<form method="post"action="<?php echo esc_attr($_SERVER["REQUEST_URI"]);?>">

Pictures – SQL Injection

Details

Affected Software: Short URL Plugin

Fixed in Version: 2.0

Issue Type:SQL Injection

Original Code: Found Here

Description

This was a vulnerability that affected the Short URL WordPress plugin.  The vulnerability is very straightforward and should have been easily detected by a security code reviewer.  The vulnerable code section takes attacker controlled data directly from $_POST[‘form_url’],$_POST[‘form_desc’],and $_POST[‘id’] and uses the tainted value immediately in dynamically built SQL statements.  One interesting piece of this particular code fix is that the developers chose to implement the code fixes near the assignment of the variable (as opposed to near consumption,in the SQL statement).

Another interesting piece of the code fix is the logic for the following conditional:

if($action == “delete”){

looks like the devs may have forgotten something :)

Developers Solution

1
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
<?php
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>
   <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 many
times a link has been clicked. It's useful for managing downloads,keeping track
of outbound links and for masking URL's. Clicking the Clear All Clicks button
will reset the count for each entry. Visit the <a href="<a href="http://www.harleyquine.com/php-scripts/short-url-plugin/%22%3Eplugin">http://www.harleyquine.com/php-scripts/short-url-plugin/">plugin</a>page</a>for more information about this plugin.</p>

<h2>Current Redirects</h2>
<table>
   <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">
?>

Butterflies - SQL Injection / XSSButterflies – SQL Injection / XSS

Details

Affected Software:WordPress Plugin Login LockDown

Fixed in Version:1.5

Issue Type:SQL Injection and XSS

Original Code: Found Here

Description

In a SpotTheVuln first,today’s example actually contains two different vulnerabilities.  The sample comes from a WordPress plugin called “Login Lockdown”.  The first vulnerability patched by the Login Lockdown developers is a SQL Injection vulnerability.  In this particular example,we see that the “$release_id” variable is passed directly to a SQL statement without being sanitized.  The Login Lockdown developers corrected this vulnerability by simply escaping the variable before including it in a dynamically built SQL statement.

The other vulnerabilities are XSS vulnerabilities.  The beginning of the code sample contains several “isset” statements which eventually assign the values from various POST parameters to php variables.  These variables are then used to build the HTML markup later in the PHP code.  The Login Lockdown developers corrected this vulnerability by using the “esc_attr” before using the user controlled data in the HTML markup.

Developers Solution

1
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
106
107
108
109
110
111
<div id="_mcePaste">

<?php

function print_loginlockdownAdminPage() {

global $wpdb;

$table_name = $wpdb->prefix . "lockdowns";

$loginlockdownAdminOptions = get_loginlockdownOptions();

if (isset($_POST['update_loginlockdownSettings'])) {

if (isset($_POST['ll_max_login_retries'])) {

$loginlockdownAdminOptions['max_login_retries'] = $_POST['ll_max_login_retries'];

}

if (isset($_POST['ll_retries_within'])) {

$loginlockdownAdminOptions['retries_within'] = $_POST['ll_retries_within'];

}

if (isset($_POST['ll_lockout_length'])) {

$loginlockdownAdminOptions['lockout_length'] = $_POST['ll_lockout_length'];

}

if (isset($_POST['ll_lockout_invalid_usernames'])) {

$loginlockdownAdminOptions['lockout_invalid_usernames'] = $_POST['ll_lockout_invalid_usernames'];

}

if (isset($_POST['ll_mask_login_errors'])) {

$loginlockdownAdminOptions['mask_login_errors'] = $_POST['ll_mask_login_errors'];

}

update_option("loginlockdownAdminOptions",$loginlockdownAdminOptions);

?>

<div class="updated"><p><strong><?php _e("Settings Updated.","loginlockdown");?></strong></p></div>

<?php

}

if (isset($_POST['release_lockdowns'])) {

if (isset($_POST['releaseme'])) {

$released = $_POST['releaseme'];

foreach ( $released as $release_id ) {

$results = $wpdb->query("UPDATE $table_name SET release_date = now() ".

- "WHERE lockdown_ID = $release_id");

+ "WHERE lockdown_ID = ". $wpdb->escape($release_id) . "");

}

}

update_option("loginlockdownAdminOptions",$loginlockdownAdminOptions);

?>

<div class="updated"><p><strong><?php _e("Lockdowns Released.","loginlockdown");?></strong></p></div>

<?php

}

$dalist = listLockedDown();

?>

<div>

<form method="post"action="<?php echo $_SERVER["REQUEST_URI"];?>">

<h2><?php _e('Login LockDown Options','loginlockdown') ?></h2>

<h3><?php _e('Max Login Retries','loginlockdown') ?></h3>

-<input type="text"name="ll_max_login_retries"size="8"value="<?php echo $loginlockdownAdminOptions['max_login_retries'];?>">

+<input type="text"name="ll_max_login_retries"size="8"value="<?php echo esc_attr($loginlockdownAdminOptions['max_login_retries']);?>">

<h3><?php _e('Retry Time Period Restriction (minutes)','loginlockdown') ?></h3>

-<input type="text"name="ll_retries_within"size="8"value="<?php echo $loginlockdownAdminOptions['retries_within'];?>">

+<input type="text"name="ll_retries_within"size="8"value="<?php echo esc_attr($loginlockdownAdminOptions['retries_within']);?>">

<h3><?php _e('Lockout Length (minutes)','loginlockdown') ?></h3>

-<input type="text"name="ll_lockout_length"size="8"value="<?php echo $loginlockdownAdminOptions['lockout_length'];?>">

+<input type="text"name="ll_lockout_length"size="8"value="<?php echo esc_attr($loginlockdownAdminOptions['lockout_length']);?>">

</div>

More Than One Night - Defense in DepthMore Than One Night – Defense in Depth

Details

Affected Software:WordPress

Fixed in Version:2.1

Issue Type:Defense in Depth

Original Code: Found Here

Description

I found this issue interesting for a couple reasons. Upon first glance,the patch appears to be a defense against SQL Injection and in essence,it is. It seems that the $q[‘cat’] value is controlled by the user and is eventually used to help build a SQL statement. Before the $q[‘cat’] value is used in a SQL statement,it is actually sanitized by the following lines:

$q['cat'] = ”.urldecode($q['cat']).”;

$q['cat'] = addslashes_gpc($q['cat']);

Once the value is sanitized,it is used to build various SQL statements. Now this particular patch was developed by the WordPress team because they discovered that when a user/attacker passes a “.” (period character) to $q[‘cat’],it would cause a SQL error which would be displayed to the user. While a single period character doesn’t give the attacker the ability to execute arbitrary SQL,it does give the attacker an information disclosure bug. In an academic sense however,the attacker has convinced the database that their provided value should be interpreted as code as opposed to data (ala SQL Injection). The reason the period character slips through is because it is not defined as a special character in the addslashes() php function… this could be useful in other situations.

The WordPress prevented the information leak by checking to see if $q[‘cat’] is an integer value. The patch here is a single line fix.

Developers Solution

1
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
// Category stuff

if ((empty($q['cat'])) || ($q['cat'] == '0') ||

// Bypass cat checks if fetching specific posts

( $this->is_single || $this->is_page )) {

$whichcat='';

} else {

$q['cat'] = ''.urldecode($q['cat']).'';

$q['cat'] = addslashes_gpc($q['cat']);

$join = "LEFT JOIN $wpdb->post2cat ON ($wpdb->posts.ID = $wpdb->post2cat.post_id) ";

$cat_array = preg_split('/[,\s]+/',$q['cat']);

$in_cats = $out_cats = '';

foreach ( $cat_array as $cat ) {

+ $cat = intval($cat);

$in = strstr($cat,'-') ? false:true;

$cat = trim($cat,'-');

if ( $in )

$in_cats .= "$cat,". get_category_children($cat,'',',');

else

$out_cats .= "$cat,". get_category_children($cat,'',',');

}

$in_cats = substr($in_cats,0,-2);

$out_cats = substr($out_cats,0,-2);

if ( strlen($in_cats) >0 )

$in_cats = "AND category_id IN ($in_cats)";

if ( strlen($out_cats) >0 )

$out_cats = "AND category_id NOT IN ($out_cats)";

$whichcat = $in_cats . $out_cats;

$distinct = 'DISTINCT';

}

// Category stuff for nice URIs

global $cache_categories;

if ('' != $q['category_name']) {

Misunderstood - SQLiMisunderstood –SQL Injection

Details

Affected Software:OSCommerce2

Fixed in Version:

Issue Type:SQL Injection

Original Code: Found Here

Description

Implementation bug here,leading to SQL injection.  The primary change involves the changing of a regular expression.  The initial check seemed be an attempt to validate data,but was too loose.  The data that slipped through validation eventually makes its way into a dynamically built SQL statement,resulting in SQL injection.

The Regular Expression is tightened up and some of the SQL statement was changed.  It’s interesting that the developers chose to implement better regular expression checking instead of trying to move this particular SQL statement to a prepared statement.

Developers Solution

1
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
// show the products of a specified manufacturer

if (isset($HTTP_GET_VARS['manufacturers_id'])) {

if (isset($HTTP_GET_VARS['filter_id']) &&tep_not_null($HTTP_GET_VARS['filter_id'])) {

// We are asked to show only a specific category

$listing_sql = "select ". $select_column_list . "p.products_id,p.manufacturers_id,p.products_price,p.products_tax_class_id,IF(s.status,s.specials_new_products_price,NULL) as specials_new_products_price,IF(s.status,s.specials_new_products_price,p.products_price) as final_price from ". TABLE_PRODUCTS . "p left join ". TABLE_SPECIALS . "s on p.products_id = s.products_id,". TABLE_PRODUCTS_DESCRIPTION . "pd,". TABLE_MANUFACTURERS . "m,". TABLE_PRODUCTS_TO_CATEGORIES . "p2c where p.products_status = '1' and p.manufacturers_id = m.manufacturers_id and m.manufacturers_id = '". (int)$HTTP_GET_VARS['manufacturers_id'] . "' and p.products_id = p2c.products_id and pd.products_id = p2c.products_id and pd.language_id = '". (int)$languages_id . "' and p2c.categories_id = '". (int)$HTTP_GET_VARS['filter_id'] . "'";

} else {

// We show them all

$listing_sql = "select ". $select_column_list . "p.products_id,p.manufacturers_id,p.products_price,p.products_tax_class_id,IF(s.status,s.specials_new_products_price,NULL) as specials_new_products_price,IF(s.status,s.specials_new_products_price,p.products_price) as final_price from ". TABLE_PRODUCTS . "p left join ". TABLE_SPECIALS . "s on p.products_id = s.products_id,". TABLE_PRODUCTS_DESCRIPTION . "pd,". TABLE_MANUFACTURERS . "m where p.products_status = '1' and pd.products_id = p.products_id and pd.language_id = '". (int)$languages_id . "' and p.manufacturers_id = m.manufacturers_id and m.manufacturers_id = '". (int)$HTTP_GET_VARS['manufacturers_id'] . "'";

}

} else {

// show the products in a given categorie

if (isset($HTTP_GET_VARS['filter_id']) &&tep_not_null($HTTP_GET_VARS['filter_id'])) {

// We are asked to show only specific catgeory

$listing_sql = "select ". $select_column_list . "p.products_id,p.manufacturers_id,p.products_price,p.products_tax_class_id,IF(s.status,s.specials_new_products_price,NULL) as specials_new_products_price,IF(s.status,s.specials_new_products_price,p.products_price) as final_price from ". TABLE_PRODUCTS . "p left join ". TABLE_SPECIALS . "s on p.products_id = s.products_id,". TABLE_PRODUCTS_DESCRIPTION . "pd,". TABLE_MANUFACTURERS . "m,". TABLE_PRODUCTS_TO_CATEGORIES . "p2c where p.products_status = '1' and p.manufacturers_id = m.manufacturers_id and m.manufacturers_id = '". (int)$HTTP_GET_VARS['filter_id'] . "' and p.products_id = p2c.products_id and pd.products_id = p2c.products_id and pd.language_id = '". (int)$languages_id . "' and p2c.categories_id = '". (int)$current_category_id . "'";

} else {

// We show them all

$listing_sql = "select ". $select_column_list . "p.products_id,p.manufacturers_id,p.products_price,p.products_tax_class_id,IF(s.status,s.specials_new_products_price,NULL) as specials_new_products_price,IF(s.status,s.specials_new_products_price,p.products_price) as final_price from ". TABLE_PRODUCTS_DESCRIPTION . "pd,". TABLE_PRODUCTS . "p left join ". TABLE_MANUFACTURERS . "m on p.manufacturers_id = m.manufacturers_id left join ". TABLE_SPECIALS . "s on p.products_id = s.products_id,". TABLE_PRODUCTS_TO_CATEGORIES . "p2c where p.products_status = '1' and p.products_id = p2c.products_id and pd.products_id = p2c.products_id and pd.language_id = '". (int)$languages_id . "' and p2c.categories_id = '". (int)$current_category_id . "'";

}

}

-   if ( (!isset($HTTP_GET_VARS['sort'])) || (!ereg('[1-8][ad]',$HTTP_GET_VARS['sort'])) || (substr($HTTP_GET_VARS['sort'],0,1) >sizeof($column_list)) ) {

+   if ( (!isset($HTTP_GET_VARS['sort'])) || (!preg_match('/^[1-8][ad]$/',$HTTP_GET_VARS['sort'])) || (substr($HTTP_GET_VARS['sort'],0,1) >sizeof($column_list)) ) {

for ($i=0,$n=sizeof($column_list);$i<$n;$i++) {

if ($column_list[$i] == 'PRODUCT_LIST_NAME') {

$HTTP_GET_VARS['sort'] = $i+1 . 'a';

+      $listing_sql .= "order by pd.products_name";

+      break;

+     }

+    }

} else {

$sort_col = substr($HTTP_GET_VARS['sort'],0 ,1);

$sort_order = substr($HTTP_GET_VARS['sort'],1);

-   $listing_sql .= ' order by ';

switch ($column_list[$sort_col-1]) {

case 'PRODUCT_LIST_MODEL':

+     $listing_sql .= "order by p.products_model ". ($sort_order == 'd' ? 'desc':'') . ",pd.products_name";

-     $listing_sql .= "p.products_model ". ($sort_order == 'd' ? 'desc':'') . ",pd.products_name";

break;

case 'PRODUCT_LIST_NAME':

$listing_sql .= "order by pd.products_name ". ($sort_order == 'd' ? 'desc':'');

break;

case 'PRODUCT_LIST_MANUFACTURER':

$listing_sql .= "order by m.manufacturers_name ". ($sort_order == 'd' ? 'desc':'') . ",pd.products_name";

break;

case 'PRODUCT_LIST_QUANTITY':

$listing_sql .= "order by p.products_quantity ". ($sort_order == 'd' ? 'desc':'') . ",pd.products_name";

break;

case 'PRODUCT_LIST_IMAGE':