Back to all stories
Blogs
Tech & Dev
Web2 Meets Web3: Hacking Decentralized Applications
8/27/2024
Web2 Meets Web3: Hacking Decentralized Applications

Note: The following information was originally presented by Peiyu Wang, a security engineer from CertiK, at the Appsec Village at DEF CON 32 on August 10, 2024.

The integration of Web2 and blockchain components within decentralized applications (Dapps) introduces a new frontier of security challenges that demand close attention. As Dapps continue to evolve and experience growing adoption, they face a wide range of vulnerabilities and attack vectors unique to their hybrid architecture. Developers and security professionals must therefore contend with risks orchestrated by malicious actors, such as client-side attacks that steal users’ crypto assets, and server-side attacks that lead to system compromise and total financial loss.

This blog will explore these complexities through real-world examples, offering insights into the differences between traditional Web2 applications and Web3 Dapps, Dapp threat modeling, and unique attack vectors enabled by the integration of blockchain components.

Web3 Dapp architecture

The following diagram illustrates the simplified architecture of a standard Web2 application. In this model, the front-end application runs in the client’s browser, while the server hosts the webpage files and server-side APIs. The client and server communicate via a protocol, most commonly the HTTPS protocol.

Image 1

Web3 Dapp architecture can be categorized into three types:

Image 2

“API-less” Dapp

As the name suggests, this type of Dapp does not have server-side APIs. The server’s sole purpose is to serve the webpage files. The primary logic of this application resides within the smart contract, and the client-side application functions as a UI to display data and handle contract transactions based on user input.

An example of such a Dapp is the 2020 version of Uniswap. From an application security perspective, the attack surface for this type of application is relatively limited; if exploited, it is often due to vulnerabilities within the smart contract itself. The client-side app often uses frameworks such as React, which includes built-in XSS filters.

“API-Enabled” Dapp

This type of Dapp includes a server-side API, but its backend does not communicate with the blockchain. Its front-end serves the same purpose as in the “API-less” Dapp.

An example of an “API-Enabled” Dapp is an NFT minting application, where the server provides an API for users to upload images and metadata, returning a URL for the image and metadata to the user. The user can then interact with the smart contract to mint an NFT using these URLs. In this case, the server functions as centralized storage for NFTs.

“Full-Scale” Dapp

The final type is a “Full-Scale” Dapp, where the backend not only serves web content, but also communicates with the blockchain and interacts with the project’s smart contracts. In these Dapps, smart contracts often contain privileged functions that can only be executed by a role known as the “owner.” The private key associated with the owner role is stored on the backend server. If this server is compromised, the consequences are severe, as the attacker could obtain the private key and gain complete control over the smart contract and its associated crypto assets. Attackers can also exploit business logic errors in the API to cause financial damage to the protocol.

An example of this type of Dapp is a cross-chain bridge. Since smart contracts on different blockchains cannot communicate directly with each other, an off-chain component is required to facilitate the communication and operation. This off-chain component is often critical to the security and functionality of the bridge, making it a high-value target for attackers.

Dapp threat modeling: The “desired” key

From a threat modeling standpoint, the most sought-after “assets” in Web3 are the actual crypto assets, such as Bitcoin or Ether. When attempting to acquire these assets, malicious actors seek seed phrases/private keys, signatures (signed “permit” messages), and API keys — all of which provide access to assets and custodians, and enable contract state manipulation. It is possible to acquire this information due to vulnerabilities on both the client and server sides, emphasizing the need for higher Web2 security within Web3 applications.

Below are some common Web2 vulnerabilities on both sides.

Web2 vulnerabilities on the client side

When examining client-side attack vectors, it is crucial to first understand the concept of a “site with already-connected wallets.” Users who visit Dapps for the first time are prompted to directly connect their wallets to the Dapps. This connection is often permanent, unless the user manually disconnects, and only a connected Dapp can send transactions to a wallet for its user to sign.

