#39910 [W&A-Medium] Numerous replay attacks (with arbitrary data) to protected endpoints are possibl
Submitted on Feb 10th 2025 at 15:10:03 UTC by @anton_quantish for Audit Comp | Shardeum: Ancillaries III
Report ID: #39910
Report Type: Websites and Applications
Report severity: Medium
Target: https://github.com/shardeum/json-rpc-server/tree/itn4
Impacts:
Taking and/modifying authenticated actions (with or without blockchain state interaction) on behalf of other users without any interaction by that user, such as:
Changing registration information
Commenting
Voting
Making trades
Withdrawals, etc.
Description
Brief/Intro
An attacker has the opportunity to, being able to intercept signed '/api/subscribe' (or any other protected endpoint) request once, execute it on multiple other JSON-RPC servers with arbitrary request body.
Vulnerability Details
Some of the API endpoints are protected with the debugMiddleware, for instance:
/api/subscribewhich changes the main validator node the JSON-RPC server communicates to;counters debug endpoints;
logs debug endpoints.
This protection allows them to be used only either if the JSON-RPC server is in debug mode, or if the request is signed with the developer key.
Below is the corresponding check code: https://github.com/shardeum/json-rpc-server/blob/616fe1007568db801b0433ece9ef822a0e39d5f6/src/middlewares/debugMiddleware.ts#L47-L81
for (const ownerPk in devPublicKeys) {
const message = {
route: _req.route.path,
count: String(_req.query.sig_counter),
}
const sigObj = {
route: _req.route.path,
count: String(_req.query.sig_counter),
requestHash: crypto.hash(Utils.safeStringify(message)),
sign: { owner: ownerPk, sig: requestSig },
}
//reguire a larger counter than before. This prevents replay attacks
const currentCounter = parseInt(sigObj.count)
const currentTime = new Date().getTime()
if (currentCounter > lastCounter && currentCounter <= currentTime + MAX_COUNTER_BUFFER_MILLISECONDS) {
let verified = verify(sigObj, ownerPk)
if (verified === true) {
const authorized = ensureKeySecurity(ownerPk, authLevel)
if (authorized) {
lastCounter = currentCounter
next()
return
} else {
/* prettier-ignore */ nestedCountersInstance.countEvent( 'security', 'Authorization failed for security level: ', authLevel )
return res.status(403).json({
status: 403,
message: 'FORBIDDEN!',
})
}
}
}
}If the request to a protected endpoint received, and the server is not in the debug mode:
The presence of
sigandsig_counterquery params is checkedIt's checked that the request
sig_counterparam is greater than thelastCounterglobal variableThe signature of request
route pathandcountis checked to be valid for any of the developer authorized public keys.
There are two vulnerabilities:
The
lastCounteris set to 0 when the server starts, which means that the attacker can re-send the signed request to:
the same server after it has been restarted – the signature will be valid, and the count will be greater than 0;
any other JSON-RPC server which has not received any protected calls yet (it's a default state of any server) or has received them earlier than the intercepted request;
The
request datais not included in the signed object, which means that the attacker can arbitrary alter the request params.
Moreover, the /api/subscribe request is HTTP-based (without encryption) and uses the GET method. All the data, including the signature, is contained in the request URL. This means that it could easily be intercepted or even logged by various network devices, such as routers (and then intercepted from there).
Impact Details
Thus, being able to once intercept the signed request to the /api/subscribe endpoint of any JSON-RPC server, the attacker can then arbitrary change the validator node ip and/or port inside the request, and successfully re-send it to other JSON-RPC servers having the same dev public keys configured that, in turn, can paralyze or disrupt the whole network.
The other debug endpoints are also vulnerable so the requests to them could also be forged.
Despite the impact falls under critical, the initial request intercepting is needed, so I downgraded the severity to High.
Proof of Concept
First, let's imitate the authorized /api/subscribe request.
With node REPL:
c = require('@shardus/crypto-utils')
c.init('69fa4195670576c0160d660c3be36556ff8d504725be8a59b5a96509e0c994bc')
utils = require('@shardus/types')
message = {
route: '/api/subscribe',
count: '12345',
}
sigObj = {
route: message.route,
count: message.count,
requestHash: c.hash(utils.Utils.safeStringify(message))
}
c.signObj(sigObj, 'YOUR_AUTHORIZED_DEV_PRIVATE_KEY', 'YOUR_AUTHORIZED_DEV_PUBLIC_KEY')You will see the request signature.
Send it with curl:
curl 'http://127.0.0.1:8080/api/subscribe?sig_counter=12345&sig=f327b7172b297d918ebd6fded5dbf4f008768f0c431660d4a7f7ebfc575d61c4f81b44d3a9e28c462dbbcbbce2ba7c1cef15d98ed306d08b820df90730f0350c09fed708000e521d9ba8e4974beef5eb0c93ab22fab2399e22d0a3f530dc727c&ip=127.0.0.1&port=11111' -H 'Content-Type: application/json'You should see the request is successfully processed (authorization is passed).
Now, as an attacker, arbitrary change the ip and/or port variables within the request and send it to any other JSON-RPC server. You will see the request is successfully processed again and the JSON-RPC server's validator node is changed. You can send it to as many servers as you want if they hasn't received any signed requests yet (because it bumps the internal replay-protection counter) or if they has received them earlier than the counter in your intercepted request.
Mitigation
Initialize the
lastCounterto be equal to the current time on server start, it will prevent the same-server replay attackInclude something instance-specific in the request signature (server IP maybe or something similar), it will prevent the cross-server replay attacks
It's better to also sign the request data itself, not only the route
Last updated
Was this helpful?