Security Issues with Self-Hosted WordPress Sites

Discussion in 'Server Operation' started by BouJJoux, Mar 7, 2025.

Tags:
  1. BouJJoux

    BouJJoux New Member

    Hi there,

    I am reaching out to you because, for the past few months, I have been experiencing issues with my self-hosted web server and my WordPress sites.

    I am using the latest versions of Debian, Nginx, and PHP 8.2 with FPM. To manage my various sites, I use ISPConfig, which was installed using the automatic setup and installation script available on your website.

    For several weeks now, all the WordPress sites hosted on my server have been repeatedly hacked using the same method :

    • Numerous different IP addresses send GET and then POST requests to various WordPress PHP files (wp-cron, wp-load) ;

    • Randomly named PHP files, such as “bdb7871b1c3c28ad1a33ea14cc307cd1.php”, are created at the root of the websites, and multiple POST requests are sent to these files ;

    • These PHP files are subsequently deleted ;

    • Certain WordPress PHP files are modified to insert malicious code (often different files each time – in plugins or themes) ;

    • Administrator accounts, posts, and comments are created.

    In my wp-config.php file, the constant define('DISALLOW_FILE_EDIT', true); is correctly set. Passwords are systematically changed to complex ones, and administrator usernames are also modified.

    I use the “Wordfence” and “Malcare” plugins to protect these websites. The infections are always detected retrospectively by the paid version of “Malcare” after the site has already been hacked. Malcare is able to identify the modified files and restore them to their original state by removing the malicious code.

    These issues have persisted for several months, and I have not been able to eliminate them.

    Although “Malcare” disinfects the sites each time, they get reinfected a few days or even hours later, always through the same process.

    I suspect a misconfiguration in Nginx or PHP that allows malicious code execution. I have attached the Nginx vhost configuration for one of my sites (they are all identical) :

    Code:
    server {
            listen *:80;
            listen [::]:80;
            listen *:443 ssl http2;
    
            ssl_protocols TLSv1.3 TLSv1.2;
            listen [::]:443 ssl http2;
            ssl_certificate /var/www/clients/client1/web1/ssl/my-wordpress-website.com-le.crt;
            ssl_certificate_key /var/www/clients/client1/web1/ssl/my-wordpress-website.com-le.key;
    
            server_name my-wordpress-website.com www.my-wordpress-website.com;
    
            root   /var/www/my-wordpress-website.com/web/;
            disable_symlinks if_not_owner from=$document_root;
    
            if ($scheme != "https") {
                rewrite ^(?!/\.well-known/acme-challenge)/ https://$http_host$request_uri? permanent;
            }
    
            index index.html index.htm index.php index.cgi index.pl index.xhtml standard_index.html;
    
            error_log /var/log/ispconfig/httpd/my-wordpress-website.com/error.log;
            access_log /var/log/ispconfig/httpd/my-wordpress-website.com/access.log combined;
    
            location ~ /\. {
                deny all;
            }
    
            location ^~ /.well-known/acme-challenge/ {
                access_log off;
                log_not_found off;
                auth_basic off;
                root /usr/local/ispconfig/interface/acme/;
                autoindex off;
                index index.html;
                try_files $uri $uri/ =404;
            }
    
            location = /favicon.ico {
                log_not_found off;
                access_log off;
                expires max;
                add_header Cache-Control "public, must-revalidate, proxy-revalidate";
            }
    
            location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
            }
            location ~ \.php$ {
                try_files /44522a2e963d90c425cae74276426d64.htm @php;
            }
    
            location @php {
                try_files $uri =404;
                include /etc/nginx/fastcgi_params;
                fastcgi_pass unix:/var/lib/php8.2-fpm/web1.sock;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_intercept_errors on;
            }
    
            location ~ ^/wp-content/uploads/sucuri {
              deny all;
            }
    
            location ~ ^/wp-content/updraft {
              deny all;
            }
    
            location ~* .(pl|cgi|py|sh|lua|asp)$ {
               return 444;
            }
    
            location ~* /(wp-config.php|readme.html|license.txt|nginx.conf) {
               deny all;
            }
    
            location /wp-content/uploads/ {
                location ~ \.php$ {
                    deny all;
                }
                location ~ \.(png|jpe?g)$ {
                    add_header Vary "Accept-Encoding";
                    add_header "Access-Control-Allow-Origin" "*";
                    add_header Cache-Control "public, no-transform";
                    access_log off;
                    log_not_found off;
                    expires max;
                    try_files $uri  $uri =404;
                }
            }
    
            location /xmlrpc.php {
              deny all;
              access_log off;
              log_not_found off;
              return 444;
            }
    
            location ^~ /wp-admin/install.php {
              deny all;
              error_page 403 =404 / ;
            }
    
            location ~* /(?:uploads|files)/.*\.php$ {
               deny all;
            }
    
            location ~* ^/(wp-content)/(.*?)\.(zip|gz|tar|bzip2|7z)\$ {
              deny all;
            }
    
            location ~* ^/wp-content/plugins/.+\.(txt|log|md)$ {
                  deny all;
                  error_page 403 =404 / ;
            }
    
            location ~* ^/wp-content/themes/.+\.(txt|log|md)$ {
                  deny all;
                  error_page 403 =404 / ;
            }
    
            location ~* ^/wp-content/uploads/.*.(html|htm|shtml|php|js|swf)$ {
                deny all;
            }
    
            location ~* ^/(license.txt|wp-includes/(.*)/.+\.(js|css)|wp-admin/(.*)/.+\.(js|css))$ {
                sub_filter_types text/css text/javascript text/plain;
                sub_filter_once on;
                sub_filter ';' '; /* $msec */ ';
            }
    
            location ~* /(?:uploads|files|wp-content|wp-includes|akismet)/.*.php$ {
                deny all;
                access_log off;
                log_not_found off;
            }
    
            location ~ /\.(svn|git)/* {
                deny all;
                access_log off;
                log_not_found off;
            }
            location ~ /\.ht {
                deny all;
                access_log off;
                log_not_found off;
            }
            location ~ /\.user.ini {
                deny all;
                access_log off;
                log_not_found off;
            }
    
            location ~* ^.+\.(bak|log|old|orig|original|php#|php~|php_bak|save|swo|swp|sql)$ {
                deny all;
                access_log off;
                log_not_found off;
            }
    
            location ~ \.user\.ini$ {
                deny all;
            }
    
            location ~* ^/(?:wp-content|wp-includes)/.*\.php$ {
                deny all;
            }
    
            location ~* ^/(?:xmlrpc\.php|wp-links-opml\.php|wp-config\.php|wp-config-sample\.php|wp-comments-post\.php|readme\.html|license\.txt)$ {
                deny all;
            }
    
            location ~* ^.+\.(curl|heic|swf|tiff|rss|atom|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
                access_log off;
                log_not_found off;
                expires max;
            }
    
            location ~* \.(?:eot|otf|ttf|woff|woff2)$ {
                expires max;
                access_log off;
                add_header Cache-Control "public";
            }
    
            location ~* \.(?:svg|svgz|mp4|webm)$ {
                expires max;
                access_log off;
                add_header Cache-Control "public";
            }
    
            location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|aac|m4a|mp3|ogg|ogv|webp)$ {
              expires 1M;
              access_log off;
              add_header Cache-Control "public";
            }
    
            location ~* \.(?:css(\.map)?|js(\.map)?)$ {
                add_header "Access-Control-Allow-Origin" "*";
                access_log off;
                log_not_found off;
                expires 30d;
            }
    
            location ~* \.(?:css|js)$ {
              expires 1y;
              access_log off;
              add_header Cache-Control "public";
            }
    
            location ~* \.(html)$ {
              expires 7d;
              access_log off;
              add_header Cache-Control "public";
            }
    
            location ~ /\. {
                deny all;
            }
    
            location ~* "\.(old|orig|original|php#|php~|php_bak|save|swo|aspx?|tpl|sh|bash|bak?|cfg|cgi|dll|exe|git|hg|ini|jsp|log|mdb|out|sql|svn|swp|tar|rdf)$" {
                deny all;
            }
    
            location ~* "(eval\()" {
                deny all;
            }
            location ~* "(127\.0\.0\.1)" {
                deny all;
            }
            location ~* "([a-z0-9]{2000})" {
                deny all;
            }
            location ~* "(javascript\:)(.*)(\;)" {
                deny all;
            }
    
            location ~* "(base64_encode)(.*)(\()" {
                deny all;
            }
            location ~* "(GLOBALS|REQUEST)(=|\[|%)" {
                deny all;
            }
            location ~* "(<|%3C).*script.*(>|%3)" {
                deny all;
            }
            location ~ "(\\|\.\.\.|\.\./|~|`|<|>|\|)" {
                deny all;
            }
            location ~* "(boot\.ini|etc/passwd|self/environ)" {
                deny all;
            }
            location ~* "(thumbs?(_editor|open)?|tim(thumb)?)\.php" {
                deny all;
            }
            location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
                deny all;
            }
            location ~* "(https?|ftp|php):/" {
                deny all;
            }
            location ~* "(=\\\'|=\\%27|/\\\'/?)\." {
                deny all;
            }
            location ~ "(\{0\}|\(/\(|\.\.\.|\+\+\+|\\\"\\\")" {
                deny all;
            }
            location ~ "(~|`|<|>|:|;|%|\\|\s|\{|\}|\[|\]|\|)" {
                deny all;
            }
            location ~* "/(=|\$&|_mm|(wp-)?config\.|cgi-|etc/passwd|muieblack)" {
                deny all;
            }
    
            location ~* "(&pws=0|_vti_|\(null\)|\{\$itemURL\}|echo(.*)kae|etc/passwd|eval\(|self/environ)" {
                deny all;
            }
            location ~* "/(^$|mobiquo|phpinfo|shell|sqlpatch|thumb|thumb_editor|thumbopen|timthumb|webshell|config|settings|configuration)\.php" {
                deny all;
            }
    
            location / {
                try_files $uri $uri/ /index.php?$args;
            }
            
            rewrite /wp-admin$ $scheme://$host$uri/ permanent;
    
            client_max_body_size 64M;
    
            fastcgi_buffers 16 16k;
            fastcgi_buffer_size 32k;
            proxy_buffer_size   128k;
            proxy_buffers   4 256k;
            proxy_busy_buffers_size   256k;
    }
    
    Do you have any ideas on how to resolve this issue, or any suggestions that could help me troubleshoot it…

    Regards,
     
  2. till

    till Super Moderator Staff Member ISPConfig Developer

    It's quite unlikely that the server configuration causes this. But as you say they infect all sites, you may either use the same WP plugin or theme or custom code in all sites and this part of the code has a security hole, or your server is infected on the root level. Check the access.log files of the sites to find the entry point, the entrypoint is in most cases a POST request, so when a site got re-infected, try to find the first post request from that IP.

    Also, the attackers might have installed cronjobs outside of ISPConfig to reinfect the sites. If a site has e.g. ID 15, then it is run by user "web15". Check fro cronjobs like this:

    crontab -l -u web15

    if its lists any cronjobs for user web15, delete them.
     
    BouJJoux likes this.
  3. till

    till Super Moderator Staff Member ISPConfig Developer

    You might also want to check the whole server with Lynis, its available on Debian and Ubuntu as a package:

    apt install lynis
     
    BouJJoux and Turgut Kalfaoglu like this.
  4. Turgut Kalfaoglu

    Turgut Kalfaoglu Member HowtoForge Supporter

    Thank you for letting me know of Lynis. It's scanning my system now :)
     
    BouJJoux likes this.
  5. BouJJoux

    BouJJoux New Member

    Hi there,
    Thanks for your replies.
    I do indeed use multiple plugins on my WordPress sites. All the plugins I use are well-known and regularly updated, and I do not use any custom code.
    I have already checked my server logs, and I can see numerous POST requests to PHP files within my plugins, such as:
    "/wp-content/plugins/updraftplus/vendor/guzzle/guzzle/src/Guzzle/Parser/Message/form_edit.inc.php",
    "/wp-content/backups-dup-pro/installer/original_files_aa539b9-13140051/phplot.php",
    "/wp-content/plugins/complianz-gdpr/functions.php",
    "/wp-content/plugins/woocommerce-pdf-vouchers/includes/templates/recipient-fields/woo-vou-recipient-message.php",
    "/wp-content/plugins/advanced-access-manager/application/Backend/View/PostOptionList.php",
    ... all at a very high rate, with different IP addresses originating from various countries.

    These POST requests sometimes result in a 403 response code and, randomly, a 200 response code.

    Regarding cron jobs, after thoroughly checking site by site, I have found no scheduled tasks.

    I have already run a scan with ClamAV, which came back negative, and also with Lynis, where I obtained a score of 75 with no critical issues.

    I am completely lost...
     
    Turgut Kalfaoglu likes this.
  6. Turgut Kalfaoglu

    Turgut Kalfaoglu Member HowtoForge Supporter

    Modsecurity might help there, and religiously updating your wordpress sites.
    Installating modsecurity is easy, but it requires tweaking afterwards to ensure that it doesn't block legitimate requests. I also like Wordfence's 'scan' feature. It tells me which addons have an update, which ones are no longer supported, and of course the occasional virus. Those POST requests you saw are all attempts at finding an addon with a weakness. Once your server replies with 200, they will attack through that opening.
     
  7. nhybgtvfr

    nhybgtvfr Well-Known Member HowtoForge Supporter

    it's no good just cleaning up modified wordpress files after a hack... you'll always miss something..
    and it's not just the obvious php/code files... the buggers often hide code within fake image files.

    personally. if hacked.. i would just save the uploads folder.. delete everything else.
    reinstalll wordpress, themes and plugins with fresh downloads from source.
    restore the uploads folder. (check every file in the uploads folder first)
    along with checking for any suspicious cron jobs, check the database. you may have to manually check every line... :eek:
    if the hackers have had enough access to create new files, they could have easily hidden code in the wordpress database.
    cleaning the files is pointless if the database is still dirty. loading a particular page could load content containing code downloading and installing all sorts of new crap back into your site.
     
    till likes this.
  8. Turgut Kalfaoglu

    Turgut Kalfaoglu Member HowtoForge Supporter

    Sometimes even "clamdscan" helps too.. I have even did things like "grep -r eval *" in the wordpress directory - usually hack files have some kinda encoding and eval finds them.
     
  9. till

    till Super Moderator Staff Member ISPConfig Developer

    Or you ran an ISPProtect scan, which also finds Malware and such Malware image files. You can use ISPProtect for one scan for free without registration: https://ispprotect.com/
     
  10. BouJJoux

    BouJJoux New Member

    Hi there,
    Thank you for help.

    I had already tested some of the solutions you mentioned (including completely recreating a website), without any "positive" results. I believe the issue really comes from a misconfiguration of NGINX on my end.

    I am indeed considering gradually switching to Apache to take advantage of and configure ModSecurity more easily, especially since Apache seems to be slightly more user-friendly with WordPress (automatic management of the .htaccess file).

    I have continued searching on forums and in my php.ini, I have edited the value of "cgi.fix_pathinfo". Since making this change, I have not been reinfected.

    If this can help others who are in the same situation…

    Regarding ISPProtect, thank you for this scan. I ran a scan on my servers (those containing infected websites), and nothing was detected.
     
  11. remkoh

    remkoh Active Member HowtoForge Supporter

    No way this is Nginx related.
    PHP maybe, if you deviated from ISPC's and PHP's recommended settings.

    I've had multiple wordpress websites hacked over the years. Mostly on Apache webservers.
    It was ALWAYS due to the customer (or it's websitebuilder) not updating anything once the website went live.
    Often clueless about that they should do that and how to do it.
     
    michelangelo likes this.

Share This Page