In this post, I will chronicle a remote operation that was executed. The operation took many weeks to achieve its target, I went from minimal information about the corporate into the total compromise of all customer’s users private information. The target is a large corporate with a sizable security program. For purposes of this blog, I will call them Target Corporate.
Target Corporate is a big E-commerce company that works in multiple countries. In this operation, I needed to do Web application Recon, source code review, exploitation, lateral movement and social engineering.
Let’s start
When I began this operation, I typically know nothing about their infrastructure operations. I needed to research the corporate and learn as much about their internal operations as possible.
I needed to look for outdated softwares and services that can be exploited. Maybe some applications with default passwords or custom web applications that might be vulnerable, looking for any resources that may be useful to gain more information.
Recon Phase
I spent hours doing recon at the target external systems. There were no obvious exploitable vulnerabilities on any of the applications that have been discovered. I found numerous web services and spent time investigating them. There was nothing in these tiny services.
I used Google search engine in addition to multiple tools to enumerate any reachable subdomains regarding Target Corporate, the most interesting subdomains were:
- management.target.com (A software to Control customer data and transactions)
- gmail.target.com (A software that is supported by google mail to provide a secure email login)
- wordpress.target.com (WordPress software installed)
(Those are not the real names of the subdomains)
At management.target.com, the application requests login credentials as it is only allowed to the stuff to login and control customers and transaction data. the application replies with a redirect to gmail.target.com once it is opened, which asks for stuff email credentials and after successful login, another redirect is sent back to management.target.com.
That means if I am able to access one of the stuff credentials who is authorized to access the management area then I am in.
Planning Phase
I created a small phishing email campaign on the emails that have been discovered during the Recon Phase.
I bought a domain and created a phishing email campaign with a Gmail phishing login page (because they use Gmail service as their main email service through gmail.target.com).
I sent few phishing emails not more than 20 email and the results were quite awful.
The domain that was used in my phishing campaign was reported by the employees and then blacklisted by Google chrome and Mozilla Firefox browsers!
I didn’t want to raise any alarms within the organization, It is a big known organization they have a security team that is aware of these types of attacks. I needed everybody to operate normally. As such, I wanted to limit my contact with the employees.
I noticed that management.target.com doesn’t use SAML authentication during logging in through gmail.target.com, instead, the employees receive “*.target.com” session after authentication that is used by management.target.com to verify if the session is valid and that the user is authorized to access the employees’ restricted area.
If I am able somehow to access any subdomain related to Target Corporate, then I can use this subdomain to hijack the “*.target.com” management session through a client-side attack.
Here was the written plan:
Hijacking the stuff session through XSS will not fit my situation because “*.target.com” session is protected by HttpOnly and Secure parameters.
Meaning I will need to hijack the session through a server-side page and to do this I will need to access any subdomain either by subdomain takeover or Remote Code Execution.
I chose the WordPress subdomain as a good target initiator for the mentioned attack. my goal was to upload a shell and gain an RCE through the WordPress script.
WordPress Assessment
I executed wpscan tool over the WordPress domain and noticed that the WordPress script is outdated, that was good news.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
WordPress version 5.2 identified (Insecure, released on 2019-05-07). | Detected By: Unique Fingerprinting (Aggressive Detection) | - https://***********************/wp-includes/sodium_compat/LICENSE md5sum is f578e4bb36468303006691e1a00ef996 | | [!] 2 vulnerabilities identified: | | [!] Title: WordPress <= 5.2.2 - Cross-Site Scripting (XSS) in URL Sanitisation | Fixed in: 5.2.3 | References: | - https://wpvulndb.com/vulnerabilities/9867 | - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16222 | - https://wordpress.org/news/2019/09/wordpress-5-2-3-security-and-maintenance-release/ | - https://github.com/WordPress/WordPress/commit/30ac67579559fe42251b5a9f887211bf61a8ed68 | | [!] Title: WordPress 5.0-5.2.2 - Authenticated Stored XSS in Shortcode Previews | Fixed in: 5.2.3 | References: | - https://wpvulndb.com/vulnerabilities/9864 | - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16219 | - https://wordpress.org/news/2019/09/wordpress-5-2-3-security-and-maintenance-release/ | - https://fortiguard.com/zeroday/FG-VD-18-165 | - https://www.fortinet.com/blog/threat-research/wordpress-core-stored-xss-vulnerability.html |
Also, many plugins have been discovered, one of them was outdated and vulnerable:
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 |
[+] userpro | Location: https://***********************/wp-content/plugins/userpro/ | | Detected By: Urls In Homepage (Passive Detection) | | [!] 5 vulnerabilities identified: | | [!] Title: UserPro <= 4.9.17 - Authentication Bypass | Fixed in: 4.9.17.1 | References: | - https://wpvulndb.com/vulnerabilities/8950 | - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16562 | - https://www.exploit-db.com/exploits/43117/ | - https://codecanyon.net/item/userpro-user-profiles-with-social-login/5958681?s_rank=9 | | [!] Title: UserPro <= 4.9.23 - Unauthenticated Cross-Site Scripting (XSS) | Fixed in: 4.9.24 | References: | - https://wpvulndb.com/vulnerabilities/9124 | - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16285 | - https://risataim.blogspot.com/2018/09/xss-en-plugin-userpro-de-wordpress.html | - https://codecanyon.net/item/userpro-user-profiles-with-social-login/5958681 | - https://userproplugin.com/ | | [!] Title: UserPro <= 4.9.20 - User Registration With Administrator Role | Fixed in: 4.9.21 | References: | - https://wpvulndb.com/vulnerabilities/9195 | - https://packetstormsecurity.com/files/151022/wpuserpro-escalate.txt | | [!] Title: UserPro <= 4.9.27 - User Registration With Administrator Role | Fixed in: 4.9.28 | References: | - https://wpvulndb.com/vulnerabilities/9202 | - https://codecanyon.net/item/userpro-user-profiles-with-social-login/5958681 | | [!] Title: UserPro <= 4.9.34 - Unauthenticated Reflected XSS | References: | - https://wpvulndb.com/vulnerabilities/9815 | - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14470 | - https://www.exploit-db.com/exploits/47304/ | - https://codecanyon.net/item/userpro-user-profiles-with-social-login/5958681 |
Based on wpscan results, the UserPro plugin was outdated and should be vulnerable, the following reflected XSS regarding CVE 2019-1447 was the only finding that is exploitable from my side.
The exploit is written in exploit-db:
https://www.exploit-db.com/exploits/47304
OK, now there is a reflected XSS but still not useful as long as I didn’t gain Remote Code Execution over the application.
My plan was to find a way to send the malicious URL to one of the WordPress admins and trigger the XSS exploit that adds a malicious plugin to the WordPress application so that I can gain RCE using the malicious installed plugin, but this was plan B because it needs user interaction through social engineering.
Back to plan A, I decided to manually do a source code review on these plugins each one by one.
I downloaded the theme as well named pierce theme and started to check the source code for any stuff that looks suspicious.
I decided to keep attention to the server-side findings, first I discovered three local file include vulnerabilities that effects piercetheme.
In file function.php:
2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 |
if(!empty($_GET['payment_response_listing'])) { $sk = $_GET['payment_response_listing']; include('lib/gateways/listing_response_'.$sk.'.php'); die(); } if($jb_action == 'pay_featured_credits' ) { include('lib/gateways/pay_featured_listing_credits.php'); die(); } if (!empty($_GET['payment_response'])) { $sk = $_GET['payment_response']; include('lib/gateways/'.$sk.'.php'); die(); } if (!empty($_GET['pay_for_item'])) { $sk = $_GET['pay_for_item']; include('lib/gateways/'.$sk.'.php'); die(); } |
Three IF conditions contain similar code but different parameter names (payment_response_listing, payment_response, pay_for_item) all are vulnerable to Local File Include, only one will be exploited at once because function die is added by the end of each if condition.
The previous piece of code that is vulnerable to Local File Include is located in a function called PricerrTheme_template_redirect.
If I am able to control the application behavior to run the mentioned function, then LFI will be exploitable.
On the beginning of the functions.php, there is a hook to the vulnerable function:
128 129 130 |
add_filter('template_redirect', 'PricerrTheme_template_redirect'); add_action('widgets_init', 'PricerrTheme_framework_init_widgets' ); add_action("manage_posts_custom_column", "PricerrTheme_my_custom_columns"); |
WordPress defined filter hooks as follows:
1 |
WordPress offers filter hooks to allow plugins to modify various types of internal data at runtime. A plugin can modify data by binding a callback to a filter hook. When the filter is later applied, each bound callback is run in order of priority, and given the opportunity to modify a value by returning a new value. |
I will simplify it, a hook is creating a function that will execute instead of another WordPress core function, In my case, it is template_redirect function.
After more tests, I observed that function template_redirect executes automatically by WordPress as it is included in wp-includes/template-loader.php file which means the code path will go through the vulnerable function.
To exploit the Local File Include, I needed to add any of the three vulnerable GET request inputs, for example wordpress.target.com/?payment_response=Page_name
The main issue was that, I am not controlling the included files extension meaning I can only exploit the local file include by including PHP files only!
Also, I am not controlling the wrapper through the user-input meaning I can’t use php:// wrapper to read the files source code, for this, the local file include was still useless!
SQL INJECTION
I started to read many and many files for hours until I reached file lib/gateways/paypal_response.php:
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 76 77 78 79 80 81 82 83 84 85 |
if(isset($_POST['custom'])) { $cust = $_POST['custom']; $cust = explode("|",$cust); $pid = $cust[0]; $uid = $cust[1]; $datemade = $cust[2]; $xtra1 = $cust[3]; $xtra2 = $cust[4]; $xtra3 = $cust[5]; $xtra4 = $cust[6]; $xtra5 = $cust[7]; $xtra6 = $cust[8]; $xtra7 = $cust[9]; $xtra8 = $cust[10]; $xtra9 = $cust[11]; $xtra10 = $cust[12]; //--------------------------------------------------- $my_arr = array(); $my_arr['extra1'] = 0; $my_arr['extra2'] = 0; $my_arr['extra3'] = 0; $my_arr['extra4'] = 0; $my_arr['extra5'] = 0; $my_arr['extra6'] = 0; $my_arr['extra7'] = 0; $my_arr['extra8'] = 0; $my_arr['extra9'] = 0; $my_arr['extra10'] = 0; if(!empty($xtra1)) $my_arr['extra' . $xtra1] = 1; if(!empty($xtra2)) $my_arr['extra' . $xtra2] = 1; if(!empty($xtra3)) $my_arr['extra' . $xtra3] = 1; if(!empty($xtra4)) $my_arr['extra' . $xtra4] = 1; if(!empty($xtra5)) $my_arr['extra' . $xtra5] = 1; if(!empty($xtra6)) $my_arr['extra' . $xtra6] = 1; if(!empty($xtra7)) $my_arr['extra' . $xtra7] = 1; if(!empty($xtra8)) $my_arr['extra' . $xtra8] = 1; if(!empty($xtra9)) $my_arr['extra' . $xtra9] = 1; if(!empty($xtra10)) $my_arr['extra' . $xtra10] = 1; $xtra1 = $my_arr['extra1']; $xtra2 = $my_arr['extra2']; $xtra3 = $my_arr['extra3']; $xtra4 = $my_arr['extra4']; $xtra5 = $my_arr['extra5']; $xtra6 = $my_arr['extra6']; $xtra7 = $my_arr['extra7']; $xtra8 = $my_arr['extra8']; $xtra9 = $my_arr['extra9']; $xtra10 = $my_arr['extra10']; //--------------------------------------------------- $payment_status = $_POST['payment_status']; if(1): //$payment_status == "Completed"): $price = get_post_meta($pid, 'price', true); if(empty($price)) $price = get_option('pricerrTheme_price'); $mc_gross = $_POST['mc_gross'] - $_POST['mc_fee']; //----------------------------------------------------- global $wpdb; $pref = $wpdb->prefix; $s1 = "select * from ".$pref."job_orders where pid='$pid' AND uid='$uid' AND date_made='$datemade'"; $r1 = $wpdb->get_results($s1); |
A user-input is sent on line 8 then decomposed using function explode into many parameters:
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$cust = $_POST['custom']; $cust = explode("|",$cust); $pid = $cust[0]; $uid = $cust[1]; $datemade = $cust[2]; $xtra1 = $cust[3]; $xtra2 = $cust[4]; $xtra3 = $cust[5]; $xtra4 = $cust[6]; $xtra5 = $cust[7]; $xtra6 = $cust[8]; $xtra7 = $cust[9]; $xtra8 = $cust[10]; $xtra9 = $cust[11]; $xtra10 = $cust[12]; |
The following three parameters $pid, $uid and $datemade are then sent to a SQL statement without any filters or sanitization:
84 85 |
$s1 = "select * from ".$pref."job_orders where pid='$pid' AND uid='$uid' AND date_made='$datemade'"; $r1 = $wpdb->get_results($s1); |
It is a SQL injection, but which type of SQL injection is it?
To answer this question I read the remaining source code:
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
if(count($r1) == 0) { $nts = addslashes($nts); $s1 = "insert into ".$pref."job_orders (pid,uid,date_made, mc_gross, notes_to_seller, extra1, extra2, extra3, extra4, extra5, extra6, extra7, extra8, extra9, extra10) values('$pid','$uid','$datemade','$mc_gross', '$nts','$xtra1','$xtra2','$xtra3','$xtra4','$xtra5','$xtra6','$xtra7','$xtra8','$xtra9','$xtra10')"; $wpdb->query($s1); //-------------- $s1 = "select * from ".$pref."job_orders where pid='$pid' AND uid='$uid' AND date_made='$datemade'"; $r1 = $wpdb->get_results($s1); $orderid = $r1[0]->id; //------------------------ $g1 = "insert into ".$pref."job_chatbox (datemade, uid, oid, content) values('$datemade','0','$orderid','$ccc')"; $wpdb->query($g1); //-------------- $uid_a = get_post($pid); $uid_a = $uid_a->post_author; $s1 = "insert into ".$pref."job_ratings (orderid, uid, pid) values('$orderid','$uid_a','$pid')"; $wpdb->query($s1); $sales = get_post_meta($pid,'sales',true); if(empty($sales)) $sales = 1; else $sales = $sales + 1; update_post_meta($pid,'sales',$sales); //--------------- // email to the owner of the job $post = get_post($pid); PricerrTheme_send_email_when_job_purchased_4_buyer($orderid, $pid, $uid, $post->post_author); PricerrTheme_send_email_when_job_purchased_4_seller($orderid, $pid, $post->post_author, $uid); //--------------- $instant = get_post_meta($pid,'instant',true); if($instant == "1") { $tm = current_time('timestamp',0); $s = "update ".$wpdb->prefix."job_orders set done_seller='1', date_finished='$tm' where id='$orderid' "; $wpdb->query($s); $ccc = __('Delivered','PricerrTheme'); $g1 = "insert into ".$wpdb->prefix."job_chatbox (datemade, uid, oid, content) values('$tm','-1','$orderid','$ccc')"; $wpdb->query($g1); PricerrTheme_send_email_when_job_delivered($orderid, $pid, $uid); } //--------------- $admin_email = get_bloginfo('admin_email'); $message = sprintf(__('A new job has been purchased on your site: <a href="%s">%s</a>', 'PricerrTheme'), get_permalink($pid), $post->post_title); PricerrTheme_send_email($admin_email, sprintf(__('New Job Purchased on your site - %s', 'PricerrTheme'), $post->post_title), $message); } endif; } |
After the injection, The code goes through an IF condition if the query didn’t return any true result.
In the IF condition in lines 144 and 155, an email is sent to the WordPress admin. That means If I executed SQL injection attacks then the application will flood the admin email. This is not a stealth attack at all!
There was only one option which is to prevent the application path from entering through the following IF condition:
88 |
if(count($r1) == 0) |
This means the injected payloads should always return at least one raw otherwise the application path will enter the IF condition and thus, the admin will be notified about my actions, also the SQL injection will be time-based because the application path will end as long as the if condition is false.
The vulnerable file doesn’t include the wp-config.php file (it is a file in the WordPress scripts that contains the database configurations). furthermore, there are few WordPress functions used that will not be clarified if I directly access the vulnerable page.
This page should be included by another file that includes the “wp-config.php” and the WordPress core functions otherwise the page will always through an error and the SQL injection will be unexploitable.
Now I can use the discovered Local File Include to include the paypal_response.php page that is vulnerable to time-based SQL injection using the following request.
1 |
https://*********************/?payment_response=paypal_response |
I tested the time-based SQL injection payloads but for the first few requests the application replies without time delay, I installed the vulnerable theme on my machine localhost and counted the number of columns in table job_orders, it was 33 columns. Next, I changed the payload by adding “or 1=1” to always return true and a union select that contains the number of columns and function sleep(10) that if executed the database host will go to the idle mode for 10 seconds, thus, the following request was crafted:
The application replied after 11 seconds. I really like this final scene that contains the proof of concept regarding the SQL injection.
In part 2. I will outline how I went from a time-based SQL injection in a subdomain into having full access to the target Corporate’s management area.