Sensitive Data Disclosure in WordPress Plugin Amelia < 1.0.49
2022-3-30 00:0:0
Author: blog.huli.tw(查看原文)
阅读量:39
收藏
Amelia is a WordPress plugin for booking systems developed by TNS. With 40,000+ active installations, it has been used for the clinic, hair salon, tutor, and so on.
In March, we studied the source code of Amelia and found three vulnerabilities in the end:
CVE-2022-0720 Amelia < 1.0.47 - Customer+ Arbitrary Appointments Update and Sensitive Data Disclosure (CVSS 6.3)
CVE-2022-0837 Amelia < 1.0.48 - Customer+ SMS Service Abuse and Sensitive Data Disclosure (CVSS 5.4)
By exploiting these vulnerabilities, a malicious actor could get all the customer’s data, including name, phone, and booking details.
In this article, I will talk about the code structure of Amelia and the details of three vulnerabilities.
A Brief Introduction to Amelia
After installing Amelia plugin, the admin can create a new booking page:
As a customer, basic personal information like name and email should be provided before making a booking:
After the customer finished booking, Amelia will create a new low-privilege account for it and send a reset password email to enable the account.
Then, the customer can log into WordPress and manage their bookings:
How WordPress Plugin Works and the Code Structure of Amelia
When developing a WordPress plugin, the developer uses add_action to add a hook to the WordPress system. The hook function will be invoked when the corresponding action has been called.
The action starts with wp_ajax_nopriv_ can be invoked via wp-admin/admin-ajax.php, following is the excerpt of admin-ajax.php:
The difference between wp_ajax_wpamelia_api and wp_ajax_nopriv_wpamelia_api is that the former requires authenticated user to perform the action, while the latter requires none.
As you can see, many plugins choose to handle both actions in the same place, to deal with the permission check itself.
In wpAmeliaApiCall, a few routes have been registered:
There are a few files in src/Infrastructure/Routes folder for handling routing. For example, src/Infrastructure/Routes/User/User.php is responsible for the routing of /users:
For example, when the request URL is /wordpress/wp-admin/admin-ajax.php?action=wpamelia_api&call=/users/wp-users, query string is action=wpamelia_api&call=/users/wp-users. After AMELIA_ACTION_SLUG has been replaced with empty string, the remaining part is /users/wp-users, that’s how the system handles routes.
Let’s check GetWPUsersController::class, the corresponding controller for /users/wp-users:
We can see a classic design pattern here: Command Pattern, every action is a command. The command will be processed by its parent class AmeliaBooking\Application\Controller\Controller:
After instantiated the command, it called $this->commandBus->handle($command). Following is the excerpt of src/Infrastructure/ContainerConfig/command.bus.php:
publicfunctionhandle(GetWPUsersCommand $command) { if (!$this->getContainer()->getPermissionsService()->currentUserCanRead(Entities::EMPLOYEES)) { thrownew AccessDeniedException('You are not allowed to read employees.'); }
if (!$this->getContainer()->getPermissionsService()->currentUserCanRead(Entities::CUSTOMERS)) { thrownew AccessDeniedException('You are not allowed to read customers.'); }
If the command is about deleting, need to pass the check of wp_verify_nonce, what is it?
wp_verify_nonce is a function provided by WordPress to perform security check. This nonce is generated in admin page via var wpAmeliaNonce = '<?php echo wp_create_nonce('ajax-nonce'); ?>';, and the nonce is actually the result of hashing.
In theory, it’s not possible to spoof the nonce unless you can get the salt, which is generated randomly at install time:
So, we can ensure that only authenticated users have the ability to use certain features via wp_verify_nonce.
Phew! That’s all about the structure of Amelia. It’s elegant compared to other plugins, and it’s easy to find what you want.
Finally, it’s time to talk about the vulnerabilities.
CVE-2022-0720: Amelia < 1.0.47 - Customer+ Arbitrary Appointments Update and Sensitive Data Disclosure
There are two modules for managing the booking. One is “Appointment”, the other is “Booking”, it’s a one-to-many relationship. One appointment can have multiple bookings.
Take /appointments/{id:[0-9]+} as an example, you can’t see other customer’s booking because there is a line to remove it in GetAppointmentCommandHandler:
1
$customerAS->removeBookingsForOtherCustomers($user, new Collection([$appointment]));
And here is the file for updating appointment(UpdateAppointmentCommandHandler.php):
if ($userAS->isProvider($user) && !$settingsDS->getSetting('roles', 'allowWriteAppointments')) { thrownew AccessDeniedException('You are not allowed to update appointment'); }
In the beginning, there are two checks. The first one is to check if the user is logged in, and the second one is to check if the user’s role is “Provider”.
There are a few roles in Amelia: Customer, (Service) Provider, and Admin. So, as a customer, we can pass the check and update other customer’s appointments!
When the customer manages their bookings, they use another module called “booking” because the appointment module is for providers. I guess that’s why there is no permission check for the customer role because the developer has a false assumption that the customer won’t access this endpoint at all.
What can we do besides updating the booking? Let’s see the response:
There is a field called “info”, it contains the personal data of the customer who booked it. This field is added by processBooking in src/Application/Services/Reservation/AbstractReservationService.php :
To sum up, a customer can update other customers’ appointments and see their personal information due to a flawed permission check. Moreover, the appointment ID is a serial number so it’s easy to enumerate.
Remediation
In 1.0.47, they made two changes.
The first one is to implement the permission check for customer:
1 2 3
if ($userAS->isCustomer($user)) { thrownew AccessDeniedException('You are not allowed to update appointment'); }
The other is about the permission check for routes, from the positive list to the negative list, only a few commands are accessible without logging in:
CVE-2022-0825: Amelia < 1.0.49 - Customer+ Arbitrary Appointments Status Update
This vulnerability is quite similar to the previous one, it’s also about permission check.
The route for updating appointment status is $app->post('/appointments/status/{id:[0-9]+}', UpdateAppointmentStatusController::class);, and the main handler is src/Application/Commands/Booking/Appointment/UpdateAppointmentStatusCommandHandler.php.
There is a permission check in the very beginning:
1 2 3 4 5
if (!$this->getContainer()->getPermissionsService()->currentUserCanWriteStatus(Entities::APPOINTMENTS)) { thrownew AccessDeniedException('You are not allowed to update appointment status'); }
Let’s dive in and see what is the implementation of currentUserCanWriteStatus:
It was noteworthy that if the user is null, it will be treated as a customer. To check if a role has certain permission, we need to see the capabilities table in src/Infrastructure/WP/config/Roles.php:
amelia_write_status_appointments is true, so the customer has permission to update the appointment status.
The rest part is just like the last vulnerability, the response of updating the appointment has a field called info, which contains the personal information of the customer who booked it.
By the way, the vulnerability is pre-auth before 1.0.48, because in 1.0.47 the permission check for routes is incomplete, an unauthenticated user can access this endpoint as well.
Remediation
Customer role has no permission of amelia_write_status_appointments since 1.0.49.
CVE-2022-0837: Amelia < 1.0.48 - Customer+ SMS Service Abuse and Sensitive Data Disclosure
Let’s see the last vulnerability, it’s still about permission check.
The route is $app->post('/notifications/sms', SendAmeliaSmsApiRequestController::class);, and the handler is SendAmeliaSmsApiRequestCommandHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
publicfunctionhandle(SendAmeliaSmsApiRequestCommand $command) { $result = new CommandResult();
Sending test notifications still costs real money, so we can drain out the account by keep calling this API.
Remediation
In 1.0.48, they added permission check in the controller:
1 2 3
if (!$this->getContainer()->getPermissionsService()->currentUserCanWrite(Entities::NOTIFICATIONS)) { thrownew AccessDeniedException('You are not allowed to send test email'); }
Conclusion
When the software becomes more and more complex, developers usually overlook some basic permission checks and have false assumptions sometimes.
For example, although front-end customers can’t see appointments-related API because it’s for providers, we can still find those API endpoints by looking at the source code in WordPress SVN.
Developers should be cautions with those authorizations when implementing those features, to make sure the current user has permission to do certain operations.
Disclosure timeline
2022-02-20 Report updating appointment vulnerability via WPScan, reserved CVE-2022-0720 2022-03-01 1.0.47 is published, fixed CVE-2022-0720. WPScan 2022-03-02 Report updating appointment vulnerability via WPScan, reserved CVE-2022-0825 2022-03-03 Report SMS related vulnerability via WPScan, reserved CVE-2022-0837 2022-03-09 1.0.48 is published, fixed CVE-2022-0837. WPScan 2022-03-14 1.0.49 is published, fixed CVE-2022-0825. WPScan 2022-03-26 Details and POC have been disclosed on WPScan 2022-03-30 Blog post published