-
-
Save juliaamosova/32608cf13cbdd6bd396a883c260c2c1b to your computer and use it in GitHub Desktop.
An intentionally vulnerable plugin developed for WordPress plugin author education.http://make.wordpress.org/plugins/2013/04/09/intentionally-vulnerable-plugin/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* Plugin Name: Damn Vulnerable WordPress Plugin | |
* Description: Intentionally vulnerable plugin for plugin author education | |
* Version: 0.1 | |
* Plugin URI: http://make.wordpress.org/plugins/2013/04/09/intentionally-vulnerable-plugin/ | |
* Author: Jon Cave | |
* Author URI: http://joncave.co.uk | |
* License: GPLv2+ | |
* | |
* DO NOT RUN THIS PLUGIN ON AN INTERNET ACCESSIBLE SITE | |
*/ | |
function dvp_admin_safety_notice() { | |
echo '<div class="error"><p><strong>WARNING:</strong> Damn Vulnerable WordPress Plugin contains | |
intentional security issues and should only be run on local development machines.</p></div>'; | |
} | |
add_action( 'all_admin_notices', 'dvp_admin_safety_notice' ); | |
// Safety precautions are out of the way so load the actual stuff | |
if (defined('LOAD_INTENTIONAL_VULNS') && LOAD_INTENTIONAL_VULNS) { | |
include( dirname(__FILE__) . '/vulnerable.php' ); | |
} | |
function dvp_install() { | |
$sql = "CREATE TABLE login_audit ( | |
ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, | |
login varchar(200) NOT NULL default '', | |
pass varchar(200) NOT NULL default '', | |
ip varchar(20) NOT NULL default '', | |
time datetime NOT NULL default '0000-00-00 00:00:00', | |
PRIMARY KEY (ID) | |
);"; | |
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); | |
dbDelta( $sql ); | |
update_option( 'dvp_unknown_logins', 1 ); | |
} | |
register_activation_hook( __FILE__, 'dvp_install' ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Fake plugin containing intentional security vulnerabilities designed for | |
* plugin author education. | |
* | |
* Do NOT run this plugin on an internet accessible site. Do NOT re-use code | |
* from this plugin. | |
* | |
* This plugin attempts to track potential attackers visiting a site and display | |
* audit information to the administrator. | |
*/ | |
/** | |
* Log failed authentication attempts. | |
* | |
* @param WP_User $user | |
* @param string $pass | |
* @return WP_User | |
*/ | |
function dvp_check_login( $user, $pass ) { | |
if ( ! wp_check_password( $pass, $user->user_pass, $user->ID ) ) { | |
dvp_log_failed_login( $user, $pass ); | |
} | |
return $user; | |
} | |
add_filter( 'wp_authenticate_user', 'dvp_check_login', 10, 2 ); | |
/** | |
* Add a log record for a failed login attempt. | |
* | |
* @param WP_User $user | |
* @param string $pass | |
*/ | |
function dvp_log_failed_login( $user, $pass ) { | |
global $wpdb; | |
$login = $user->user_login; | |
$ip = dvp_get_ip(); | |
$time = current_time( 'mysql' ); | |
// JULIA'S COMMENT: SQL Injection vulnerability: | |
// Login details should not be saved in plain text, including failed attempts. | |
// Instead, we should be using the second argument of $wpdb->prepare to pass the actuall variables | |
// and use placeholders like %d and $s (printf format strings) to pass integers and strings (implement value binding method). | |
$wpdb->query( $wpdb->prepare( "INSERT INTO login_audit (login, pass, ip, time) VALUES (%s, %s, %s, %s)", array ($login, $pass, $ip, $time) ) ); | |
} | |
function dvp_menu() { | |
add_submenu_page( 'tools.php', 'Failed Logins', 'Failed Logins', 'manage_options', 'failed-logins', 'dvp_admin' ); | |
} | |
add_action( 'admin_menu', 'dvp_menu' ); | |
// Display the failed login(s) | |
function dvp_admin() { | |
echo '<div class="wrap">'; | |
// JULIA'S COMMENT: Making sure that the evalue is set, not null and not empty here. | |
// Also making sure that the value is of the type we need so using | |
// `absint` - convert a value to non-negative integer. | |
if ( isset( $_GET['id'] ) && ! empty( absint( $_GET['id'] ) ) ) { | |
dvp_view_log( absint( $_GET['id'] ) ); | |
} else { | |
dvp_view_all_logs(); | |
} | |
echo '</div>'; | |
} | |
// Display all failed login attempts + options form | |
function dvp_view_all_logs() { | |
global $wpdb; | |
$logs = $wpdb->get_results( "SELECT * FROM login_audit", ARRAY_A ); | |
echo '<h2>Failed logins</h2>'; | |
if (empty($logs)) { | |
echo '<p>None... yet</p>'; | |
} else { | |
echo '<table><thead><tr><td>Username</td><td>Password</td><td>IP address</td><td>Time</td></tr></thead><tbody>'; | |
foreach ($logs as $log) { | |
echo '<tr>'; | |
// JULIA'S COMMENT: using `esc_html` and `esc_url` to sanitize the output to prevent an XSS attack. | |
echo '<td>' . esc_html( $log['login'] ) . '</td>'; | |
echo '<td>' . esc_html( $log['pass'] ) . '</td>'; | |
echo '<td>' . esc_html( $log['ip'] ) . '</td>'; | |
$url = add_query_arg( 'id', $log['ID'], menu_page_url( 'failed-logins', false ) ); | |
echo '<td><a href="' . esc_url( $url ) . '">' . esc_html( $log['time'] ) . '</a></td>'; | |
echo '</tr>'; | |
} | |
echo '</tbody></table>'; | |
} | |
echo '<hr />'; | |
echo '<h3>Settings</h3>'; | |
echo '<form action="admin-post.php?action=dvp_settings" method="post">'; | |
wp_nonce_field( 'dvp_settings', 'nonce' ); | |
echo '<label>'; | |
echo '<input type="checkbox" name="option[dvp_unknown_logins]" value="1" ' . checked(1, get_option('dvp_unknown_logins'), false) . ' />'; | |
echo 'Should login attempts for unknown usernames be logged?</label>'; | |
submit_button( 'Update', 'secondary' ); | |
echo '</form>'; | |
} | |
// Display a single failed attempt with a form to delete the entry | |
function dvp_view_log( $id ) { | |
global $wpdb; | |
// JULIA'S COMMENT: SQL Injection vulnerability: same explanation as on line 42. | |
$log = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM login_audit WHERE ID = %d', array($id) ) ); | |
echo '<h2>Failed login #' . $id . '</h2>'; | |
echo '<div>'; | |
// JULIA'S COMMENT: using `esc_html` to sanitize the output to prevent an XSS attack. | |
echo '<strong>Username:</strong> ' . esc_html( $log['login'] ); | |
echo '<br /><strong>Attempted password:</strong> ' . esc_html( $log['pass'] ); | |
echo '<br /><strong>IP address:</strong> ' . esc_html( $log['ip'] ); | |
echo '<br /><strong>Time of event:</strong> ' . esc_html( $log['time'] ); | |
echo '</div>'; | |
echo '<form action="admin-post.php?action=dvp_delete_log" method="post">'; | |
wp_nonce_field(); | |
echo '<input type="hidden" name="id" value="' . $id . '" />'; | |
echo '<input type="hidden" name="redirect" value="' . $_SERVER['PHP_SELF'] . '?page=failed-logins" />'; | |
submit_button( 'Delete entry', 'delete' ); | |
echo '</form>'; | |
} | |
// Delete entry handler | |
function dvp_delete_log() { | |
check_admin_referer(); | |
// JULIA'S COMMENT: Making sure that the evalue is set, not null and not empty here. | |
// Also making sure that the value is of the type we need so using | |
// `absint` - convert a value to non-negative integer. | |
if ( isset( $_POST['id'] ) && ! empty( absint( $_POST['id'] ) ) ) { | |
global $wpdb; | |
// JULIA'S COMMENT: SQL Injection vulnerability: same explanation as on line 42. | |
$wpdb->query( $wpdb->prepare( 'SELECT * FROM login_audit WHERE ID = %d', array($_POST['id']) ) ); | |
} | |
// JULIA'S COMMENT: see comment below on line 152 regarding why we shouldn't use $_REQUEST. | |
wp_redirect( $_POST['redirect'] ); | |
} | |
add_action( 'admin_post_dvp_delete_log', 'dvp_delete_log' ); | |
// Update plugin options handler | |
function dvp_change_settings() { | |
// CSRF defence + caps check | |
// JULIA'S COMMENT: $_REQUEST should never be used because it is hard to track where the data is coming | |
// from (was it POST, or GET, or a cookie?), which makes reviewing the code more difficult. | |
// Additionally, it makes it easy to introduce sneaky and hard to find bugs, as any of the | |
// aforementioned locations can supply the data, which is hard to predict. Much better to be | |
// explicit and use either $_POST or $_GET instead. | |
if (isset($_POST['nonce']) && ! wp_verify_nonce($_POST['nonce'], 'dvp_settings') | |
|| ! current_user_can( 'manage_options' ) | |
) { | |
// JULIA'S COMMENT: Using `wp_sanitize_redirect` - sanitizes a URL for use in a redirect. | |
// Removes invalid URL characters in the URL parameter. | |
wp_safe_redirect( wp_sanitize_redirect( admin_url( 'tools.php?page=failed-logins' ) ) ); | |
} | |
if ( ! isset( $_POST['option']['dvp_unknown_logins'] ) ) | |
$_POST['option']['dvp_unknown_logins'] = 0; | |
// Update options and redirect | |
foreach ( $_POST['option'] as $name => $value ) | |
update_option( $name, $value ); | |
// JULIA'S COMMENT: Using `wp_sanitize_redirect` - sanitizes a URL for use in a redirect. | |
// Removes invalid URL characters in the URL parameter. | |
wp_safe_redirect( wp_sanitize_redirect( admin_url( 'tools.php?page=failed-logins' ) ) ); | |
} | |
add_action( 'admin_post_dvp_settings', 'dvp_change_settings' ); | |
/** | |
* Retrieve the IP address of the current user | |
* | |
* @return string IP address of current user | |
*/ | |
function dvp_get_ip() { | |
// True IP in case of proxies | |
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { | |
return $_SERVER['HTTP_X_FORWARDED_FOR']; | |
} else if (isset($_SERVER['REMOTE_ADDR'])) { | |
return $_SERVER['REMOTE_ADDR']; | |
} | |
return '0.0.0.0'; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment