

Prologue
There was a FullChain challenge (RCE, LPE and SBX) in Codegate 2025 Final.
While brainstorming ideas for Codegate challenges, we came up with the idea of creating RCE, SBX, and LPE challenges and offering bonus points (FullChain) to teams that solved all of them. That's how the FullChain series came to be.
This series consisted of 4 problems in total: the 3 individual challenges and a FullChain bonus challenge that could be solved by simply chaining them together after solving all three (RCE, LPE, SBX).
Before we begin, a shout-out to the challenge solvers 🙂
- RCE: BunkyoWesterns (First Blood!), The Duck, Blue Water 
- SBX: None 😞 
- LPE: GYG (First Blood!), BunkyoWesterns, CyGoN, Blue Water 
Also, shout-out to our challenge authors 🙂 !!
- Hyungil Moon (@mhibio-ptw), Jongseong Kim (@nevul37) and Dongjun Kim (@smlijun) 
Here is the full challenges and exploit codes [link]
Part 1: RCE

Challenge Overview
The goal of the challenge is to find vulnerabilities in the renderer process and develop an exploit code by analyzing the provided rce-sbx-138-0-7204-97.patch file.
The patch file creates a new Blink module called minishell in the renderer. It provides various shell functions and file writing and saving, and the file data is managed through Codegate File System (CFS), which is a browser API.
The available commands are:
They are similar to the basic shell commands. Commands such as exec are not implemented, but there are several file operations.
When a file is opened, it is managed through file_descriptor_ in the form of a FileBuffer class until Save.
A user can invoke the minishell as follows:
Callable methods can be bound in the *idl file.
In short, one user can have multiple shells and execute each command in one shell.
Vulnerability
We can see the main functionality in mini_shell.cc.
However, the vulnerability is pretty simple compared to the file size. The following shows the FileBuffer structure.
In here, we can see the fixed-size buffer. Let’s check the part which uses it.
There is a size check for the input data vector, but there is no any bound check for idx_ so an out-of-bounds (OOB) read/write occurs.
Although the vulnerability is simple, we need to obtain arbitrary address read / write primitives with this relative address read / write, and finally achieve Arbitrary Code Execution.
Exploit - AAR/W
Now, we have relative read / write primitive of uint64_t size. In fact, there is no difference in the method to achieve arbitrary address read / write.
However, in order to access an arbitrary address, we must know the address of the current object. This is because we need to measure the distance to move to the target.
There are various ways to leak the address of a controllable object.
In this challenge, it is difficult to achieve address leakage with just a simple OOB read because there is no valid address area written anywhere in the heap area. Among them, we tried using brand new technique that can stably leak objects by utilizing the characteristics of Oilpan GC.
Oilpan GC
The Heap object of Oilpan GC has the following structure [link].
Oilpan GC uses a different allocation method than PartitionAlloc (PA), which is mark-and-sweep and space. Unlike PA, which uses slot-bucket, Oilpan allocates space for the heap and divides (i.e., allocates) the heap object as much as requested size from the space when a request comes in.
In other words, without a fixed slot, it dynamically allocates multiple sizes in each space.
When they lose their reference and are GC reclaims them, they take the form of FreeList::Entry.
When an object in the space is freed, the HeapObject changes to a FreeList::Entry, and additional next_ fields are created to point to the next freed object.
Leak Idea
The idea is as follows:
- Loop the action below enough times to allocate new space - Spray shell object 
- Spray File in each shell 
 
- Trigger gc() 
- Read the - next_of the header of the next adjacent chunk of- FileBufferin the- N-thSprayed Object
- Leak - (N-1)thSprayed_object
Since each shell has only one File Buffer, N shells are needed to spray N File Buffers.
Considering the characteristics of the Oilpan GC described above, consider the following chunk situation.

Currently, there is only my object in space. When an object is dynamically divided(i.e., allocated) from space, gc() is executed and the small areas between each object will be treated as Free Entry, forming a FreeList as shown above.
We can now read the chained Free Entry by reading temp = sizeof(FileBuffer) + 0x8 from the Sprayed2 object, and leak the Sprayed 1 address through heap_leak = temp - sizeof(FileBuffer)
This allows us to leak the address of the object with only spray and out-of-bounds, regardless of how big the distance is between Sprayed 1 and Sprayed 2 whether there is a stable address.
Since we have the address of the Sprayed 1 object and the relative address read / write, we can perform arbitrary address read / write.
In the exploit, after sufficient spray, it triggers gc() and then leaks objects 90th to 89th.
Exploit - Arbitrary Code Execution
Now, we obtain the arbitrary address read / write primitives.
In a typical V8 engine, addrof is used to obtain address of a Wasm RWX Page. However, we only have OOB, and it seems difficult to create an addrof primitive.
So what should we do?
Overwrite the vtable of HeapMojoRemote to call 0x4141414141414141?
The challenge says that it should be exploited on chrome.exe running on Windows 11 24H2. That is, in order to achieve arbitrary function calls in the challenge, CFG Bypass must be accompanied. Of course, considering the huge size of the code base, there may be many gadgets that can bypass CFG.
Also, Function::Invoker Chaining, a well-known technique, can bypass CFG.
We wanted to find a more stable method, and after auditing the code, we found that there is a LazyInstance Getter for WasmCodePointerObject. We can leak Wasm RWX Page by reading WasmCodePointerTable → entrypoint_.
Let's overwrite RWX Page with arbitrary shellcode and execute wasm exports function.
In the end, we can stably execute arbitrary shellcode while maintaining persistence. An interesting fact is that the bug of the SBX challenge can be triggered even in the Renderer. However, triggering the vulnerability requires a slight race condition in the SBX Challenge, we are unsure whether UAF Object can be reliably occupied in Blink.
Part 2: SBX

Challenge Overview
This challenge was inspired by a real-world case. So, SBX seems to be the most difficult challenge in the FullChain series.
TL;DR
- Triggering a vulnerability to create a - UAF
- Occupying UAF Object through - Raceand- Spray
- Create arbitrary read / write / call primitives 
The goal of the challenge is to find vulnerabilities in the browser process and develop sandbox escape exploit code by analyzing the provided rce-sbx-138-0-7204-97.patch file.
This patch file creates a new Mojo Endpoint Impl called Codegate File System in the browser.
CFS implements DirectoryImpl and FileImpl. These are structured as a tree with the Root Directory as the root under the File System Manager. Files can execute Read / Write / Edit / Close operations, and Directories have operations such as Create / Delete / Change, etc.
Below is the cfs.mojom file defining the mojom interface.
Vulnerability
The vulnerability in the ChangeItemLocation function is simple in nature but complex in its exploitation. Inaccurate input validation for filename_src / filename_dst causes smart pointer malfunctions, leading to UAF.
The ChangeItemLocation function checks the source and destination through ValidateChangeLocation [1] returns the destination_directory. It then removes the src item from the current directory [2], and performs AddItemInternal[3] to the directory to move
Since it's still difficult to find vulnerabilities, let's look at the ValidateChangeLocation function further.
From this, we can identify several things.
- The src and dst files must always exist. 
- The src file cannot be the current or parent directory. 
- The dst location can be the parent or current directory. 
- If it's a filename, it checks if the file is a directory and returns a pointer. 
It seems to perform all checks well, but one thing is missing.
There is no check for when src and dst items are the same.
In other words, in the following situation [1], the next call [2] becomes a valid call.
To connect this to UAF, let's go back to ChangeItemLocation.
Now that we can set src and dst files to be the same, at the moment just before [3] executes, file_to_move and destination_directory will point the same object.
If we can release file_to_move in this situation, we can make destination_directory dangling and use it as UAF.
To release file_to_move, we need to look at the AddItemInternal function.
At the destination_directory, the ownership of file_to_move is passed to AddItemInternal with std::move()[4] to allow destination_directory→item_list to be bound with ownership.
However, if the new_item that came in with ownership as an argument isn't bound anywhere and the function ends, the reference will be 0 and be released at the end of the function.
The AddItemInternal function checks if a file with the same name already exists in the destination directory, and if so, returns false.
This is an appropriate action for the release scenario described above.
Trigger
Now, let's call ChangeItemLocation again, considering the following situation:
file_to_move(src) and destination_directory(dst) are pointing to /root/dir1.
AddItemInternal is triggered and checks for duplicate filenames in ./root/dir1.
Since there is a duplicate filename ./root/dir1/dir1, the function will return false and finish.
At this point, file_to_move loses its reference and is freed.
Since file_to_move has been released, destination_directory has also been freed.
Because AddItemInternal returned false, PostTask will be executed.  It then passes the freed destination_directory as an argument to RecoverItem, causing UAF to occur when RecoverItem is executed.
Heap Spray
The first step of the exploit is to occupy the freed object.
There are various heap spray techniques in real browser exploitation, but since this is a "CTF Challenge" , the primitive may exists that challengers could easily access.
CodegateFileImpl is the heap spray primitive provided by the challenge.
CodegateFile→Write() stores the incoming array data as std::vector<uint8_t>.
This means we can create unlimited controllable Heap Objects of any desired size.
Let's try to occupy the UAF object using this.
Occupy
To occupy, we need to spray object before RecoverItem, which is posted to ThreadRunner, is executed.
However, the journey to achieve this is a bit complicated.
- Since the Challenge is a Release version, many Code Snippets are excluded, and the speed is extremely fast. This means that the race window between the end of - ChangeItemLocationand the execution of the posted RecoverItem is extremely short.
- Finding an Object of exactly the same size that can be Sprayed from another thread? This can be very helpful when the Race Window is short, but finding a Sprayable Object in time can be difficult, and more effort may be required to achieve - Thread cachebypass.
Fortunately, there are no restrictions on interface calls and the creation of Directories and Files, so let's try to occupy by satisfying the given conditions.
When a Mojo IPC Call comes in, the IO Thread receives it, and after processes like Impl identification and Input Validation, it PostTasks the appropriate function to each Impl ThreadRunner.
All Codegate*** IPC calls will be executed on the same thread, and it's impossible to overwrite UAF Object with CodegateFile while ChangeItemLocation is running.
So, let's try to buy some time between ChangeItemLocation and RecoverItem.

We have called the normally functioning ChangeItemLocation function multiple times and then we called ChangeItemLocation, which can trigger UAF.
The above is the queue of the SequenceTaskRunner. It shows the tasks posted so far waiting for execution.
This way, before the UAF Trigger is executed, we can send additional Mojo IPC call for as long as ChangeItemLocation runs.
If we push in CodegateFile::Write N times during this time,

The running TaskRunner will take the form above, and if time passes and UAF is triggered

It will look like the above. Now the Write Spray work begins, and if N was sufficient, we will eventually be able to occupy the Freed Object.

We can repeat this until successfully overwriting.
In the exploit, we can improve stability by comparing the file name, vector, etc., to check if it has been successfully overwritten.
Leak
After overwriting the UAF Object, we need a controllable area and address to avoid crashes and control the flow. There are various techniques to achieve this, but in this challenge, we'll use std::vector<> to allocate our objects at a Known Address. 
(With a few attempts, you will see that it is s impossible to receive a leak through Mojo Response.)
When std::vector<> reaches full capacity, it releases the existing heap, allocates a new heap with increased capacity, and moves the existing data.
Using CodegateFile→Read(), we can leak UAF Object->CodegateItem→itemname_. Additionally, using CodegateFile→Edit(), we can manipulate UAF Object→vector{start, last, end}
The next step is to create & leak a Known Address of size 0x800, then occupy that area.
- UAF object parents→Rename(uaf_object, "A" * 0x800)- The - std::stringwill be allocated in a heap slot of size- 0x800.
- This - 0x800size area will be used later for Fake Object, ROP, Temp Memory, etc.
 
- UAF Object→Read()to leak- UAF Object→itemname_.
- Modify the - UAF Object Vectoras follows:- UAF Object→Vector→start=- heapleak
- UAF Object→Vector→last=- heapleak
- UAF Object→Vector→end=- heapleak + 0x800
  
- Perform - UAF Object→CreateItem- 0x800 / 8times.- The vector will point to - std::string, and the 0x800 size will be filled.
  
- Perform - UAF Object→CreateItemone more time.- Due to insufficient capacity, the existing area ( - std::string) is released, and the existing data is moved to the new vector space.
  
- Perform - Spray(0x800, spray_cnt).- We can now occupy the freed 0x800, and this address becomes the one leaked in steps 1 and 2. 
  
In this way, we can achieve Known Address creation, leaking, and occupation, and now we can make Fake Objects, ROP Chains, etc. in that area.
Simply using rename() itself for Spray is not suitable because the JS → Mojo → Impl encoding conversion process doesn't properly recognize Null Characters and characters in the UTF-8 range, which is why we need to use the method above. The same goes for Leak .
In exploit, we can use specific fields(here, std::string item_name_) as success identifiers.
AAR/W Primitive
CodegateFileImpl has the same structure as CodegateDirectoryImpl, but the std::vector type is uint8_t.
By creating a Fake CodegateFileImpl, adding it to the UAF Object, and manipulating the m_first, m_last, and m_endof the Fake CodegateFileImpl, we can achieve arbitrary address read / write.

