Skip to content

Instantly share code, notes, and snippets.

@WPprodigy
Forked from joncave/plugin.php
Last active March 10, 2022 08:17
Show Gist options
  • Save WPprodigy/e8c5815791c284f2fa1dd0d058acc7e4 to your computer and use it in GitHub Desktop.
Save WPprodigy/e8c5815791c284f2fa1dd0d058acc7e4 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/
<?php
/**
* Admin View: All logs
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<table>
<thead>
<tr>
<td><?php esc_html_e( 'Username', 'damn-vulnerable-wordpress-plugin' ) ?></td>
<td><?php esc_html_e( 'Password', 'damn-vulnerable-wordpress-plugin' ) ?></td>
<td><?php esc_html_e( 'IP Address', 'damn-vulnerable-wordpress-plugin' ) ?></td>
<td><?php esc_html_e( 'Time', 'damn-vulnerable-wordpress-plugin' ) ?></td>
</tr>
</thead>
<tbody>
<?php foreach ( $logs as $log ) : ?>
<tr>
<td><?php echo esc_html( $log['login'] ) ?></td>
<td><?php echo esc_html( $log['pass'] ) ?></td>
<td><?php echo esc_html( $log['ip'] ) ?></td>
<td>
<a href="<?php echo esc_url( add_query_arg( 'id', absint( $log['ID'] ), menu_page_url( 'failed-logins', false ) ) ); ?>">
<?php echo date( "{$date_format} H:i", strtotime( esc_html( $log['time'] ) ) ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<hr />
<h3><?php esc_html_e( 'Settings', 'damn-vulnerable-wordpress-plugin' ) ?></h3>
<form action="<?php echo esc_url( add_query_arg( 'action', 'dvp_settings', admin_url( 'admin-post.php' ) ) ); ?>" method="post">
<label>
<input type="checkbox" name="option[dvp_unknown_logins]" value="1" <?php checked( esc_attr( get_option( 'dvp_unknown_logins', '1' ) ) ) ?> />
<?php esc_html_e( 'Should login attempts for unknown usernames be logged?', 'damn-vulnerable-wordpress-plugin' ) ?>
</label>
<?php wp_nonce_field( 'dvp_settings' ) ?>
<p class="submit">
<input type="submit" name="submit" id="submit" class="button" value="<?php esc_html_e( 'Save Changes', 'damn-vulnerable-wordpress-plugin' ) ?>" />
</p>
</form>
<?php
/**
* Admin View: A single log
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<div>
<strong><?php esc_html_e( 'Username:', 'damn-vulnerable-wordpress-plugin' ) ?></strong> <?php echo esc_html( $log['login'] ) ?>
<br /><strong><?php esc_html_e( 'Attempted password:', 'damn-vulnerable-wordpress-plugin' ) ?></strong> <?php echo esc_html( $log['pass'] ) ?>
<br /><strong><?php esc_html_e( 'IP Address:', 'damn-vulnerable-wordpress-plugin' ) ?></strong> <?php echo esc_html( $log['ip'] ) ?>
<br /><strong><?php esc_html_e( 'Time of event:', 'damn-vulnerable-wordpress-plugin' ) ?></strong> <?php echo date( "{$date_format} H:i", strtotime( esc_html( $log['time'] ) ) ); ?>
</div>
<form action="<?php echo esc_url( add_query_arg( 'action', 'dvp_delete_log', admin_url( 'admin-post.php' ) ) ); ?>" method="post">
<input type="hidden" name="id" value="<?php echo absint( $id ) ?>" />
<input type="hidden" name="redirect" value="<?php echo esc_url( menu_page_url( 'failed-logins', false ) ) ?>" />
<?php wp_nonce_field( 'dvp_delete_log' ) ?>
<p class="submit">
<input type="submit" name="submit" id="submit" class="button" value="<?php esc_html_e( 'Delete Entry', 'damn-vulnerable-wordpress-plugin' ) ?>" />
</p>
</form>
<?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
* Text Domain: damn-vulnerable-wordpress-plugin
* License: GPLv2+
*
* DO NOT RUN THIS PLUGIN ON AN INTERNET ACCESSIBLE SITE
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Display a notice in the admin to prevent use of this plugin on a real site.
*/
function dvp_admin_safety_notice() {
echo '<div class="error"><p>' . wp_kses( __( '<strong>WARNING:</strong> Damn Vulnerable WordPress Plugin contains intentional security issues and should only be run on local development machines.', 'damn-vulnerable-wordpress-plugin' ), array( 'strong' => array() ) ) . '</p></div>';
}
add_action( 'all_admin_notices', 'dvp_admin_safety_notice' );
// Safety precautions are out of the way so load the actual stuff.
// In order for the plugin to continue working, you must define this constant.
if ( defined( 'LOAD_INTENTIONAL_VULNS' ) && LOAD_INTENTIONAL_VULNS ) {
include( dirname( __FILE__ ) . '/vulnerable.php' );
}
/**
* Install the database table where we will store logs.
*/
function dvp_install() {
if ( get_option( 'dvp_installed', 0 ) ) {
return;
}
global $wpdb;
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
$collate = '';
if ( $wpdb->has_cap( 'collation' ) ) {
$collate = $wpdb->get_charset_collate();
}
$sql = "
CREATE TABLE {$wpdb->prefix}dvp_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)
) $collate;
";
dbDelta( $sql );
update_option( 'dvp_installed', 1 );
}
register_activation_hook( __FILE__, 'dvp_install' );
<?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.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Log failed authentication attempts when the username was invalid.
* The wp_authenticate_user hook happens to late to do this.
*
* @param string $user Username entered during login.
* @param string $pass Password entered during login.
*/
function dvp_check_username( $user, $pass ) {
if ( ! empty( $user ) && ! username_exists( $user ) && get_option( 'dvp_unknown_logins', 1 ) ) {
dvp_log_failed_login( $user, $pass );
}
}
add_action( 'wp_authenticate', 'dvp_check_username', 10, 2 );
/**
* Log failed authentication attempts when the username was valid but password was not.
*
* @param WP_User|WP_Error $user User object, else error object if a previous callback failed.
* @param string $pass The password that was entered during login.
* @return WP_User|WP_Error
*/
function dvp_check_password( $user, $pass ) {
if ( ! is_wp_error( $user ) && ! wp_check_password( $pass, $user->user_pass, $user->ID ) ) {
dvp_log_failed_login( $user, $pass );
}
return $user;
}
add_filter( 'wp_authenticate_user', 'dvp_check_password', 10, 2 );
/**
* Add a log record for a failed login attempt.
*
* @param WP_User|string $user
* @param string $pass
*/
function dvp_log_failed_login( $user, $pass ) {
global $wpdb;
if ( $user instanceof WP_User ) {
$user_login = sanitize_text_field( $user->user_login );
} else {
$user_login = sanitize_text_field( $user );
}
$password = sanitize_text_field( $pass );
$ip_address = sanitize_text_field( dvp_get_ip() );
$wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->prefix}dvp_login_audit ( login, pass, ip, time ) VALUES ( %s, %s, %s, %s )", array( $user_login, $password, $ip_address, current_time( 'mysql' ) ) ) );
}
/**
* Add a WP Admin submenu page found at Tools > Failed Logins.
*/
function dvp_menu() {
add_submenu_page( 'tools.php', esc_html__( 'Failed Logins', 'damn-vulnerable-wordpress-plugin' ), esc_html__( 'Failed Logins', 'damn-vulnerable-wordpress-plugin' ), 'manage_options', 'failed-logins', 'dvp_admin' );
}
add_action( 'admin_menu', 'dvp_menu' );
/**
* Display the failed login(s) in the admin. Routes to either show one specific failed attempt, or all of them.
*/
function dvp_admin() {
?>
<div class="wrap">
<?php
if ( isset( $_GET['id'] ) && ! empty( absint( $_GET['id'] ) ) ) {
dvp_view_log( absint( $_GET['id'] ) );
} else {
dvp_view_all_logs();
}
?>
</div>
<?php
}
/**
* Display all failed login attempts + options form.
*
* TODO: Need to paginate results for better performance and scalability.
*/
function dvp_view_all_logs() {
global $wpdb;
$logs = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}dvp_login_audit", ARRAY_A );
echo '<h2>' . esc_html__( 'Failed Logins', 'damn-vulnerable-wordpress-plugin' ) . '</h2>';
if ( empty( $logs ) ) {
echo '<p>' . esc_html__( 'None&hellip; yet', 'damn-vulnerable-wordpress-plugin' ) . '</p>';
} else {
$date_format = get_option( 'date_format' );
include_once( dirname( __FILE__ ) . '/views/html-admin-all-logs.php' );
}
}
/**
* Display a single failed attempt with a form to delete the entry.
*/
function dvp_view_log( $id ) {
global $wpdb;
$log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}dvp_login_audit WHERE ID = %d", absint( $id ) ), ARRAY_A );
echo '<h2>' . sprintf( esc_html__( 'Failed login #%d', 'damn-vulnerable-wordpress-plugin' ), absint( $id ) ) . '</h2>';
if ( empty( $log ) ) {
echo '<p>' . esc_html__( 'No entry found for this ID.', 'damn-vulnerable-wordpress-plugin' ) . '</p>';
} else {
$date_format = get_option( 'date_format' );
include_once( dirname( __FILE__ ) . '/views/html-admin-single-log.php' );
}
}
/**
* Delete an entry.
*/
function dvp_delete_log() {
check_admin_referer( 'dvp_delete_log' );
if ( isset( $_POST['id'] ) && ! empty( absint( $_POST['id'] ) ) ) {
global $wpdb;
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}dvp_login_audit WHERE ID = %d", absint( $_POST['id'] ) ) );
}
if ( ! empty( $_POST['redirect'] ) ) {
wp_safe_redirect( wp_sanitize_redirect( $_POST['redirect'] ) );
}
}
add_action( 'admin_post_dvp_delete_log', 'dvp_delete_log' );
/**
* Update plugin options.
*/
function dvp_change_settings() {
// CSRF defence + caps check
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'dvp_settings' ) || ! current_user_can( 'manage_options' ) ) {
wp_safe_redirect( wp_sanitize_redirect( admin_url( 'tools.php?page=failed-logins' ) ) );
exit;
}
if ( ! isset( $_POST['option']['dvp_unknown_logins'] ) ) {
$_POST['option']['dvp_unknown_logins'] = 0;
}
// Update options and redirect.
// Not ideal to do this for just one setting, but leaving for the challenge.
foreach ( $_POST['option'] as $name => $value ) {
// Would list all valid fields here, and check against an array if there were more.
if ( 'dvp_unknown_logins' !== $name ) {
continue;
}
update_option( $name, sanitize_text_field( $value ) );
}
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() {
if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) {
return wp_unslash( $_SERVER['HTTP_X_REAL_IP'] );
} elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
// Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2.
// Make sure we always only send through the first IP in the list which should always be the client IP.
return (string) rest_is_ip_address( trim( current( preg_split( '/[,:]/', wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) );
} elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
return wp_unslash( $_SERVER['REMOTE_ADDR'] );
}
return '0.0.0.0';
}
@WPprodigy
Copy link
Author

WPprodigy commented Mar 7, 2018

Notable security fixes:

  • Fix the use of $wpdb->prepare() by passing in the second argument for the substitute variables.
  • Use $wpdb->prepare() in other places where arguments needed to be substituted in.
  • Sanitize inputs before saving to the database.
  • Escape outputs before displaying in the browser.
  • Use wp_safe_redirect() to ensure the user stays on-site.
  • Exit after wp_safe_redirect() to prevent further code execution.
  • Use wp_sanitize_redirect() before redirecting.
  • Handle $user being a WP_ERROR object.
  • Don't rely on $_SERVER variables to be safe/secure.
  • Don't blindly update any db option based on the presence of a form field.

Some Extras:

  1. Made all strings translatable under the plugin's text domain.
  2. Properly prefix the database table, and use the correct db character collate.
  3. Enhanced the IP Address function for some edge cases.
  4. Moved the heavy html parts to view/template files for easier reading.
  5. Implemented the unknown_logins logging feature.
  6. Output date in localized format.

Things TODO:

  • Paginate the log in the admin and don't request all rows at once.
  • Provide a way to bulk delete logs, and clear olds ones out at certain intervals.
  • Move the wp_authenticate_user filter logic into one function with wp_authenticate hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment