People often ask me "How did you learn how to hack?" The answer: by reading. This page is a collection of the blog posts and other articles that I have accumulated over the years of my journey. Enjoy!
Notification:Save() function calls into taskSave(). When saving, it checks the objects origin and notification. It performs a write like this -
this.notifications[origin][notification.id] = notification;
origin is set to __proto__ then notification.id will also be part of the write, with the rest of the notification being the value written. Using this, any global JavaScript can be overwritten! Since is not just limited to the NotificationDB.jsm either; it affects all JavaScript modules for any Chrome-level things.TabAttributes.jsm module, there is some code that iterates through the element of a list called data using a for. Luckily for us, this will only iterate over prototypes! Using this code, it's possible to set arbitrary HTML (typically XUL) attribute of a tab. To trigger this, there are a few ways but one of them is the most convenient - crash the tab and on automatic reinitialization the pollution happens.onunderflow attributes to execute arbitrary JavaScript within the Chrome process. Since this is highly privileged process, compromise is fairly trivial. security.sandbox.content.level to 0 in order to prevent sandboxing in new tabs for the future. From there, we open a new tab and call C:\\Windows\\System32\\cmd.exe to execute arbitrary commands. Game over at this point.GatherAsyncParentCompletions is called. Within this, there is a call to array.push, which uses the prototype hierarchy. By setting the getter/setter for the prototype, we can trigger an external call to me made. Why does this matter? We can get access to the module type in JavaScript!UnsafeSetReservedSlot()slot_ array.VirtualProtect from their shellcode in order to circumvent the JIT W^X protection.performConversionForTokens() with a path of different tokens. However, this doesn't contain an allowlist or checks for reentrancy.balanceOf for TONIC on the current contract. The sent in amount of TONIC is thought to be amount that the user now has, crediting them all of this.stake() part way through the execution of this, an attacker can send in tokens as part of the stake. So, they get counted both as the stake and as part of the transfer, leading to a double use of the TONIC. By repeating this over and over again, it's possible to steal almost all of the funds from the protocol.MarkerTransferAuthorization with an authz wrapped call. So, the modules bank, authz, auth and marker were the only possible culprits here.time.Now() value in Go. Yikes! A big source of non-determinism. When performing an upgrade, nodes that came on sooner were fine on these grants. However, some nodes that came on later after the upgrade would have been this grant fail! In the end of the day, this was actually an issue with the Cosmos SDK itself. transfer-encoding and the size of these chunks. So, with this, they had a potential vulnerability. After some effort with playing with these values, they got a crash. What was the crash about? The value 0x0a0d could be written to an arbitrary offset on the stack./bin/init binary, they found a signature check being performed but assumed more checks were in it. So, they patched the function do_halt() to not exit.0xXXXXX0a0d. With this return addresses, saved base points and locals did not seem like get paths to hit. So, they decided to target a heap address that was on the stack that contained function pointers.system function. Eventually, they got this working with a payload to run one of the limited commands in the shell. They eventually came up with a more complicated ROP chain to get a shell though.Gateway contract. requestRemoveMargin() it emits an event for a bot to see. Once the bot sees it, it will call finishRemoveMargin() with signed event data and a signature to finalize the request. finishRemoveMargin, finishUpdateLiquidity and finishLiquidate. In the former two, they have an internal function for checking the _checkRequestId to increment the nonce to prevent replay attacks.finishLiquidate(). However, since the position NFT would have already been burned then it would have failed anyway. So, no issue, right?cumlativePnlOnEngine field in the liquidate struct matches the requiredMargin field. Since the verification happened in the previous call, there is no validation on any of these values!abi.decode() didn't fail with the different data lengths. Overall, good finding with a fun write up!withdraw() to transfer ETH, the totalAssets were being updated after this call. So, the classic reentrancy was on!0xABAB.... It's possible to downgrade a user with a MitM to force this version of the protocol. Again, amazing.strstr function as well, so de Bruijn sequences can be used to make this even more efficient. There is a lockout for failed attempts here though.