Here is simple Exploit POC for AAR/W.
Arbitrary Call Primitive
By setting the UAF Object→directory_vtable to a controllable heap object and manipulating only CodegateDirectory→ListItems, we can achieve arbitrary function calls.
Like the Renderer, the Browser Process also has CFG Mitigation enabled.
This time, we use the well-known technique of Didwrite → Function Invoker Chain to achieve CFG Bypass and arbitrary function calls.
Since we've manipulated the vtable of the UAF Object and achieved arbitrary function calls, rcx(this) currently points to the UAF Object.
The variable ptr[1] will be the value of UAF Object + 0x10, and [2] allows us to make a new function call (ptr+8).
We maintain the RefCount at ptr+0x0 as 1 (gadget constraint), and set ptr+0x8 with useful Gadget from Function Invoker.
The gadget above looks good for our situation.
At the point when the Function Invoker is executed, rcx can be manipulated to an Arbitrary Address, so we can set it to our obtained Heap Leak, allowing us to configure the function and argv as desired.

Arbitrary Call achieved!
Part 3: LPE

The files provided in the challenge are minimal. Aside from the pre-distributed Windows 11 image, only the PoW (Proof-of-Work) code and the MemoryStorage.sys file were given. We should analyze the MemoryStorage.sys driver and use it to achieve privilege escalation on Windows.
Challenge Overview
MemoryStorage.sys is a Windows Kernel Driver, developed using the legacy Windows Driver Model (WDM). When examining the driver's DriverEntry function, it contains only two functional components: one function responsible for initializing the kernel stack cookie and another function that handles the main routine of the driver.
In the sub_14000173C function, the driver creates a symbolic link named \DosDevices\MemoryStorage and registers the device with the kernel. This symbolic link allows user-mode applications to communicate with the driver by sending requests.
Inside the conditional block, the driver registers its dispatch routines. The entries a1->MajorFunction[0] and a1->MajorFunction[2] correspond to the IRP_MJ_CREATE and IRP_MJ_CLOSE routines, which are called when a device handle is opened or closed. These routines are not essential for solving the challenge, so they are mentioned only for completeness.
The most important part is the assignment to a1->MajorFunction[14], which registers the sub_140001370 function as the handler for IRP_MJ_DEVICE_CONTROL. This routine is responsible for handling various commands based on I/O Control Codes (IOCTLs), and it plays a central role in the vulnerability analysis.
Let’s now take a closer look at the sub_140001370 function. This function acts as a handler that processes commands based on four specific I/O Control Codes.
Before examining the behavior of each code, it is helpful to briefly review the structure of an IRP (I/O Request Packet), which is used when sending requests to a kernel driver. The IRP is a fundamental data structure in Windows used to represent I/O operations, and it carries information about the requested operation, the involved device, and associated buffers. Understanding how IRPs work is important for analyzing how the driver handles user-mode requests.
IRP Structure
An IRP (I/O Request Packet) is a kernel-level structure used to handle I/O requests between the operating system and device drivers. It follows a specific internal layout, and one of its important fields is CurrentStackLocation, which points to an IO_STACK_LOCATION structure. This structure helps each layer in the driver stack process the IRP appropriately.
Every I/O request is handled according to the information stored in the IRP and its associated IO_STACK_LOCATION structure. These fields allow the driver to process each request in a way that fits its purpose.

For example, the sub_140001370 function processes requests based on the I/O Control Code. In this case, the MajorFunction field in the IO_STACK_LOCATION structure has the value 14, which corresponds to IRP_MJ_DEVICE_CONTROL. When the IoControlCode field matches a specific value, the driver executes the function that implements the behavior for that particular I/O request.

The values shown in the sub_140001370 function all come from fields within the IRP structure. Since the IRP contains many fields, it is not practical to explain all of them here. For a full reference, please refer to the official documentation at here.
For the purpose of solving this challenge, we will focus only on the Type3InputBuffer field located inside IRP->CurrentStackLocation. This field points to the user-provided input buffer, which the driver uses to receive data from user space.
Type3InputBuffer is used as the input buffer when a request is sent using the Neither I/O method. This leads us to another important concept: what exactly is the Neither I/O method?
Neither I/O
According to the official documentation [link], the Neither I/O method does not provide a SystemBuffer (a kernel-mode buffer) or an MDL (Memory Descriptor List) when accessing the input or output buffer. Instead, it uses the user-mode virtual address directly. This means that in I/O requests using this method, the data provided by the user remains in user space and is not copied into kernel memory.
In this context, the input buffer is handled through the Type3InputBuffer field within the IO_STACK_LOCATION structure, while the output buffer is accessed via the UserBuffer field in the IRP structure. These pointers reference memory located in user space, and it becomes the driver's responsibility to validate and safely use them.

