wp-job-manager ≤ 1.29.2 preauth POI / unserialize of user supplied data

Wordpress has gone trough interesting period of time. They have tried to fix critical vulnerabilities:

in the Wordpress core via public writings and dummy PR, but security issues need to be solved by tech persons and their advice should be considered regarding the facts and not regarding the number of followers on social media.

Despite all of the advice's and tries (see h1 report and my medium writings) security team of Wordpress doesn’t recognize any of the issues and now all of us need to handle the consequences of their decisions.

Fix of the database abstraction library introduced 3 new vulnerabilities in the Wordpress core and the first one e.g. PHP object injection resulted with 2 critical vulnerabilities in quite popular wordpress plugins managed by Automattic:

Here I’ll explain the PHP object injection that was introduced in wp-job-manager quite popular WP plugin with 100k active installs holding valuable data on the server side as resumes from candidates that apply for certain job.

The issue

In the wp-job-manager-functions.php function get_job_listings we have the following:

$cached_query = true;
if ( false === ( $result = get_transient( $query_args_hash ) ) ) {
$result = new WP_Query( $query_args );
$cached_query = false;
set_transient( $query_args_hash, $result, DAY_IN_SECONDS );

e.g. it will create hash from user $_REQUEST input, will create the WP_Query object and will cache it in the database. But as we know from the warning if you serialize data structure that holds esc_sql value in it, when inserted into DB will result with damaged serialized string, but also if attacker careful crafts its payload, it will result with Object Injection e.g. unserialize of user supplied data!

More details

If we visit jobs listing / search page and use it, the following request will be issued:

curl 'http://localhost/wpm/wrt/index.php/jm-ajax/get_listings/' -H 'Cookie: wordpress_test_cookie=WP+Cookie+check' -H 'Origin: http://localhost' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-US,en;q=0.8' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Accept: */*' -H 'Referer: http://localhost/wpm/wrt/index.php/jobs/' -H 'X-Requested-With: XMLHttpRequest' -H 'Connection: keep-alive' --data 'lang=&search_keywords=attack%25%25%25keyword&search_location=test+location&filter_job_type%5B%5D=freelance&filter_job_type%5B%5D=full-time&filter_job_type%5B%5D=internship&filter_job_type%5B%5D=part-time&filter_job_type%5B%5D=temporary&filter_job_type%5B%5D=&per_page=10&orderby=featured&order=DESC&page=1&show_pagination=false&form_data=search_keywords%3Dattack%2525%2525%2525keyword%26search_location%3Dtest%2Blocation%26filter_job_type%255B%255D%3Dfreelance%26filter_job_type%255B%255D%3Dfull-time%26filter_job_type%255B%255D%3Dinternship%26filter_job_type%255B%255D%3Dpart-time%26filter_job_type%255B%255D%3Dtemporary%26filter_job_type%255B%255D%3D' --compressed

as you can notice from the request I have set up attack%%%keyword as keyword and test location as location. This results with transient in the DB (it is in the options table) and there we have the serialized WP_Query object. If we inspect the serialized object we will notice the following:

...s:376:"wp_posts.post_title LIKE '%attack\\%\\%\\%keyword%'";}}s:9:"tax_query";O:12:"WP_Tax_Query":6:{s:7:"queries";a:0:{}s:8:"relation";s:3:"AND";s:16:"�*�table_aliases";a:0:{}s:13:"queried_terms";a:0:{}s:13:"primary_table";s:8:"wp_posts";s:17:"primary_id_column";s:2:"ID";}s:10:"meta_query";O:13:"WP_Meta_Query":9:{s:7:"queries";a:2:{i:0;a:4:{i:0;a:3:{s:3:"key";s:29:"geolocation_formatted_address";s:5:"value";s:13:"test location";...

As you can notice s:376:"... there are not 376 characters between " and PHP will continue to count towards 376 and will stop looking for " character, but also you can notice that after this serialized property there is another serialized property that says s:13:”test location” e.g. this is the place where we can craft out payload and to make this damaged serialized object into “good” serialized object that will fulfill attackers needs.


At the moment I can see from the stats that update process is more than bad on the wp-job-manager plugin and instead to dump here full working exploit I’ll referrer towards this PoC code:

function goatgoat(){
global $wpdb;
$arr = array(
$arr = esc_sql($arr);
$arr = wp_unslash($arr);
$payload = $wpdb->remove_placeholder_escape(maybe_serialize($arr));
echo "<pre>";
add_action("init", "goatgoat");

What is the attack surface?

Attack surface is quite huge because unknown attacker could attack:

  • Underling PHP version via unserialize exploit
  • To look for POI gadget chain that will allow RCE, SQLi, XSS!
  • Recreation of serializedWP_Query object that will hold XSS payload in it!
  • Maybe there is default SSRF in the WP core somewhere, m?


Share this information in order this information to reach the owners of WP instances where this plugin is applied due the nature of data they hold on their server side in order to update the plugin ASAP.

Finding vulnerabilities like this one

You can find vulnerability like this one with simple static analysis of source code against some WP plugin / theme:

  • look for set_transcient function
  • If data structure (array or object) is saved there you are step close to find POI in the plugin
  • If data structure holds esc_sql value in it and there is another user input after it, there you are, report security issue!

You are welcome

I would like to give credits towards wp-job-manager development team! They have solved the issue quite fast and rewarded me a bounty on the h1 (report will become public in few days).


If you are wp developer or wp host provider or wp security product provider with valuable list of clients, we offer subscription list and we are exceptional (B2B only).