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:

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 __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, noncetimestamp. 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&key=FdjydGAAKasmht1nFnR4MS5itFeh4R1Lk, dạng serialized sẽ là: appKey=CuGsbe6HdAe6vDBHFew2Di&nonce=1750861745141&timestamp=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&timestamp=1750861745141&key=FdjydGAAKasmht1nFnR4MS5itFeh4R1Lk45bff6cc09d8a21830a41876508a18f1: 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 SameSiteNone:

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/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 processTokenticketNo (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ới id 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

Resources