Identifying whether an I/O Control Routine uses the Neither I/O method is straightforward. If the I/O Control Code, when divided by 4, leaves a remainder of 3, it indicates that the routine uses the Neither I/O method.
In simpler terms, if the last half-byte (nibble) of the I/O Control Code is one of the following values: 3, 7, B, or F, then the corresponding routine is using Neither I/O.
Keep Analyzing sub_140001370
Let’s now continue analyzing the sub_140001370 function. As explained earlier, we can determine that the I/O Control Codes 0x7101003 and 0x7101007 use the Neither I/O method. This means that within the routines handling these codes, the driver uses the Type3InputBuffer field as the input buffer.
Since Neither I/O directly passes a user-mode virtual address to the driver, the buffer referenced by Type3InputBuffer resides in user space, not in kernel memory. Therefore, any data access using this pointer happens in the user’s address space unless the driver explicitly validates or copies it into a safe region.
Root Cause Analysis: Stack-based buffer overflow
Let’s analyze the function sub_140001274, which handles the I/O Control Code 0x7101003. This function copies data from a user-supplied memory region into a local kernel buffer, named Dst, by following five main steps:
- The function first checks whether - Type3InputBufferis a valid user-mode pointer and whether its size is at least- 0x10bytes.
- It then examines the first two bytes of - Type3InputBufferand checks whether the value is greater than- 0x40and not equal to zero.
- The function reads an 8-byte value from the address at an 8-byte offset within - Type3InputBuffer. This value, which represents another pointer, is stored in the local variable- v4.
- Using - ProbeForRead, it validates whether the address stored in- v4is a readable user-mode address.
- If all the above checks pass, the function copies a number of bytes equal to the value read in step 2, from the memory pointed to by - v4into the local variable- Dst.
Through this process, the driver attempts to read and copy user-supplied data, but as we will see later, this logic can be exploited if the validations are incomplete or misused.
At first glance, it appears that all user-supplied addresses and values are properly validated, making the function seem secure. However, remember that Type3InputBuffer points to user-mode memory.
The flaw in this code arises in step [2]. In this step, the function reads the first two bytes of Type3InputBuffer and checks whether the value falls within a valid range. But in step [5], when calling memcpy, it does not use the previously stored v3 value. Instead, it reads from Type3InputBuffer again, causing a double fetch.
This allows the user to trigger a race condition and copy more than 0x40 bytes into the local variable Dst.
However, in order to exploit this stack-based buffer overflow vulnerability, leaking a kernel address is essential. So where does the vulnerability that allows kernel address leakage exist?
Root Cause Analysis: Information Disclosure
Let’s analyze the function sub_14000113C, which handles the I/O Control Code 0x7101007. This routine also uses the Neither I/O method. The function can be broken down into the following four steps:
- It first checks whether - Type3InputBufferis not- NULLand whether the input buffer length is at least 8 bytes. It then uses the- ProbeForReadfunction to verify that- Type3InputBufferis a valid user-mode address.
- It checks whether the first 2-byte value of - Type3InputBufferis non-zero and less than or equal to- 0x40.
- It allocates a memory pool of size - 0x10000bytes.
- It copies the contents of the local variable - Dstinto the allocated pool, using the size specified in the first 2 bytes of- Type3InputBuffer.
- The allocated pool is then stored in the global array - qword_140003080.
This function, like the previous one, is also vulnerable to a double fetch between steps [2] and [4]. Because of this, more than 0x40 bytes of the Dst variable can be copied into the pool. The question is: where can this copied data be read from?
The answer lies in the function sub_1400014CC, which handles the I/O Control Code 0x7101010. This function, named LoggingMemoryInformationForInternalMemoryStorageDriver, performs the following steps.
First, it creates a Section. Then, it maps 0x10000 bytes of memory to the Section. After that, it copies the contents from the global array qword_140003080 into the mapped Section memory. Finally, it unmaps the Section and closes its handle in Kernel Mode.
This raises the question: How the data inside the Section can be accessed if it is unmapped and closed immediately.
The answer is that on Windows, if a user-mode process opens the Section in shared mode beforehand, the kernel is unable to fully unmap and close the Section. By occupying the Section from user mode before the driver releases it, it becomes possible to access the data stored inside, including leaked kernel addresses.
For reference, some of the leaked kernel addresses are shown below. Since parts of both ntoskrnl and the kernel driver's addresses have been leaked, there should be no issue in constructing a Stack ROP chain 👍
Exploit
Now that all necessary information has been gathered, we can exploit the stack-based buffer overflow using a ROP chain to obtain SYSTEM privileges. The ROP chain operates in the following sequence:
- Before entering the ROP chain, a - cmdprocess running with Medium Integrity is created. This process continuously attempts to read- C:\Windows\System32\flag.txtusing the- curlcommand.
- Using the PID of the - cmdprocess, the ROP chain calls the- PsLookupProcessByProcessIdfunction to retrieve the EPROCESS address of the- cmdprocess.
- The same function is called again to obtain the EPROCESS address of the System process. (In Windows, the PID of the System process is always 4.) 
- The Token value of the - cmdprocess is then overwritten with the Token value from the System process.
Once this chain completes, the cmd process that was initially created will be running with SYSTEM privileges. However, the process is not yet complete. Because the ROP chain was executed in Kernel Context, control must be returned to User Context. Additionally, some time is needed for the cmd process to read flag.txt and send the content outward.
Although functions like KiKernelSysretExit could be used to return to User Context, this particular exploit only required a brief delay to allow the data transmission. To achieve this, a simple \xEB\xFE gadget was used at the end of the ROP chain, which causes an infinite self-jump, effectively stalling the kernel and giving the user-mode process enough time to complete its task.
Part 4: FullChain

