0

I have a malware java script that was added to the end of every post. I tried using an SQL statement in phpMyAdmin. Here is a shortened version of that:

SET @virus = "<script>var _0x2cf4=['MSIE"+CHAR(59)+"','OPR','Chromium','Chrome','ppkcookie','location','https://www.wow-robotics.xyz','onload','getElementById'...(and a lot more obfuscated script)...;
UPDATE
  wp_posts
SET
  post_content =
REPLACE
  (
    post_content,
    @virus COLLATE utf8mb4_unicode_520_ci,
    ''
  );

This was not initially successful because many false matches caused deletions all over the website. I didn't have time to find out why, so I shortened the search text to be only the beginning of the script, which broke the malware and rendered it merely ugly. This got me out of trouble, but obviously it's not the best solution.

How should I do this? I have tried to tokenize the post content, using the text of the malware script as the token:

<?php

ignore_user_abort(true);
set_time_limit(30);

$path = $_SERVER['DOCUMENT_ROOT'];
require( $path.'/wp-load.php' );
$post_table = $wpdb->prefix . 'posts';
$meta_table = $wpdb->prefix . 'postmeta';
$starting_virus_text = "<script>['MSIE;','OPR','Chromium','Chrome','ppkcookie','location','https://www.wow-robotics.xy";

$args = array(
        'post_type' => 'page',
        'post_status' => 'publish',
        'posts_per_page' => -1,
    );

$posts = get_posts($args);

echo 'Post count is '.count($posts).'<br>';

foreach($posts as $post){
    $new_content = strtok($post->post_content, $starting_virus_text);
    if ($post->ID == 192) { // my home page, just for example
        echo 'POST 192<br><pre>';
        var_dump($post->post_content);
    }
    if (strlen($new_content) > 20){
        echo $new_content;
        die;
    }
}

echo '<br>Done.';

?>

This was also unsuccessful. The token is never found in the post content, even though it is there. Should I do a character-ny-character type examination and store post_content in an intermediate variable? Any advice is appreciated.

3 Answers 3

1

