Listen to this Post
Sync-in Server v2.3.0 exposes POST /api/app/sync/register.
The endpoint accepts credentials and a TOTP code.
It is used to register a desktop sync client.
@AuthTokenSkip() bypasses the bearer-token guard.
The route is reachable without prior session.
SyncClientsManager.register() handles the request.
On failed TOTP, it calls updateAccesses(user, ip, false).
updateAccesses() defaults isAuthTwoFa to false.
If TOTP enabled site-wide and account has 2FA active, freeze branch triggers.
The freeze writes passwordAttempts back unchanged.
The counter never reaches USER_MAX_PASSWORD_ATTEMPTS.
USER_MAX_PASSWORD_ATTEMPTS is 10.
The account lockout gate never fires.
This allows repeated TOTP failures through this endpoint.
A successful TOTP guess registers a sync client.
It returns clientId and clientToken.
The account needs desktop app permission.
The registration payload must be valid.
The token can be exchanged via POST /api/app/sync/auth/cookie.
This yields an authenticated session.
While guessed TOTP code is still valid, attacker can call POST /api/auth/2fa/disable.
Attacker already knows password.
MFA can be removed.
The freeze was designed for web login flow.
Correct password at POST /api/auth/login produces partial session.
Counter should be preserved until POST /api/auth/2fa/login/verify.
That route passes isAuthTwoFa=true and increments on failure.
register() reused updateAccesses() with default false.
Same freeze also applied to logUser().
logUser() is called by POST /api/auth/login and POST /api/auth/token.
Wrong password for 2FA account preserved passwordAttempts.
Remediation adds && success to freeze condition.
That restores lockout after 10 failures.
DailyCVE Form:
Platform: Sync-in Server v2.3.0
Version: v2.3.0
Vulnerability : TOTP lockout bypass
Severity: Not stated
date: Not provided
Prediction: Unknown patch date
What Undercode Say:
Analytics:
python3 poc_totp_bruteforce.py --url http://192.168.16.132:8080 --user mfatest --password 'Str0ngP@ss99!' --concurrency 4 --batch 100
@Post(SYNC_ROUTE.REGISTER)
@AuthTokenSkip()
register(@Body() syncClientRegistrationDto: SyncClientRegistrationDto, @Req() req: FastifyRequest): Promise<SyncClientAuthRegistration> {
return this.syncClientsManager.register(syncClientRegistrationDto, req.ip)
}
this.usersManager.updateAccesses(user, ip, false).catch((e: Error) => this.logger.error({ tag: this.register.name, msg: `${e}` }))
throw new HttpException(authCode.message, HttpStatus.UNAUTHORIZED)
async updateAccesses(user: UserModel, ip: string, success: boolean, isAuthTwoFa = false) {
let passwordAttempts: number
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled) {
passwordAttempts = user.passwordAttempts
} else {
passwordAttempts = success ? 0 : Math.min(user.passwordAttempts + 1, USER_MAX_PASSWORD_ATTEMPTS)
}
await this.usersQueries.updateUserOrGuest(user.id, {
...
passwordAttempts: passwordAttempts,
isActive: user.isActive && passwordAttempts < USER_MAX_PASSWORD_ATTEMPTS
})
}
// Before
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled) {
// After
if (!isAuthTwoFa && configuration.auth.mfa.totp.enabled && user.twoFaEnabled && success) {
Total attempts : 195,907 Time elapsed : 17769.5s (296.2min) Average RPS : 11.0 VALID TOTP CODE FOUND : 026961 clientId : 13951b88-03d4-4854-8a29-cd8921d73d82 clientToken : c92ef5ca-7432-44d0-8a94-56398bfe4117 passwordAttempts : 0 2FA DISABLED. Account 'mfatest' now accessible with password alone.
Exploit: (Educational Purposes!)
Create test account with TOTP MFA and desktop sync permission.
Run poc_totp_bruteforce.py.
Brute-force TOTP via POST /api/app/sync/register.
On valid code, obtain clientId and clientToken.
Exchange via POST /api/app/sync/auth/cookie.
Call POST /api/auth/2fa/disable while TOTP valid.
Protection:
Add && success to freeze condition.
Apply IP and account rate limiter.
Rate limit POST /api/app/sync/register.
Rate limit pre-auth credential endpoints.
Monitor repeated TOTP failures.
Impact:
An attacker with valid credentials for TOTP-enabled account with desktop sync permission can brute-force second factor through POST /api/app/sync/register without lockout.
With drift 1, 3 of 1,000,000 six-digit codes valid per 30-second window (p = 3/1,000,000).
Expected 333,333 attempts to find valid code.
At 3 r/s: 50% = 231,049 attempts, 21.4 h.
At 3 r/s: 90% = 767,528 attempts, 71.1 h.
At 3 r/s: 95% = 998,577 attempts, 92.5 h.
At 3 r/s: 99% = 1,535,056 attempts, 142.1 h.
At 3 r/s: Expected mean = 333,333 attempts, 30.9 h.
Deployments with server.workers > 1 may allow higher throughput.
Throughput depends on password verification cost, worker count, database latency, deployment limits.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