RCE to SBX
The existing RCE-SBX chaining method was to change enable_mojo_js to True. However, a year ago, Chrome introduced a new Mitigation [link] to prevent exploitation.
Mitigation Detail
The existing method to enable mojo_js was as follows:
- Find the current - RenderFrameImplin the chrome binary,
- Overwrite the member variable - enable_mojo_jsof- RenderFrameImpl
However, the new mitigation does the following:
- If ScriptContext is not finished, grant ReadOnly permission to - enable_mojo_jsarea via- ProtectMemory.
- Mojo binding can be enabled only when ScriptContext is finished. 
That is, it is impossible to overwrite mojo_js_binding during Script execution (= during Exploitation ).
Now, there is a difficulty in chaining with the existing method.
Bypass
We can do endless things after achieving arbitrary code execution. For example, “making ReadOnly memory into ReadWrite memory”.
As mentioned earlier, whether enable_mojo_js is enabled or not is now managed in ExecuteContext.
The default flags applied to the new ExecuteContext are managed globally with ProtectedMemory applied.
In other words, if the global enable_mojo_js default flags managed by (ReadOnly)ProtectedMemory are set to true and a new Script Context is created(Reload), the context will be able to use mojo bindings.
We can use base::AutoWritableMemoryBase::SetMemoryReadWrite to grant rw permission to protected memory, and base::AutoWritableMemoryBase::SetMemoryRead to grant readonly permission to protected memory.
If we execute the shellcode that does this and then execute windows.reload, we can normally obtain a context where mojo_js_binding is activated.
DEMO
Epilogue
Web browsers remain high-value targets consistently exploited in numerous in-the-wild attacks. Despite extensive security mitigations introduced by Chrome over the years, our full-chain exploit demonstrates that bypassing these protections remains feasible. Similarly, although Windows Control Flow Guard (CFG) provides robust protection mechanisms, sophisticated techniques exist to effectively circumvent these defenses.
Creating this CTF challenge series has been a rewarding experience, and we sincerely hope participants enjoyed tackling these challenges as much as we enjoyed developing them. Our goal was not only to provide engaging, technically intricate challenges but also to reflect real-world exploitation scenarios, showcasing the latest trends and bypass techniques employed in actual threat environments.
Moving forward, we are committed to continuing our exploration of emerging exploitation techniques and developing challenges aligned with the latest cybersecurity trends. Our research journey is ongoing, and we have no intention of slowing down. In future posts, we aim to delve deeper into analyzing genuine in-the-wild vulnerabilities, demonstrating real-world bug chaining techniques, and sharing our insights and methodologies with the broader security community.
Thanks to the all participants and readers for your interest and engagement. Stay tuned for more groundbreaking research and practical insights.
Popular Articles










