Resources

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!

Taking home a $20K bounty with Oasis platform shutdown vulnerability - 1074

Trust SecurityPosted 3 Years Ago
  • Oasis offers leveraged trading and borrowing, among other things. Oasis Earn had recently been added to the scope of Immunefi. Since they had audited this before with no findings, it was worth taking some time to go through the new code.
  • Oasis Earn offers a variety of trading strategies. This is done by users keeping their funds in a private DSProxy smart wallet. To perform a trade, they delegate execution to the Earn smart contracts, which implement the trading strategy. Users send ETH to DSProxy, which will perform the specified actions for trading automatically.
  • The Earn smart contracts use a delegateCall pattern. This allows for the editing of state variables from any location but is a very dangerous case. Earn has a large collection of Actions, such as Swap and SendToken. The real meat of this call is the executeOp and its allowances:
    • ServiceRegistry: A mapping between service names and their ETH address.
    • OperationRegistry: An array of allowed actions.
    • OperationStorage: State for the operation execution. This is done because the local state belongs to the user's smart wallet.
    • OperationExecutor: Contract called directly from the original DSProxy call. This receives an array of calls with calldata to execute the operations explained above.
  • What's the bug then? Oasis Earn made a nice but terrible assumption: executeOp will run within the context of a user's DSProxy. What can we do with this? Storage is unique per execution (as explained above) but a selfdestruct() would be a permanent brick if we could make that happen.
  • This attack had a few constraints that made exploitation extremely difficult and limits the options for exploitation:
    • OperationsRegistry must be in the array of actions for each operation name.
    • delegateCall target must be within the allowlisted service addresses.
    • OperationExecutor storage must be empty, since we didn't call it from the proper location - DSProxy.
  • Once here, the author thought about code reuse (ROP, JOP, etc.) attack from the binary exploitation world. The primitive is an arbitrary delegateCall to a limited set of contracts with arbitrary calldata. The list of contracts that can be called is for the entire Oasis platform and NOT just Earn. The author wrote a script to parse all of the potential functions they could jump to using this method.
  • InitializableUpgradeabilityProxy is a proxy for the AAVE LendingPool implementation. If the data provided for the initialize call is empty, and the previous implementation is 0, then we have created a strange state confusion problem. The best part: there's a delegateCall at the end of this with data we control! Now, we've taken a limited address delegateCall and turned this into an arbitrary address!
  • One last trick... the call we want to make will get rejected because a function verifyAction will reject it since LendingPool was not a registered action by the Oasis Earn. The function hasActionsToVerify validates that there is a list of actions; if so, it will verify. However, passing in a CustomOperation bypasses this verification while making the action still usable. Neat!
  • The POC:
    1. Create the selfdestruct contract.
    2. Generate the calldata passed to executeOp.
    3. Call executeOp() with initialize() calldata, targetHash=InitializableUpgradeableProxy service hash,operationName = CustomOperation
    4. Hit the boom! Contract destroyed and funds are lost.
  • Overall, a very complicated article and project! First, a design decision had unintended consequences. Then, the complexity of the modular system created the opportunity for exploitation via a classic binary exploitation technique. Pretty rad bug and exploit!