Unfortunately, malicious actors can exploit this feature through the following methods:

  • Initiating malicious transactions: On a “site with already-connected wallets,” a vulnerability that allows an attacker to inject JavaScript and initiate malicious transactions would be classified as critical. Web2 vulnerabilities that can lead to this include Cross-Site Scripting (XSS), subdomain takeovers, DNS hijacking, and supply chain attacks.
  • Deceiving users into signing malicious transactions through UI element tampering: If modifying the website's JavaScript to initiate a malicious transaction is not feasible, a malicious actor might employ HTML injection or clickjacking. These techniques can deceive users into unknowingly signing a malicious transaction by manipulating the user interface.
  • Redirection to a phishing site: Vulnerabilities that are often considered low severity in Web2, such as “open redirects,” can be more severe when involving crypto due to the possibility of victims' assets being permanently lost.

To reduce the impact of the above attack vectors, users should only connect to sites they trust. Oftentimes, users who trust sites will automatically sign transaction requests without much consideration. Consequently, if a client-side vulnerability exists in a popular Dapp with many connected users wallets, the severity of the vulnerability will dramatically increase.

Web2 vulnerabilities on the server side

Attackers also aim to steal crypto assets or cause negative financial impact to the project by exploiting vulnerabilities on the server side. The following server side attack vectors are common:

  • Stealing keys: Via various Web2 vulnerabilities, such as RCE, SQLi, LFI, directory traversal, SSRF, debug pages, or server misconfigurations, can leak private key information.
  • Stealing crypto assets from other user accounts or via admin features: Broken access control and privilege escalation enable attackers to steal crypto assets from other users’ accounts.
  • Spending or owning more assets than a user’s actual balance: Malicious actors can achieve this through business logic errors or race conditions.
  • Taking down a server that hosts NFT images and metadata.

To summarize, when testing a Web3 Dapp, once a Web2 vulnerability is identified, consider how it could be exploited in a Web3 context. This approach often allows you to escalate the severity of the identified vulnerability.

Unique attack vectors enabled in Web3 applications

The integration of Web3 components into various applications has introduced unique attack vectors. Based on past findings when testing Dapps, we have broken down these attack vectors into three primary categories:

The mishandling of blockchain transaction characteristics:

  • Is the on-chain transaction constructed with accurate data in the correct format?
  • Is there an API that leads to blockchain transactions and can be called without restrictions?
  • Does the API wait for the transaction to finish before sending the HTTP response?
  • Is the API’s handling logic designed to accommodate the extended duration of transactions?
  • Does the application include sensitive information in the transaction data?
  • Can the application correctly distinguish different transaction types (e.g., internal transactions)?
  • Does the application correctly handle reverted or failed transactions?
  • Can a malicious payload be injected into transaction calldata?
  • Does the backend wait for enough block confirmations?

Initiating attacks against Web2 backend components via smart contracts:

  • Does the application store any sensitive information in smart contracts?
  • Are the data and smart contracts stored in the database consistent with each other?
  • Is there anything that could cause the data in the backend database and smart contracts to be out of sync?
  • Can a malicious payload be injected into the backend via data stored in the smart contract?
  • Is there a way to tamper with data in the “event” emitted by the smart contract to exploit the backend?
  • Does the backend verify the smart contract address when handling requests?
  • Does the smart contract contain vulnerabilities that could be abused to exploit the backend?

The mishandling of crypto asset operations:

  • Does the backend correctly process different types of assets (e.g. native token vs. ERC-20 tokens)?
  • Does the backend ignore failed or reverted token transfer transactions?
  • Does the backend correctly handle token decimals?
  • Can a malicious payload be injected into the backend via the token name, symbol, or metadata?
  • Does the backend ignore self-deployed fake tokens?
  • Does the backend accept token deposits via internal transactions and handle them correctly?
  • Can the backend correctly handle special tokens, such as deflationary or rebase tokens?
  • Does the backend handle slippage correctly during token swaps?

The following case studies demonstrate what might happen if the integration of Web3 components is incorrectly handled.

Case Study #1: Wasted Crypto in Gas via Unrestricted API

The first example is a token airdrop Dapp, which requires users to register their walled addresses for participation. First, a user would call the “/register” API, providing their on-chain address. The backend would then call the “register()” function in the smart contract, which can only be called by the smart contract admin address.

During the pentest, the auditor found that the API accepts any Ethereum address, as long as the address data is in the correct format. Every time the API is called, the backend sends a transaction to the smart contract.

Consequently, a malicious actor might repeatedly call the API with randomly-generated addresses to consume all the ETH tokens in the admin's wallet as gas for the transactions.

Image 3

