id.oppo.com
Client-Side Encryption/Decryption
Domain id.oppo.com
thực hiện mã hóa AES-ECB-128 cho request body và giải mã response body ở client side:
- Decrypt: https://gchq.github.io/CyberChef/#recipe=From_Base64(‘A-Za-z0-9%2B/%3D’,true,false)AES_Decrypt(%7B’option’:‘Hex’,‘string’:‘5a7654534e394c474543643650705a53’%7D,%7B’option’:‘Hex’,‘string’:‘0000000000000000000000000000000’%7D,‘ECB’,‘Raw’,‘Raw’,%7B’option’:‘Hex’,‘string’:”%7D,%7B’option’:‘Hex’,‘string’:”%7D)&input=MgpSOUJwY213eTFMd3BtcHN5N1NZMmdqelJFSEVTdThvZjVNaytYV2tGd0JZU0E3cVZXeG9KMlo4N212bFZ6bE03MndyQ0RxRjIrKzBLWnFlK2hCb1NBcmxlMWlBbkZIeEtvS0dWYWxZeGJ4a1U3bGhyVmo0eFFnRzZZYTR4QURm&oenc=65001
- Encrypt: https://gchq.github.io/CyberChef/#recipe=To_Base64(‘A-Za-z0-9%2B/%3D’)AES_Encrypt(%7B’option’:‘Hex’,‘string’:‘5a7654534e394c474543643650705a53’%7D,%7B’option’:‘Hex’,‘string’:‘0000000000000000000000000000000’%7D,‘ECB’,‘Raw’,‘Raw’,%7B’option’:‘Hex’,‘string’:”%7D)To_Base64(‘A-Za-z0-9%2B/%3D’)&input=eyJhcHBLZXkiOiJDdUdzYmU2SGRBZTZ2REJIRmV3MkRpIiwic2lnbiI6ImIyMGU4YTlhMDhkMjY1NDRkOTU5MmYxYjBlMzk1YzE5Iiwibm9uY2UiOiIxNzQ2MTkxOTY3MTI1IiwidGltZXN0YW1wIjoxNzQ2MTkxOTY3MTI1LCJwcm9jZXNzVG9rZW4iOiJuTnZ3dDlZS0hUOFhGNXdmS2tIZURhOElvdklqU1dVTVVZcmM0Z2ErM1lHdFhUM0JUUy9BNWJqTEY3dFhOUGtldzh3bWtZcU40U2tjdkgwMkR5c0ZDMWpDWXhHNnoxL3I1L2VXVW9xaVlMeWh2aVZ6UEVnc3pPSkhFRUpwaEpzdFptTitoMFNwZnBhT3NIb3poQnN6dlRWVXA2eU1lcEZxVzc0VW94c08yK0xHZVg0QTR1RE1NRTdZRVhxcGZURzluRE1ZZjI5WGdVSTZYL0tTZmY2cFN1VEJlSFZ1REV4YzlRd2J5Ym5ObDIyWEIwcFg3VVpoNEY3UStBaW5nQjJDblpRL0pjdFR0WWZqc2kxZStPZlFJc0kyWllZcU15V0tvam53M24waTd4ZUl4ZEFRZjNQQWhkaGZaYTE2ZzNJaUV5OU9kaGlXUU4zOTYrREg3eWYvalVWZCszNGFqWWNySWFjK2VnZ0E1ajQ9IiwidmFsaWRhdGVDb2RlIjoiNjg2Njg3IiwidGlja2V0Tm8iOiIiLCJjYXB0Y2hhVGlja2V0IjoiIiwiZGV2aWNlSWQiOiIxMzc1ODVjNjhjZTVhZGM4NDUyMjk5MmNmODI1MTRkYyIsImNhbGxiYWNrVXJsIjoiaHR0cHM6Ly9jb21tdW5pdHkub3Bwby5jb20vIn0&oenc=65001&ieol=CRLF&oeol=FF
Key mã hóa bị thay đổi mỗi lần file JavaScript bị thay đổi. Để tìm key, ta cần đặt breakpoint ở đoạn code sau của file `/new/js/v2/index.3eb61a63418875ab55d9.js` và in ra giá trị `index.aesKey`:
~~~js
{
key: "decrypt",
value: function (t) {
return e.aesDecrypt(t, this.aesKey);
}
}
~~~
Giá trị this.aesKey
sau đó sẽ được truyền vào hàm sau:
parse: function (e) {
return h.parse(unescape(encodeURIComponent(e)))
}
Hàm h.parse
được định nghĩa như sau:
parse: function (e) {
for (var t = e.length, n = [], r = 0; r < t; r++) n[r >>> 2] |= (255 & e.charCodeAt(r)) << 24 - r % 4 * 8;
return new l.init(n, t)
}
Hàm này thuộc thư viện CryptoJS giúp chuyển chuỗi ASCII thành WordArray, bao gồm các số thập biểu diễn cho từng byte. Cuối cùng, key chính là dạng binary của this.aesKey
. Ví dụ, key Xy9QZvL4MMfEITYz
sẽ có dạng binary biểu diễn bằng hex là 587939515a764c344d4d66454954597a
.
title: Question: Khóa được tạo thành như thế nào?
Sau đây là cách mà this.aesKey
được tạo thành:
O = '__st',
k = function () {
function e(t) {
var n,
r,
i;
Object(u.a) (this, e);
var o = (w(O) || '').split('__'),
a = Object(c.a) (o, 2),
s = a[0],
l = a[1];
this.aesKey = s &&
l ||
Với hàm w
được dùng để lấy giá trị ở trong session storage:
var w = function (e, t) {
try {
return t ? JSON.parse(window.sessionStorage.getItem(e)) : window.sessionStorage.getItem(e)
} catch (n) {
return null
}
};
Giá trị mẫu của __st
ở trong session storage: 0rW+mlygQDZmv2fTyFQUuWYMSs6pEi4c341Fz3ANiKw=__SJBLvqiNAe1HY49G
.
Đoạn code:
a = Object(c.a) (o, 2),
s = a[0],
l = a[1];
Được dùng để destructuring và gán mảng o
cho biến a
. Sau đó, s
sẽ là phần trước dấu __
và l
là phần sau dấu __
. Giá trị của aesKey
sẽ là phần sau dấu __
nếu phần trước dấu __
không phải là falsy (xem thêm JS Booleans).
title: Question: Nguồn dữ liệu của `__st`?
Khi intercept từng gói tin lúc reload lại trang id.oppo.com
, thấy được rằng __st
được set sau khi các request bị mã hóa được gửi đi. Nên ta có thể suy đoán rằng key được gen ra ở phía client và được gửi đến server rồi mới được lưu vào session storage.
Khi phân tích một gói tin bị mã hóa, thấy rằng có header X-Key
rất có khả năng là AES key đã được mã hóa:
X-Key: JR/HMh20tUagSjLVZFXYjkm5TXXdiCZ9USxA2ijKuIVYeU7rBTT1JB65jOjOPyqfi3+qP82CYzcTcWugVgj8b5djWV6y9+hJPbNfiAh23qyjbK8OWv/Q1zQ/+BcEMReytuGA4VQCrveFRZOKH9DD/jJUuXr0NY+EP+mVlsnueYc=
Ngoài ra, trong JavaScript cũng có đoạn code sau:
c.a.config({
appKey: 'CuGsbe6HdAe6vDBHFew2Di',
signKey: '&key=FdjydGAAKasmht1nFnR4MS5itFeh4R1Lk',
excludeSignFields: [
'context'
],
protocolVersion: '1.0',
ignoreHeaderJSON: !0,
timeout: 10000,
useEncrypt: [
'test',
'prod'
].includes(f.a.env),
rsaPublicKey: 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCf5viGpYn1duRt9wzwca1SEuL+wwnBfBfza0nTuLPYR5uZyheUoFI+cudN9eB4jlvXij4yAxH59ML8BhVUab/j+TmeDsCe+OLpswdHWEXtY1HacLpw/wpsKQHBQZYhAARZRx/4J5/fiz/pJcH5qVGYK0Yu8c9CNl9/eHDQkj9LoQIDAQAB'
});
Có thể rsaPublicKey
chính là public key của private key mà được dùng để mã hóa AES key.
title: Success: Thử gửi các plaintext request để xem server có chấp nhận hay không?
Server không chấp nhận và trả về response như sau:
~~~json
{
"error": {
"code": "2710000",
"message": "请求参数异常"
},
"success": false
}
~~~
Response này tương tự như khi ta gửi request sử dụng sai chữ ký số.
Tuy nhiên, nếu ta đổi `Content-Type` của request thành `application/json` thay vì `application/encrypted-json` thì ta có thể gửi được request và nhận được response dưới dạng bản rõ. Ngoài ra, nếu gửi request bằng Burp, ta cũng cần thay đổi `Accept` header thành `application/json`.
title: Question: Liệu có thể khai thác CSRF ở bản thân domain `id.oppo.com`?
Không thể do ta cần header `X-Session-Ticket`:
~~~http
X-Session-Ticket: 7MUIJ5KSHikDmRt5ZxtSbWYMSs6pEi4c341Fz3ANiKw=
~~~
Đây chính là giá trị phía trước chuỗi `__` của `__st` trong session storage (xem thêm [[#client-side-encryptiondecryption|Client-Side Encryption/Decryption]]).
Tuy nhiên, liệu ta có thể sử dụng JSON body và `X-Session-Ticket` của một request hợp lệ (chẳng hạn của attacker) để gửi đi cùng với cookie của nạn nhân nhằm thực hiện một hành động mà họ không muốn hay không?
Câu trả lời là có.
Vấn đề còn lại là liệu server có validate header `Content-Type` hay không vì chúng ta không thể sử dụng HTML form để gửi request có `Content-Type` là `application/json`. Nếu có thì ta chỉ còn trông chờ vào việc tìm được một endpoint nào đó không sử dụng request body hoặc không validate `Content-Type`.
Bên cạnh dùng HTML form để tấn công thì ta cũng có thể dùng [[Fetch API]] và điều này đỏi hỏi response của OPTIONS request cần phải có [[Port Swigger - Access-Control-Allow-Origin Response Header|ACAO]] header.
title: Todo: Decrypt tất cả các requests/responses để kiểm tra sensitive data.
Có thể tham khảo [[#api-endpoints|API Endpoints]] để biết danh sách các requests.
Decrypting Traffic In Burp Suite
Ban đầu, ý tưởng để decrypt traffic trong Burp Suite là xây dựng một extension để cho phép giải mã và mã hóa thông qua context menu. Cụ thể hơn, để decrypt/encrypt một request/response body, ta cần sao chép nó vào editor của extension rồi nhấn decrypt/encrypt. Tuy nhiên, cách làm này làm chậm đáng kể quá trình test.
Sau đó, ý tưởng chuyển sang xây dựng một extension cho phép tự động decrypt request/response khi nó đi qua Burp Suite Proxy.
Tuy nhiên, có một ý tưởng dễ triển khai hơn sau khi hiểu rõ cơ chế mã hóa: sử dụng Match and Replace.
Cụ thể hơn, ta sẽ viết 2 rule sau:
Rule đầu tiên là để thay content type thành dạng bản rõ, điều này cũng khiến response trả về có dạng bản rõ.
Rule thứ hai là để thay giá trị khóa được truyền vào hàm mã hóa (e.aesDecrypt(t, this.aesKey);
) thành một khóa rỗng. Việc mã hóa bằng khóa rỗng tương đương với việc không mã hóa.
Request Signing
Tất cả các request gửi đi từ phía client đều có trường sign
:
{"appKey":"CuGsbe6HdAe6vDBHFew2Di","sign":"45bff6cc09d8a21830a41876508a18f1","nonce":"1750861745141","timestamp":1750861745141,"processToken":""}
Trường này là giá trị MD5 hash của một chuỗi được tạo thành từ các chuỗi còn lại.
Bước đầu, tìm kiếm nơi sử dụng signKey
thì tìm thấy đoạn code sau:
s = o.appKey,
c = o.signKey,
u = o.excludeSignFields,
l = void 0 === u ? [] : u,
s &&
c &&
(
f = (new Date).getTime(),
(
p = Object.assign({
appKey: s,
sign: '',
nonce: String(f),
timestamp: f
}, o.data)
).sign = U(p, c, l),
o.data = p
)
Hàm Object.assign
được dùng để sao chép tất cả các thuộc tính trong object đầu tiên vào object thứ 2 (o.data
). Giá trị o.data
có thể chính là request body mà client cần gửi.
Có thể thấy, nó sao chép 4 field là appKey
, sign
, nonce
và timestamp
. Giá trị của nonce
là giá trị chuỗi của timestamp.
Giá trị sign
sau đó được tính bằng hàm U
.
Hàm U
có định nghĩa như sau:
U = function (e, t) {
var n = arguments.length > 2 &&
void 0 !== arguments[2] ? arguments[2] : [];
if ('object' !== Object(i.a) (e)) return '';
var r = [];
for (var o in e) {
var a = o;
if (
- 1 === n.indexOf(a) &&
Object.prototype.hasOwnProperty.call(e, a) &&
'undefined' !== typeof e[a] &&
null !== e[a]
) {
var s = 'object' === Object(i.a) (e[a]) ? JSON.stringify(e[a]) : String(e[a]);
s &&
r.push(''.concat(a, '=').concat(s))
}
}
return r.sort(),
P() (r.join('&') + t).toString()
};
Một cách tổng quan, nó sẽ lặp qua từng cặp key value trong object e
(tức là p
hay o.data
mà có các field vừa được thêm mới vào) rồi serialize nó thành dạng key=value
. Từng cặp key-value sẽ được push vào mảng và cuối cùng là mảng sẽ được join lại bằng dấu &
kèm theo giá trị của signKey
ở cuối.
Đặc biệt, key nào có value là rỗng thì sẽ bị bỏ qua.
Ví dụ với request body trên và giá trị của signKey
là &key=FdjydGAAKasmht1nFnR4MS5itFeh4R1Lk
, dạng serialized sẽ là: appKey=CuGsbe6HdAe6vDBHFew2Di&nonce=1750861745141×tamp=1750861745141&key=FdjydGAAKasmht1nFnR4MS5itFeh4R1Lk
.
Giá trị này sau đó sẽ được truyền vào hàm P
. Hàm P
sau đó sẽ gọi hàm _createHelper
:
_createHelper: function (e) {
return function (t, n) {
return new e.init(n).finalize(t)
}
},
Với t
là serialized string ở trên.
Hàm finalize
có định nghĩa như sau:
finalize: function (e) {
return e &&
this._append(e),
this._doFinalize()
},
Hàm _doFinalize
chính là hàm tạo ra chữ ký để gán vào thuộc tính sign
:
_doFinalize: function () {
var t = this._data,
n = t.words,
r = 8 * this._nDataBytes,
i = 8 * t.sigBytes;
n[i >>> 5] |= 128 << 24 - i % 32;
var o = e.floor(r / 4294967296),
a = r;
n[15 + (i + 64 >>> 9 << 4)] = 16711935 & (o << 8 | o >>> 24) | 4278255360 & (o << 24 | o >>> 8),
n[14 + (i + 64 >>> 9 << 4)] = 16711935 & (a << 8 | a >>> 24) | 4278255360 & (a << 24 | a >>> 8),
t.sigBytes = 4 * (n.length + 1),
this._process();
for (var s = this._hash, c = s.words, u = 0; u < 4; u++) {
var l = c[u];
c[u] = 16711935 & (l << 8 | l >>> 24) | 4278255360 & (l << 24 | l >>> 8)
}
return s
},
Hàm này thực hiện các logic của MD5 hash nên ta suy ra được thuật toán tạo chữ ký là MD5.
Thật vậy, MD5 hash của appKey=CuGsbe6HdAe6vDBHFew2Di&nonce=1750861745141×tamp=1750861745141&key=FdjydGAAKasmht1nFnR4MS5itFeh4R1Lk
là 45bff6cc09d8a21830a41876508a18f1
: MD5 - CyberChef, giống với giá trị của field sign
trong request body.
Từ những thông tin trên, ta có thể dễ dàng tạo ra một request giả mạo với chữ ký hợp lệ.
PoC:
Request gốc (đã bỏ lớp mã hóa bằng cách dùng application/json
):
Request đã bị chỉnh sửa (thay chữ i
viết thường ở cuối appKey
thành chữ I
viết hoa)
Ký lại và sử dụng chữ ký đúng:
Login with Email Flow
Login URL gửi đến id.oppo.com
từ các ứng dụng cần xác thực:
https://id.oppo.com/apis/login/authAndCallBack?bizAppKey=GhGxduZv6HCk5wnyZ34P4G&callback=https%3A%2F%2Fcommunity.oppo.com%2F&language=en_US
Với bizAppKey
cho là unique đối với từng app còn callback
đóng vai trò như là redirect URL sau khi đăng nhập.
title: Fail: Khai thác Open Redirect ở tham số `callback`.
Chỉ có thể redirect on-site (`community.oppo.com` trong URL trên) chứ không redirect đến subdomain hay external domain.
Note: Nếu tìm được client application nào đó mà có client-side redirect thì ta có thể hình thành attack chain.
POST /apis/login/validate-password
Request body
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "0eab65c1eed72fe1cc4e42dfe1d26e40",
"nonce": "1747379020722",
"timestamp": 1747379020722,
"countryCallingCode": "",
"accountType": "EMAIL",
"md5Pwd": "f05f37a6e9205a0bb935d471e050e041054ad6bff794a330c6647996d61af79e",
"loginName": "marucube35@wearehackerone.com",
"processToken": "",
"captchaTicket": "",
"callbackUrl": "https://community.oppo.com/",
"deviceId": "40bda5de95bd43a16cfe6871c7a88415"
}
Có thể thấy, giá trị của `md5Pwd` không đúng format của MD5 hash.
Trường `sign` đóng vai trò như là chữ ký số cho các field trong request và nếu thay đổi nội dung mà không thay đổi chữ ký số thì ta sẽ nhận được response sau:
~~~json
{
"error": {
"code": "2710000",
"message": "请求参数异常"
},
"success": false
}
~~~
Với `请求参数异常` có nghĩa là Request Parameter Abnormality.
Việc thêm các trường khác vào JSON body không làm vi phạm chữ ký số. Nói cách khác, giá trị `sign` chỉ được tạo ra bởi một số field đã được quy định sẵn. Điều này giúp ta có thể sử dụng `"="` như là key của field nhằm giả dạng một JSON body thông qua form body.
title: Success: Tìm hiểu cách client-side script tạo ra chữ ký.
Tham khảo [[#request-signing|Request Signing]].
Nếu thành công, response của nó sẽ là:
{
"error": {
"code": "2710104",
"message": "Enter verification code"
},
"success": false
}
POST /apis/captcha/get-captcha-js
Tiếp đến, nó có thể sẽ gửi request để lấy CAPCHA challenge:
POST /apis/captcha/get-captcha-js
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "f2df64395c081574324ed6afb9b5513c",
"nonce": "1747379065197",
"timestamp": 1747379065197,
"captchaType": "DRAG_PUZZLE",
"routingType": "email",
"routingValue": "marucube35@wearehackerone.com"
}
Response body:
{
"data": {
"close": false,
"jsSdkUrl": "https://id.oppo.com/apis/captcha/get-captcha-js-sdk?languageTag=en-US",
"providerJsSdkUrl": "https://captcha-sgp-sec.heytapmobile.com/dx-captcha/index.js"
},
"success": true
}
POST /apis/login/validate-password
Sau khi giải CAPCHA challenge thì nó sẽ gửi POST request đến validate-password
một lần nữa cùng với captchaTicket
:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "1ae6cb46c22e7c82d8bef11457445de7",
"nonce": "1747379738612",
"timestamp": 1747379738612,
"countryCallingCode": "",
"accountType": "EMAIL",
"md5Pwd": "c97e55464db080a8421f98ea8bf31717",
"loginName": "marucube35@wearehackerone.com",
"processToken": "",
"captchaTicket": "{\"success\":true,\"provider\":\"DINGXIANG\",\"result\":\"{\\\"ret\\\":0,\\\"ticket\\\":\\\"196D7F2D437B960842D69F14AC7F7FA3345CEA324E2ADABE6EAA7@sgp:6826e37cZfafcNbwouOjIcWeA5eG3XOnVorB4Vj1\\\"}\"}",
"callbackUrl": "https://community.oppo.com/",
"deviceId": "40bda5de95bd43a16cfe6871c7a88415"
}
Lúc này, giá trị của md5Pwd
đã có đúng định dạng của MD5 hash.
Response body sẽ có processToken
:
{
"error": {
"code": "2310002",
"errorData": {
"processToken": "E8xneW5ZKGGDh19qffq36Z_singapore"
},
"message": "需要登录校验"
},
"success": false
}
POST /apis/business/authentication/auth
Request body có chứa processToken
như sau:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "ab484cfad09136a70eec7ea6423c116b",
"nonce": "1747380023099",
"timestamp": 1747380023099,
"appId": "3574817",
"businessId": "d08fce87c95e4d5e927b8ed85eb6dfdd",
"mspBizK": "4iAEb620alQ8s8c8ss8o0K8sS",
"mspBizSec": "80620774fE983aab1E8C564e3f213b62",
"captchaTicket": "",
"processToken": "E8xneW5ZKGGDh19qffq36Z_singapore",
"excludeValidateTypes": "PASSWORD"
}
Response body:
{
"data": {
"nextProcessToken": "E8xneW5ZKGGDh19qffq36Z_singapore"
},
"success": true
}
POST /apis/business/authentication/list
Request body cũng có chứa processToken
như sau:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "9f690c1fe6e17e849e18ef2bcae6fdfe",
"nonce": "1747380023330",
"timestamp": 1747380023330,
"processToken": "E8xneW5ZKGGDh19qffq36Z_singapore"
}
Response body:
{
"data": {
"country": "VN",
"nextProcessToken": "XmFzE9Tua9dnEzWfG5Gvdg_singapore",
"validateList": [
{
"subValidate": [
{
"showInfo": [
"ma***@gmail.com"
],
"subValidateName": "EMAIL"
}
],
"validateName": "EMAIL"
}
]
},
"success": true
}
POST /apis/business/authentication/send-validate-code
Ứng dụng sẽ yêu cầu nhập mã OTP ngay tại bước này. Body của request dùng để yêu cầu OTP code:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "9d95bb2978e44ee5d3d78c94a91d7ab4",
"nonce": "1747380278115",
"timestamp": 1747380278115,
"processToken": "E8xneW5ZKGGDh19qffq36Z_singapore",
"subValidateName": "EMAIL"
}
Response body:
{
"data": {
"codeLength": 6,
"nextProcessToken": "E8xneW5ZKGGDh19qffq36Z_singapore"
},
"success": true
}
POST /apis/business/authentication/validate-input-data
Request nhập OTP có body như sau:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "bd7bbdf74cb15cd652ef0ad5002d5d2e",
"nonce": "1747380458241",
"timestamp": 1747380458241,
"processToken": "E8xneW5ZKGGDh19qffq36Z_singapore",
"validateName": "EMAIL",
"subValidateName": "EMAIL",
"validateData": "924570"
}
Response body có chứa một ticket:
{
"data": {
"nextProcessToken": "E8xneW5ZKGGDh19qffq36Z_singapore",
"ticketNo": "PCWGusiQ21AYdQEkETSRu7"
},
"success": true
}
Để trigger lại việc kiểm tra OTP thì cần xóa cookie hoặc session storage hoặc mở một container tab mới (FireFox).
title: Fail: Thử sử dụng response của một tài khoản khác để bypass OTP
Reference: [PayPal Bypass OTP Verification Code Vulnerability Worth $15,000 Bounty | by HackerPlus+ | Medium](https://medium.com/@HackerPlus/paypal-bypass-otp-verification-code-vulnerability-worth-15-000-bounty-d1ec8285648e)
Đã bypass được bước OTP nhưng không thể bypass được request tiếp theo ([[#post-apisvalidate-systemlogin|POST `/apis/validate-system/login`]]]) do có lỗi:
~~~json
{"error":{"code":"1114001","message":"Session expired due to inactivity. Please try again."},"success":false}
~~~~
Có thể lỗi là do ta sử dụng `nextProcessToken` không đúng với `processToken` trong request trước đó hoặc ticket đã bị expire sau khi nó được sử dụng.
Khi sử dụng `nextProcessToken` giống với `processToken` thì ta nhận được response sau:
~~~json
{"success":false,"error":{"code":"1114002","message":"Process execution error","errorData":null},"data":null}
~~~
title: Fail: Nếu ta chỉ nhận ticket từ một tài khoản khác nhưng không sử dụng nó thì có thể bypass được request sau hay không?
Không, vẫn bị lỗi như trên.
POST /apis/validate-system/login
Request dùng để nhận cookie cho domain id.oppo.com
và authorization code cho client application.
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "36160347ec518ade8a82e68100cd0616",
"nonce": "1747380458575",
"timestamp": 1747380458575,
"processToken": "E8xneW5ZKGGDh19qffq36Z_singapore",
"ticketNo": "PCWGusiQ21AYdQEkETSRu7",
"deviceId": "40bda5de95bd43a16cfe6871c7a88415"
}
Response của request này có chứa một cookie của domain id.oppo.com
với attribute SameSite
là None
:
HTTP/2 200 OK
Date: Fri, 16 May 2025 07:27:38 GMT
Content-Type: application/encrypted-json;charset=UTF-8
Content-Length: 6976
Server: nginx
Set-Cookie: sessionKey=a3cQd8X-o6RSuBfOmO7fiFkhBIhPrQuQ7dKRWdSuL8w_cnF3V6mDd3tWPUR8IlxA7yLLK6ceLD0;
{
"data": {
"callbackUrl": "https://community.oppo.com/",
"code": "XMLQkYgaMcMM1EWSX4bT6n_singapore",
"encryptSessionId": "eyJpdiI6IkZRdDBXcE9oVW9HZnJXVi9IemVNcFE9PSIsInZhbHVlIjoiS3ZCWjBXWFNtdElDVjVCOGJuMmVGbjRBb3hESU1Ic0JMQjVoS05VM3didUhpVDA3SCttRHBMQ0N0RHdMZ29ITjZ1R0trS1BrTEFQZHFvb1pZR0RHTjc2QjRnK242aWhsVXVxZGhqTW9hblZwc2NIKzhrRFIvcGlBT0JUTVZIMnAiLCJtYWMiOiJkYzY3NjgyMzc3NGZjM2RhM2JhMzZlNjI3ZmU2YzBiYjlkM2IxZWUwZGZjYmY1NTM1OGY5MGEwOWVkN2MzM2NhIn0=",
"encryptSessions": [
"https://c.coloros.net/login?key=Fy4jbYpNCYnsqEf81ZtzLX_singapore",
"https://gray-push-intl.oppo.com/?key=LyBcmA7TwLjYHyXB2tmJgD_singapore",
"https://annotate.ait.heytapmobi.com/annotate/oppo/account?key=JD6dm44T1hba4XUk7eiadC_singapore",
"https://iactivity.cdo.oppomobile.com/activity-user/cookie/session?key=8uK7GsQfuosTETg6WwLCv_singapore",
"https://u-id.oppomobile.com/union/loginCallback?key=HBNnMS8HGzYbKwo2NQYDbV_singapore",
"https://u.oppomobile.com/union/loginCallback?key=8bmTR54FW6Akqa8cMXaqm1_singapore",
"https://lockcard-test.oppoer.me/api/setCookie?key=XZ3EMUBkMvZ9SA97CHVZrY_singapore",
"https://htsg-storeapi-sg.oppo.com/mall/account/set/cookie?key=X7GsuzyAKJvZKRZVUCPvGh_singapore"
],
"sessionId": "a3cQd8X-o6RSuBfOmO7fiFkhBIhPrQuQ7dKRWdSuL8w_cnF3V6mDd3tWPUR8IlxA7yLLK6ceLD0",
"setCookieUrl": [
"https://www.opposhop.in/setssocookie.php?sign=ef1449042c2e935176df7747bcf9fc8e&value=eyJpdiI6IlIrYXFkSXdkUWQvdHpTSFJLNU81ZXc9PSIsInZhbHVlIjoialBNQ3NXWG1kNTJHN1dCL2xMbHk2VFJiNjdzL2VSbWZMUVZ6OFhQQnRTK0owOE43WSsyNmpmUW0xc0pTejEzZW5hOU9tZ0x0TEhseURuRUpwUTNpRU8yak0rTVFZdG9DZzRsYUg0OW9oNVZNNXordlo1Q09maDRpM3JsRDlyQkQiLCJtYWMiOiIzNGVkNjEwYmMxNmUwMzlkNzhkNzBkZDBjNTllYWE0ZTI5YWRmN2MwOTJlMzI4OTdkODZlZGU1MGM2OTBjYzBhIn0=&key=NEWOPPOSID",
"https://innereye.myoppo.com/auth/api/oppoid/setcookie?sign=f5ba3406672b3ab60ce77b7654538692&value=eyJpdiI6ImllaEtsWVBrYk1GWUZDZWI4UU9MUWc9PSIsInZhbHVlIjoiTmNBUE96TGpQWkcvY0V1UGF0cURKQ3B0SGRtUjdGU3I1VTcyTmIvL2pZc0lLd0lYUFp0ZUc0b05ONmJBQy8zMUxYZjlwZzRNQVYwOFQ0M2pnY21SZlNrNlZ1SldMWVR5eWFKTU9FU0VRU28xRVBNWUFFZFB1K1RCRDdZQ2tDa3AiLCJtYWMiOiI0ODkzNzZiNTRhOGZiNjM2MDc4ZjFjMWRjY2EyMmExOWE4OTAzYjVhNTg4MTQ1MzAwNjRlMGYzMWQ3ZTUxODY3In0=&key=NEWOPPOSID",
"https://forum.cdo.oppomobile.com?sign=b3b5617eff053500eaa4a9bbb96ebc13&value=eyJpdiI6IjBBR0ZFOEJ5Vm5XV2dzVGtCaUhsdVE9PSIsInZhbHVlIjoidHd2NldNSFNZazB3NWNaYXo1UTlOZGlpWWwyQTJDSjdCQ0lidWNGSlU4SUdlTnFKajBuTFB0eDFRNkFJSTFDN1dkaGNQSWxpMXlYcXMrbjhHT1gwcFNwOUpBTmJERWNNUzJUYTFPSkNaVFRRb1A5dTdlWU9BODFpbnRyWDlPMUMiLCJtYWMiOiJkMjg3NTk5NTk2ZWVjYjljM2RhNTY4NzM1YjE2NzViYzdhNzU0ZmNmNDFiZmM3MTFjOWU3ZWY2MzVlMDNkZGQ0In0=&key=NEWOPPOSID",
"https://bbs.coloros.com/setcookie.php?sign=d3f65dc97d77dc5b4084f560d3d41bcc&value=eyJpdiI6Im82U2RPd1ZpQzJESGRkWkN6MU9ITEE9PSIsInZhbHVlIjoiaEdQb1NobHJuQ0luZU1qVDNWd1lpbEtOUDBsdE5JTGJNWVFCSHRjdFhaVDBGemlxQjRQQUxtT2I5S1JFaUdkOVdldm01a0VQY1dQSHVBdkxQZkEweWlkcytnYnVkL1F2SkE0bmh2R3pjOHAxTEpyN3o4S2V6bmZrUzN2SldWU0EiLCJtYWMiOiJjNTNmNTM1OWI3MjI0NTk4ZDM2ZDAyY2FiNWJmODIyZmVjZWRkMDg0ZGM5YTg0NWMwODJjM2IzYjViYjlkMDc3In0=&key=NEWOPPOSID",
"https://www.oppo.cn/setssocookie.php?sign=357d28786f76ffc2b7ddb45942659e88&value=eyJpdiI6IjFGaHh5WHZVekdaaUFtSGVZSTF0eGc9PSIsInZhbHVlIjoia2JwTnVUUHNkR2NGRGlpTmNWUUMybi9PMXVCd0NJUVkrRWR5ajF6QzVPRkFDN0FZSEpJS2J3L01rbE1qdXd3K3BkSC9hL0JidThEMHlRMVo5V2VZMG9DNk55WDZiMFVoSitMVnhEcVVray9vb0UxNlIvTW4xNHlWdmxYU29ETWUiLCJtYWMiOiIxMjBhMjQwZjIxNmIyMjIwYjAzZDY4ZDRhZjVkMmQ4MGFmYzFhNGVjOGUzZjIyOTVkOWZjOWZjOGIyZTkyMTEwIn0=&key=NEWOPPOSID",
"https://cz.oppomobile.com/recharge/service/setcookie?sign=d1cff9265f030278e7ea9000fa974989&value=eyJpdiI6Im11US8zNnR2dTBLVnNGNUR5MXBLMUE9PSIsInZhbHVlIjoiTElkNzNFd3JUMUhtYythdENtN2prYVZGZlVZM2xiZ201M2dFTlZBbHRSaG1tTVRPU041QVdMRjRPR2wxd1h1L0tWNkxWcHZyaFhFankxV1dZa1ZMVjVSNkhXc1YxQ0J3ekhtRWx1T0tjZnJSNFpiZk1FYW9QMVUwejg5V1JLZE8iLCJtYWMiOiI2YTI4MGFkODZkOGVmNGI4NzcwMmRkN2JjYzJhMTBmNzc1MTgxNDBhZGYzMzFlMzliODg1ZDIyZGQ2OTkwYjRiIn0=&key=NEWOPPOSID",
"https://community.coloros.com/setcookie.php?sign=aa0443e33f7637526e67973af64e3c2c&value=eyJpdiI6InZZSW5HYStRN2RIRnBQT0lkT3Z0UGc9PSIsInZhbHVlIjoiaDBRVUVPeFBnVjd2Q1lDWFpCMXhYMXhYU0tVQjkwZmhyZEwrVG1lRnluR3BkYm1iaUhRa1p3My9Gc3htVVhoa2V5UjE4akd3SlF5QkI5akJZM0RtRkgrTU9xUG5zWHEvSnpvSUVVaFFJV3RRNW1aNmtOZURNRTFxT3lYSlVVNDQiLCJtYWMiOiIyZDM3NTRlODQ4ZWJlODEyNDc5ZDY1M2NhNWRlYjMwZWY1ZmIxYTkxMWE4ZDAzZjEwMDE3NDQxOWZkMTNkN2I3In0=&key=NEWOPPOSID",
"https://cm.ads.oppomobile.com/act/worldcup/setCookie?sign=a10a0db0cb0aedfce805ebe757d17610&value=eyJpdiI6IkhSZis3WVp1MHdCRERPSlBTSllJTkE9PSIsInZhbHVlIjoiTU1IL2VLVVJ1c3VSUisrUUdhUEk4bm16ek55V3BMY1MzYVRqNElJYW9XdzRTZHNpRGFxSWxDVVRsNWlJL0d1RUhtNGNKVzdyRS9XOGtNNngrSzNCR0R3T0N4NjVrdStkZm9NbTAzYkpuSTRBVE5RMVFYQnkySnVnWUl5eG8xazEiLCJtYWMiOiI0YWE3NTczMjZjYjQ3MWVkMTY2NTBkNThiOGRhODMxZmMzNTkyNTFkODY5YjIyMDYyYTkxZmE2MDNkMTE1OTBkIn0=&key=NEWOPPOSID",
"https://jfadmin.oppomobile.com/api/localshop/setcookie?sign=ced24b9969ecdb3592a702a0573bde21&value=eyJpdiI6InNNblphQmlaRDV4UkRIajlXTnJzeXc9PSIsInZhbHVlIjoieENlWU9JMUFiSHNNcE9UcFRuTnJHSzhIZHBHOTZBc0t3RTYwTGdNT3g1Z1RFcmR4R2wzQUxNKzVYTFhJc1RscVZaUmozRndCYU5KS0ExTkdGOXVuTDZycURZeG5MUWxWcnBlRlRVUWl5WmVJUFYxWnRDbUwyQmg4RDQxam5QTXUiLCJtYWMiOiIyODhhZTU4ZTEwYWQyNjU5ZWUyOTY0ZDJhZjI3YjJiYTllMzgxYmFiN2Q1MTcxNmE1ODg0NjI5MTVlZmQxMWZlIn0=&key=NEWOPPOSID"
],
"userId": "1243030932"
},
"success": true
}
Giá trị code
trong JSON sẽ được gửi đến callback URL.
title: Todo: Gửi request đến `/apis/validate-system/login` với một `ticketNo` của tài khoản khác.
Không tạo ra được chữ ký hợp lệ.
Update 1: tham khảo [[#request-signing|Request Signing]].
POST /apis/check-callback-and-query-brand
Sau request này còn có một POST request nữa gửi đến /apis/check-callback-and-query-brand
:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "efce89fd4623f25e1ba64cd22b340832",
"nonce": "1747380458757",
"timestamp": 1747380458757,
"callBackUrl": "https://community.oppo.com/"
}
Response body:
{
"data": {
"brand": "OPPO",
"callBackUrl": "https://community.oppo.com/"
},
"success": true
}
Callback
Sau khi đăng nhập thì có một request gửi đến callback URL với code
như sau:
https://community.oppo.com/?code=XMLQkYgaMcMM1EWSX4bT6n_singapore
Tuy nhiên, response của nó là 405 và không có cookie nào được trả về.
Thực chất, cookie chỉ được cung cấp khi có request sau:
https://community.oppo.com/ajax/authorize/frontend/login?code=XMLQkYgaMcMM1EWSX4bT6n_singapore&t=1746245524488
Login with Google later
Login with Phone Number later
Binding with Google later
Authorization request có URL như sau:
https://accounts.google.com/o/oauth2/auth?redirect_uri=storagerelay%3A%2F%2Fhttps%2Fid.oppo.com%3Fid%3Dauth214122&response_type=permission%20id_token&scope=email%20profile%20openid&openid.realm=&include_granted_scopes=true&client_id=73932226848-b137nbr60po3i8avbb558cot8oker12h.apps.googleusercontent.com&ss_domain=https%3A%2F%2Fid.oppo.com&fetch_basic_profile=true&gsiwebsdk=2
Binding request có URL như sau:
https://id.oppo.com/v2/profile/third_bind.html?thirdPartyType=google&language=en-US&access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjY2MGVmM2I5Nzg0YmRmNTZlYmU4NTlmNTc3ZjdmYjJlOGMxY2VmZmIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNzM5MzIyMjY4NDgtYjEzN25icjYwcG8zaThhdmJiNTU4Y290OG9rZXIxMmguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI3MzkzMjIyNjg0OC1iMTM3bmJyNjBwbzNpOGF2YmI1NThjb3Q4b2tlcjEyaC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEwNDU3ODM1MDU3NDU2NDcwMDQyOSIsImVtYWlsIjoibWFydWN1YmUzNUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlBYRkpXbmI4eEZvaEsyMVRqOVdrc3ciLCJuYmYiOjE3NDczODkwMzQsIm5hbWUiOiJMw6ogTWluaCBRdcOibiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NKMmh1bWR5cUtMak0wb0RYOUw1TUQ2Z0VwclFBcDhXZTJ6QmFsb2E1TFBKeG56RkJBZj1zOTYtYyIsImdpdmVuX25hbWUiOiJMw6ogTWluaCBRdcOibiIsImlhdCI6MTc0NzM4OTMzNCwiZXhwIjoxNzQ3MzkyOTM0LCJqdGkiOiI5ODgxN2EwY2YzOWJkOWJhZTM3MDBmZTg1MThlOTgxN2U0ZDBiYTkxIn0.UutkEZMVVvuZ_uHRBerYZKULlds-UXREL13LmgznqqobOKukvNdnrRg8DOkLM3zNINd_YaqHFmiZs5SPWdO61svtUTRu2XBO08Ta-Pw4iCyMe98unRVnGvo36B37AO8kmvcCkjM7lDjizYtwFUEKaJx671AQO0p9Mf88CGJuMrwjkMAC-hKSPWTIZqYzvtaOEl8G3SoZpj9jnTfk_ukrAxg7DGpm1VFa6Jwe7lxP0-opxRHBqc7GhHEzA48A7IpehqOjH1l-KlDS7nqkh9ARlEN4NOWJ4AOdqNlHm4HGXLjGisMzItMSfkQCyjJTTGrYKm3EkNPqUPDLiX4z0gZMYg&state=XIsnFfW9RQm2bX57vVCA
Param access_token
khi được decode ra sẽ có dạng như sau:
{
"alg": "RS256",
"kid": "660ef3b9784bdf56ebe859f577f7fb2e8c1ceffb",
"typ": "JWT"
}
{
"iss": "accounts.google.com",
"azp": "73932226848-b137nbr60po3i8avbb558cot8oker12h.apps.googleusercontent.com",
"aud": "73932226848-b137nbr60po3i8avbb558cot8oker12h.apps.googleusercontent.com",
"sub": "104578350574564700429",
"email": "marucube35@gmail.com",
"email_verified": true,
"at_hash": "s8wR_W9-b2jlF8Ianc6BtA",
"nbf": 1747388945,
"name": "Lê Minh Quân",
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJ2humdyqKLjM0oDX9L5MD6gEprQAp8We2zBaloa5LPJxnzFBAf=s96-c",
"given_name": "Lê Minh Quân",
"iat": 1747389245,
"exp": 1747392845,
"jti": "09e49d498110da40e4f3b2273091b65b78ec86b6"
}
title: Todo: Thử thay đổi dữ liệu trong JWT
Password Reset Flow later
URL đến trang reset password:
https://id.oppo.com/v2/find_password.html?bizAppKey=GhGxduZv6HCk5wnyZ34P4G&callback=https%3A%2F%2Fcommunity.oppo.com%2F%3FlogOut%3Dtrue
Ứng dụng cho phép điền SĐT hoặc email.
- [ ] Password Reset Poisoning
- [ ] Host Header Attacks
Delete Account Flow
Trước tiên, cần phải trải qua bước xác thực OTP thông qua 2 POST request gửi đến /apis/business/authentication/send-validate-code
và /apis/business/authentication/validate-input-data
.
Sau đó sẽ là POST request gửi đến /apis/drop-account/ask-drop
với request body như sau:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "6ee82b9c9a90652e0e7dc8d5b76ead57",
"nonce": "1747466790882",
"timestamp": 1747466790882
}
Response body:
{
"data": {
"nextProcessToken": "8cSBzJB4kabJwCZLvaQXV8_singapore"
},
"success": true
}
Tiếp đến là POST request đến /apis/drop-account/analyze-conditions
với request body có chứa processToken
và ticketNo
(nhận được response của /apis/business/authentication/validate-input-data
) như sau:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "352cb2c037eb9f2c120015549fddf1c5",
"nonce": "1747466791042",
"timestamp": 1747466791042,
"appId": 3574817,
"processToken": "8cSBzJB4kabJwCZLvaQXV8_singapore",
"businessId": "84fef13a57dd4a4ca3326e22189cbf5b",
"ticketNo": "6JTHowdr4HEEQHwLCXLYuN"
}
Response body:
{
"data": {
"conditions": [],
"needReconfirm": false,
"nextProcessToken": "Asa3EPMtbs4kcn1cff1myt_singapore"
},
"success": true
}
title: Todo: Thử gửi request mà không có `ticketNo`.
Tái sử dụng request body của endpoint `/apis/business/authentication/list`.
Cuối cùng, request dùng để xóa tài khoản sẽ được gửi đến endpoint /apis/drop-account/drop
với request body sau:
{
"appKey": "CuGsbe6HdAe6vDBHFew2Di",
"sign": "94c746775421a319aa7a76b290e1d00c",
"nonce": "1747466847878",
"timestamp": 1747466847878,
"appId": 3574817,
"processToken": "Asa3EPMtbs4kcn1cff1myt_singapore",
"businessId": "2065172a2da04928b6137598ccad465a",
"ticketNo": "MHqZPQfwEbYiX9cmSM8ifw"
}
Có thể thấy, nội dung của nó tương tự với request gửi đến endpoint /apis/drop-account/analyze-conditions
.
Response body:
{
"data": {
"deleteMark": {
"dropMessage": "Your request will be reviewed within 3 days. In the mean time, your account will be locked. You wont be to reset your password, recover your account, or sign in from a new device.",
"dropType": "Deletion request submitted",
"immediatelyDrop": false
}
},
"success": true
}
title: Todo: Thử gửi request mà không có `ticketNo`.
Tái sử dụng request body của endpoint `/apis/business/authentication/list`.
API Endpoints
Tìm thấy một số API endpoints trong file JS:
var r = n(16),
i = Object(r.e) ('/apis/captcha/get-captcha-js'),
o = Object(r.e) ('/apis/register/send-validate-code'),
a = Object(r.e) ('/apis/register/verify-register-validate-code'),
s = Object(r.e) ('/apis/login/validate-code-and-login'),
c = Object(r.e) ('/apis/login/send-bind-mobile-validate-code'),
u = Object(r.e) ('/apis/login/validate-bind-mobile-code-login'),
l = Object(r.e) ('/apis/login/verify-oneplus-upgrade-validate-code'),
f = n(283),
p = Object(r.e) ('/apis/login/set-password-login'),
h = Object(r.e) ('/apis/login/login-type-list'),
d = Object(r.e) ('/apis/login/validate-password'),
m = Object(r.e) ('/apis/login/upgrade-account');
function v(e) {
return Object(r.e) ('/apis/validate-system/login', !0) (e)
}
function y(e) {
return Object(r.e) ('/apis/quick-login/user-info', !0) (e)
}
function g(e) {
return Object(r.e) ('/apis/quick-login/login', !0) (e)
}
var b = Object(r.e) ('/apis/register/save-and-login'),
w = Object(r.e) ('/apis/login/scan/generate-qrcode'),
x = Object(r.e) ('/apis/login/scan/check-qrcode'),
O = Object(r.e) ('/apis/login/oneplus/password/check-account'),
_ = Object(r.e) ('/apis/oneplus/login/password/upgrade-validation'),
k = Object(r.e) ('/apis/oneplus/upgrade-account'),
E = n(357),
j = Object(r.e) ('/apis/third-party/getShowThirdPartyList'),
S = function (e) {
return Object(r.e) ('/apis/third-party/send-login-validate-code') (e)
},
A = function (e) {
return Object(r.e) ('/apis/third-party/check-validate-code-and-register') (e)
},
C = function (e) {
return Object(r.e) ('/apis/third-party/check-userstatus') (e)
},
T = function (e) {
return Object(r.e) ('/apis/third-party/bind-mobile-and-login') (e)
},
P = function (e) {
return Object(r.e) ('/apis/third-party/check-and-login') (e)
},
I = function (e, t) {
return Object(r.e) (
t ? '/apis/third-party/validate-password' : '/apis/third-party/set-password'
) (e)
},
D = function (e) {
return Object(r.e) ('/apis/third-party/bind-and-login') (e)
},
R = function (e) {
return Object(r.e) ('/apis/third-party/send-bind-mobile-validate-code') (e)
},
L = function (e) {
return Object(r.e) ('/apis/third-party/check-bind-mobile-validate-code') (e)
},
M = function (e) {
return Object(r.e) ('/apis/third-party/authorizationCode') (e)
},
N = function (e) {
return Object(r.e) ('/apis/third-party/login') (e)
},
B = n(456),
F = function (e) {
return Object(r.e) ('/apis/reset-password/v3/used-accounts') (e)
},
V = function (e) {
return Object(r.e) ('/apis/reset-password/v3/check-account') (e)
},
U = function (e) {
return Object(r.e) ('/apis/login/oneplus/forget-pwd/check-account') (e)
},
G = function (e) {
return Object(r.e) ('/apis/reset-password/v3/reset-password') (e)
},
z = function (e) {
return Object(r.e) ('/apis/country/idc-status') (e)
},
H = function (e) {
return Object(r.e) ('/apis/register/oneplus/check-account') (e)
},
W = function (e) {
return Object(r.e) ('/apis/third-party/send-register-validate-code') (e)
},
q = function (e) {
return Object(r.e) ('/apis/third-party/check-register-validate-code') (e)
},
K = function (e) {
return Object(r.e) ('/apis/register/validate-captcha-send-code') (e)
},
Y = function (e) {
return Object(r.e) ('/apis/third-party/register-and-login') (e)
};
function Q(e) {
return Object(r.e) (
'/apis/validate-system/bind-mobile/send-verification-code-to-new-mobile'
) (e)
}
var X = Object(r.e) ('/apis/bind-mobile/validate-verification-code-to-new-mobile')
forum.cdo.oppomobile.com
Trang web này sử dụng windframework
và có thể có lỗ hổng CVE-2019-13472: 💀 Exploit for PHPwind v9.1.0 - Multiple Cross Site Scripting Vulnerabilities CVE-2019-13472.
Ngoài ra nó còn có CVE-2019-6691.
title: Todo: Thử khai thác CVE-2019-13472 và CVE-2019-6691.
ccp.oppo.com
Tìm ra một số endpoint không yêu cầu xác thực:
-
https://ccp.oppo.com/api/commonSetting
-
https://ccp.oppo.com/api/emoji/delete/emojiPackage
-
https://ccp.oppo.com/api/emoji/sort/emojiPackage
-
https://ccp.oppo.com/api/emoji/update/emojiPackage
-
https://ccp.oppo.com/api/kefu/login
-
https://ccp.oppo.com/api/kefu/resetpasd/submitPassword
-
https://ccp.oppo.com/api/kefu/resetpasd/verifySmsCode
-
https://ccp.oppo.com/chat/api/sdk/template/logs/insert
:POST /chat/api/sdk/template/logs/insert
:HttpMediaTypeNotSupportedException
. Với một JSON bất kỳ thì status code là 200. -
https://ccp.oppo.com/chat/api/sdk/template/name/get
:GET /chat/api/sdk/template/name/get?templateId={id}
vớiid
chạy từ 60 đến 264. -
https://ccp.oppo.com/client/sendAudioByH5InWx/enabled
-
https://ccp.oppo.com/openapi/chat/cache/add
-
https://ccp.oppo.com/openapi/chat/cache/get
-
https://ccp.oppo.com/robot/api/channelProperty/robotChannelInfo
-
https://ccp.oppo.com/robot/api/style/gkm/product/getProductNames
-
https://ccp.oppo.com/robot/api/style/idMapping/getUserImei
-
https://ccp.oppo.com/webapi/emoji/emojiPackage/map
-
https://ccp.oppo.com/webapi/user/getLeaveCustomfield
:GET /webapi/user/getLeaveCustomfield?appKey=3858be3c20ceb6298575736cf27858a7
-
https://ccp.oppo.com/webapi/user/getPreSessionInfo
title: Todo: Mở rộng attack surface và fuzzing
- [x] Brute-force để tìm thêm các endpoint
- [ ] Sử dụng `arjun` hoặc Param Miner trên tất cả các endpoint của method là GET
- [ ] Fuzz tất cả các param
Sử dụng ffuf
tìm được thêm các path có GET method như sau:
/root/recon/oppo/single/ccp.oppo.com > select url,content_length,content_type from `ffuf.txt` where status_code not in (500,302) and content_length not in(70,43,54,0)
+-----------------------------------------------------+----------------+--------------------------------+
| url | content_length | content_type |
+-----------------------------------------------------+----------------+--------------------------------+
| https://ccp.oppo.com/chat/api/sdk/template/name/get | 87 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/webapi/emoji/emojiPackage/get | 61 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/api/kefu/login | 87 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/webapi/emoji/emojiPackage/map | 61 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/client/mobile | 6763 | text/html;charset=utf-8 |
| https://ccp.oppo.com/api/polling | 76 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/robot/api/test | 87 | application/json;charset=UTF-8 |
+-----------------------------------------------------+----------------+--------------------------------+
Sử dụng ffuf
tìm ra được thêm các path có POST method như sau:
/root/recon/oppo/single/ccp.oppo.com > select url,content_length,content_type from `ffuf-post.txt` where status_code not in (302,405,500) and content_length not in (0,54,71)
+---------------------------------------------------------+----------------+--------------------------------+
| url | content_length | content_type |
+---------------------------------------------------------+----------------+--------------------------------+
| https://ccp.oppo.com/api/kefu/add | 6148 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/openapi/chat/cache/add | 40 | text/plain;charset=UTF-8 |
| https://ccp.oppo.com/api/kefu/delete | 6148 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/openapi/chat/cache/get | 40 | text/plain;charset=UTF-8 |
| https://ccp.oppo.com/webapi/emoji/emojiPackage/get | 61 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/chat/api/sdk/template/logs/insert | 7133 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/api/kefu/login | 87 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/webapi/emoji/emojiPackage/map | 61 | application/json;charset=UTF-8 |
| https://ccp.oppo.com/api/kefu/update | 6148 | application/json;charset=UTF-8 |
+---------------------------------------------------------+----------------+--------------------------------+
chat-sg.oppo.com
Tìm được danh sách các URL có response type là JSON sau:
-
https://chat-sg.oppo.com/chat-msg/user/msg.action
-
https://chat-sg.oppo.com/chat-set/highModel/treeProductSeriesModel/4
-
https://chat-sg.oppo.com/chat-visit/offline/userRemainDelayTimeWithRobot
-
https://chat-sg.oppo.com/chat-visit/user/config.action
-
https://chat-sg.oppo.com/chat-web/data/postMsg.action
-
https://chat-sg.oppo.com/chat-web/invoke/addTicketSatisfactionScoreInfo.action
-
https://chat-sg.oppo.com/chat-web/invoke/checkUserTicketInfo.action
-
https://chat-sg.oppo.com/chat-web/invoke/getCusMsgTemplateConfig.action
-
https://chat-sg.oppo.com/chat-web/invoke/getTemplateFieldsInfo.action
-
https://chat-sg.oppo.com/chat-web/invoke/getUserDealTicketInfoList.action
-
https://chat-sg.oppo.com/chat-web/invoke/queryTicketTypeInfoList.action
-
https://chat-sg.oppo.com/chat-web/user/cancleQueue
-
https://chat-sg.oppo.com/chat-web/user/completeSession
-
https://chat-sg.oppo.com/chat-web/user/getGroupList.action
-
https://chat-sg.oppo.com/chat-web/user/getRobotSwitchList.action
-
https://chat-sg.oppo.com/chat-web/user/getUserTicketInfoList.action
-
https://chat-sg.oppo.com/chat-web/user/getWsTemplate.action
-
https://chat-sg.oppo.com/chat-web/user/input.action
-
https://chat-sg.oppo.com/chat-web/user/isComment.action
-
https://chat-sg.oppo.com/chat-web/user/leaveMsg.action
-
https://chat-sg.oppo.com/chat-web/user/queryUserCids.action
-
https://chat-sg.oppo.com/chat-web/user/rbAnswerComment.action
-
https://chat-sg.oppo.com/chat-web/user/reviewMethodClick.action
-
https://chat-sg.oppo.com/chat-web/user/reviewMethodClose.action
-
https://chat-sg.oppo.com/chat-web/user/reviewMethodCommentClick.action
-
https://chat-sg.oppo.com/chat-web/user/robotSatisfactionMessage
-
https://chat-sg.oppo.com/chat-web/user/satisfactionMessage.action
-
https://chat-sg.oppo.com/chat-web/user/saveReadNotice
-
https://chat-sg.oppo.com/chat-web/user/userOpenCommit.action
-
https://chat-sg.oppo.com/chat-web/webchat/fileupload.action
Khi tìm trên GitHub các path chẳng hạn như msg.action
, config.action
hay postMsg.action
thì tìm được file sau: blued-7.20.6-src/com/sobot/chat/api/apiUtils/ZhiChiUrlApi.java at 40587cb4b82485161d82170fd8df744a19413aca · lack21115/blued-7.20.6-src. File này thuộc về một project có tên là Sobot.
title: Todo: Nghiên cứu mã nguồn của Sobot để tìm cách tấn công.
Quét source code với Semgrep thì tìm được đoạn code deserialize cookie sử dụng hàm nguy hiểm: sobot/network/http/cookie/PersistentCookieStore.java at f10e386972cf0461f51cbc50c741d48b5cc8105e · semgrep-scanning/sobot:
return ((SerializableHttpCookie) new ObjectInputStream(new ByteArrayInputStream(hexStringToByteArray(str))).readObject()).getCookie();
title: Todo: Thử khai thác lỗ hổng insecure deserizalization
title: Done: Chạy `arjun` cho toàn bộ các URL trên với cả 2 phương thức là GET và POST.
- [x] Xem file `arjun.txt`
- [x] Xem file `arjun-post.txt`
Tất cả các endpoint đều có một param là `callback` (kiểu dữ liệu tùy ý) và nó sẽ reflected vào response dưới dạng là một hàm. Ví dụ, nếu response gốc là `{"openComment":0}` và `callback` là `func` thì response sẽ trở thành `7235({"openComment":0})`. Header `Content-Type` là `application/json` và cặp dấu `<>` bị HTML encoded nên ta không thể khai thác XSS.
Tìm được thêm tài liệu của Sobot: Sobot: All-in-One Contact Center Solution | Omnichannel & AI