Is the malware in each post in the database? If so, something is inserting it, so you need to fix that first. Lots of googles/bings/ducks on how to remove malware from a WP site. (I have my own procedure here that I use for clients: https://www.securitydawg.com/recovering-from-a-hacked-wordpress-site/ ; there are others.)

I'd do a thorough cleanup of the site; changing credentials (everywhere, not just WP), strong passwords throughout; look for unauthorized files; check wp-config.php and other files for inserted code; a fresh install of plugins/themes from known good sources (via FTP); update everything; update your PHP version; and more.

And then look at the wp-posts table for inserted content. The script code might be different for each post, so you may need to manually fix things.

And, of course, a backup of your database before any major changes.

3
  • I have already removed the malware from the site. In this question I am looking for a programmatic way to remove a very long, complex "string" from each post in the database, should the string exist. In my particular case the injected "string" is identical in each post_content field. However, even if the malware had customized each entry you could still use strtok() to find the beginning of the problem and truncate at that point.
    – Elkrat
    Commented Jan 30, 2020 at 15:41
  • Thanks for the comment. Your question was not clear that the malware had already been cleaned. But using the process to truncate the post content at the point of the malware could be risky - all of the malware I've seen added to post content (or files) occurs at the beginning of the post/file content. I think it would be more time-consuming to try to tweak things programmatically (because of all of the iterations needed to test/determine effectiveness) than to manually inspect/clear each post. You could apply a "WHERE" in phpMyAdmin to search/show suspect posts, and only clean those manually. Commented Jan 30, 2020 at 18:27
  • This one is uniformly appended to the end of each post. It came from the fact that I had not excluded PHP execution on the uploads directory. I removed the malware code immediately, but it appended its java redirect to literally every post in the database, across 6 websites on the same host. I cleaned up all the published pages and for most of the other post types the remnants of the script never get printed on the front end. So I'm out of trouble but it drives me nuts that I can't get the SQL to hit properly, or get strtok() to detect it.
    – Elkrat
    Commented Jan 31, 2020 at 22:01
1

The answer my friend, was blowing in the regex of str_repl() - using the opening and closing tags to excise the malware script from post_content

First I find a few specific posts in the database with samples of the malware, to get the beginning string of it, in my case:

<script>;','OPR'

Then I wrote the following script to give me the precise length of the string and to see that it's stable over my sample rows in the database:

ignore_user_abort(true);
set_time_limit(30);

$path = $_SERVER['DOCUMENT_ROOT'];
require( $path.'/wp-load.php' );

$test_posts = array(1,15002,9664,3342);

foreach ($test_posts as $post){ 
    $post = get_post($post);
    $content = $post->post_content;
    $strlen = strlen($content);
    // to delete a script tag... preg_replace('/(<script>.+?)+(<\/script>)/i', '', $string);
    // starting virus text is <script>;','OPR'  must use backslash to escape the folling characters: \ ^ . $ | ( ) [ ] * + ? { } ,
    $new_content = preg_replace('/(<script>;\'\,\'OPR\'.+?)+(<\/script>)/i', '', $content);
    $new_strlen = strlen($new_content);
    $difference = $strlen - $new_strlen;
    echo 'Post '.$post->ID.': '.$strlen.'-'.$new_strlen.'=<b>'.($difference).'</b><br>';
}

echo '<br>Done.';

I saw the same number in all the entries: 2409. So I plug 2409 into my second script that actually does the post updating:

ignore_user_abort(true);
set_time_limit(90);

$path = $_SERVER['DOCUMENT_ROOT'];
require( $path.'/wp-load.php' );

$args = array(
        'post_type' => array(
                            'post',
                            'page',
                            'revision',
                            'item',
                            'wpsl_stores',
                            'nav_menu_item',
                            'accordion_menu',
                            'et_pb_layout',
                            'scheduled-action',
                            'wpsl_stores',
                            'popup_theme',
            ),
        'post_status' => array('publish', 'draft', 'inherit'),
        'posts_per_page' => -1,
    );

$posts = get_posts($args);

echo 'Post count is '.count($posts).'<br>';

$post_of_interest = 1;

foreach($posts as $post){
    $content = $post->post_content;
    $strlen = strlen($content);
    // starting virus text is <script>;','OPR'
    $new_content = preg_replace('/(<script>;\'\,\'OPR\'.+?)+(<\/script>)/i', '', $content);
    $new_strlen = strlen($new_content);
    if (($strlen - $new_strlen) == 2409){ // I know my target string has a specific number of characters
        echo '<br><b>*** HIT '.$post->ID.' ***</b><br>';
    } else {
        echo '-';
    }

    if($post->ID == $post_of_interest){
        echo '<br>'.$post_of_interest.' original strlen='.$strlen.' New strlen='.$new_strlen.' Difference='.($strlen - $new_strlen).'<br>';
    }
    $args = array(
            'ID' => $post->ID,
            'post_content' => $new_content,
        );
    wp_update_post( $args ); 
}

echo '<br>Done.';

Hope this helps someone.

2
  • Thanks for script, this was very helpful to delete malware javascript from wordpress database.
    – Vitalij
    Commented Mar 25, 2020 at 8:07
  • I'm going to add another answer, because I had to formulate another method when this one did not work.
    – Elkrat
    Commented Mar 26, 2020 at 12:09
1

A better approach to cleaning Wordpress posts is to delete all script tags along with their contents. I happen to know there should be no scripts inside my post content so this is safe for me. Make a quick backup of your database first, so you no cry.

Also if you pay attention to what the function author did, you can use this to strip any tags out of html. Alternatively, you can strip all tags EXCEPT the one you specify by leaving the last parameter false. I set it to true, which is what makes it strip ONLY the specified tag.

The function is from https://gist.github.com/marcanuy/7651298

BTW if anyone can chime in on how these people are injecting these scripts, I would be very grateful. I had this happen on a site that I've hardened - moved wp-config above the public html directory, changed permissions, moved the login address, etc. It's unnerving, because while the spammy stuff makes me look bad, the real problem is that it demonstrates a security vulnerability.

<?php
ignore_user_abort(true);
set_time_limit(90);

$path = $_SERVER['DOCUMENT_ROOT'];
require( $path.'/wp-load.php' );

$args = array(
        'post_type' => array( // add all your post types here, but be careful: it will strip all scripts from their content
                            'post',
                            'page',
                            'revision',
                            'item',
                            'wpsl_stores',
                            'nav_menu_item',
                            'accordion_menu',
                            'et_pb_layout',
                            'scheduled-action',
                            'wpsl_stores',
                            'popup_theme',
            ),
        'post_status' => array('publish', 'draft', 'inherit'),
        'posts_per_page' => -1,
    );

$posts = get_posts($args);

echo 'Post count is '.count($posts).'<br>';

foreach($posts as $post){

    $content = $post->post_content;
    $new_content = strip_tags_content($content, '<script>', true);

    $args = array(
            'ID' => $post->ID,
            'post_content' => $new_content,
        );
    wp_update_post( $args );    
}

echo '<br>Done.';

function strip_tags_content($text, $tags = '', $invert = FALSE) {

  preg_match_all('/<(.+?)[\s]*\/?[\s]*>/si', trim($tags), $tags);
  $tags = array_unique($tags[1]);

  if(is_array($tags) AND count($tags) > 0) {
    if($invert == FALSE) {
      return preg_replace('@<(?!(?:'. implode('|', $tags) .')\b)(\w+)\b.*?>.*?</\1>@si', '', $text);
    }
    else {
      return preg_replace('@<('. implode('|', $tags) .')\b.*?>.*?</\1>@si', '', $text);
    }
  }
  elseif($invert == FALSE) {
    return preg_replace('@<(\w+)\b.*?>.*?</\1>@si', '', $text);
  }
  return $text;
} 

?>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.