Case Study #2: Poor Transaction Time Handling Leads to DoS

This vulnerability may exist in any Dapp that has APIs triggering the backend to send an on-chain transaction. In the vulnerable implementation, the HTTP server keeps the connection open while waiting for the transaction execution result before returning the HTTP response, as shown in the GPT generated example code below.

Image 4

We discovered that, for blockchains with long block times (e.g., 12 seconds for Ethereum), the HTTP connection will remain open for a considerable amount of time. The API server can be easily subjected to a DoS attack with concurrent requests sent from a single machine.

Case Study #3: Poor Transaction Time Handling Leads to Race Condition

In the following GameFi Dapp case, users can purchase in-game items with in-game currency. To obtain the in-game currency, users need to deposit crypto assets into a smart contract, after which the backend updates their in-game balance. To withdraw and convert the in-game currency to crypto assets, the process works as follows:

  1. Users submit a withdrawal API request to the backend.
  2. The backend validates the user’s balance and calls the "withdraw()" privileged function in the smart contract to release the crypto assets to the user.

Image 5

During the pentest of the application, the auditor discovered that the backend updates the user’s in-game balance, only after confirming that the transaction is successfully executed on the blockchain. However, the backend didn’t “lock” the user’s balance before sending the on-chain transaction, enabling users to purchase items during the transaction execution time and receive crypto assets, essentially obtaining in-game items for free.

Case Study #4: Bridge Backend Lacks Smart Contract Address Validation

A cross-chain bridge works as follows:

  1. A user deposit tokens into the smart contract on the source chain and obtains the transaction hash.
  2. The user then submits the deposit transaction hash to the backend via API.
  3. The backend fetches information from the blockchain for the transaction hash and validates the deposit.
  4. Once the validation confirms everything is correct, the bridge backend releases funds to the user on the destination chain.

A token bridge backend needs to extract and validate a lot of information from the deposit transaction, such as the token amount, the token type, and the sender’s and receiver’s addresses. The auditor discovered that the backend failed to validate that the smart contract receiving the token deposit was the one operated by the project team.

To exploit the validation vulnerability, an attacker can deploy a fake bridge contract, perform a deposit into the self-deployed contract, submit the transaction hash to the API, and wait for the backend to misprocess it and release the token on the destination chain.

For more details on this finding, view the pentest report here.

Case Study #5: Mishandling of Asset Classes

Our final example involves a lightweight crypto trading application where users can deposit crypto assets and perform token swap activities. The backend of this application uses Fireblocks as its crypto custodian service, and its deposit flow is as follows:

  1. A user submits a request to get the deposit address, and the backend retrieves one from Fireblocks.
  2. The user then deposits crypto assets into the address owned by Fireblocks.
  3. The backend periodically queries the address transactions via the “fireblocks.getTransactions” API.
  4. The user's in-app balance is increased once the API returns deposit transaction information.

When we deposited 0.1 MATIC into the deposit address, the balance of our account increased by “1.1.” To investigate this odd result, we viewed the on-chain address data in a blockchain explorer and confirmed that the token indeed arrived at the address with 0.1 MATIC. Upon further verification, we found a record of a scam NFT (ERC1155) airdropped into the address.

Image 6

The results showed that the backend recognized the receipt of one scam NFT as a 1 MATIC token deposit. When reviewing the source code, we then discover the root cause: The backend fails to distinguish the token type when processing transaction information returned from Fireblocks. To create a PoC for this finding, the auditor deployed an ERC-1155 contract and minted 99 valueless NFTs to the address, which caused the account balance to increase by 99 MATIC.

The State of Dapp Security

As Dapps evolve and integrate more complex Web2 backend systems, new security challenges emerge. Developers who are not well-versed in both Web2 and Web3 security may inadvertently introduce vulnerabilities, which can have severe consequences. While Web3 exploits, such as those involving smart contracts, are publicly visible, Dapps exploited through Web2 components might have less exposure. To mitigate these risks, it is essential to perform rigorous penetration testing in addition to smart contract audits, ensuring a robust security posture across both Web2 and Web3 elements of the application.

At CertiK, we have conducted extensive research on various vulnerabilities common in the ecosystem. For thorough penetration testing of applications, audits of your smart contract code, or consultations with our team of experienced auditors and security experts, please get in touch with us at CertiK.com.