We’re given a shell one-liner that operates on some hidden input (marked as [REDACTED]) and a list of MD5 hashes as output. The task is to recover the original input / flag.
echo "[REDACTED]" | fold -w 3 | xargs -L1 -I {} sh -c 'echo "{}" | md5sum' | awk '{print $1}'
And the provided output:
edd94d6e72b217ef22de208d5246300c
73a338a5cb6f23fb56cfd9320ea18414
3e3e0a54a3a0bced0522f414db8df3aa
665dc131f2c2104e7cba175cdb0f37d7
6bc898592761e343f5284ee0e1a641be
cf358237d3a6cbea098de5fb5b733f24
c670e2c9053c8e9e600c8bc01ddd16ae
010b08f101ec8929efff98e5e6418b7c
e0bc06e650fd4a19f67951e6cf0b3615
83d06450b3ad8b05ba802f0c117e706f
32c24e443f8997e7eb14a212dccb707d
54021079e528a133037e732a36774509
Goal: recover [REDACTED] and thus the flag.
Break the command down stage by stage:
echo "[REDACTED]" | fold -w 3 | xargs -L1 -I {} sh -c 'echo "{}" | md5sum' | awk '{print $1}'
echo "[REDACTED]"
Prints the original secret string followed by a newline (\n).
fold -w 3
Splits the input into fixed-width chunks of 3 characters per line.
For example, if the input was:
ABCDEFGHI
then fold -w 3 would output:
ABC
DEF
GHI
xargs -L1 -I {} sh -c 'echo "{}" | md5sum'
xargs -L1 takes one line at a time from stdin.sh -c 'echo "{}" | md5sum'
echo appends a newline. So the data being hashed is actually:<3 characters> + "\n"awk '{print $1}'md5sum prints the hash plus - (or filename). awk '{print $1}' keeps only the hash itself.So the pipeline can be summarized as:
Split the secret into 3‑character chunks and output the MD5 hash of each chunk (including a trailing newline).
We are given 12 hashes, which correspond to 12 chunks of 3 characters each.
That means the original secret (not counting the newline that echo added to the whole string) is:
12 chunks × 3 characters = 36 characters
In CTFs, flags are often of the form:
MCTF25{...}
So it’s reasonable to expect the secret is a flag with 36 characters total.
Now that we know each hash is:
MD5( three_characters + "\n" )
we can brute force each chunk independently.
A–Za–z0–9{ } _s, compute md5(s + "\n").Because each chunk is independent, this is a 12 × (search space) problem, but the search space for “reasonable flag” characters is small enough for a quick brute force.
import hashlib
import string
hashes = [
"edd94d6e72b217ef22de208d5246300c",
"73a338a5cb6f23fb56cfd9320ea18414",
"3e3e0a54a3a0bced0522f414db8df3aa",
"665dc131f2c2104e7cba175cdb0f37d7",
"6bc898592761e343f5284ee0e1a641be",
"cf358237d3a6cbea098de5fb5b733f24",
"c670e2c9053c8e9e600c8bc01ddd16ae",
"010b08f101ec8929efff98e5e6418b7c",
"e0bc06e650fd4a19f67951e6cf0b3615",
"83d06450b3ad8b05ba802f0c117e706f",
"32c24e443f8997e7eb14a212dccb707d",
"54021079e528a133037e732a36774509",
]
charset = string.ascii_letters + string.digits + "{}_"
def md5_with_newline(s: str) -> str:
return hashlib.md5((s + "\n").encode()).hexdigest()
chunks = []
for h in hashes:
found = None
for a in charset:
for b in charset:
for c in charset:
s = a + b + c
if md5_with_newline(s) == h:
found = s
print(f"{h} -> {s!r}")
break
if found:
break
if found:
break
if not found:
raise ValueError(f"No match found for hash {h}")
chunks.append(found)
secret = "".join(chunks)
print("Recovered secret:", secret)
Running this script yields the following chunks (in order):
MCT
F25
{p1
p1n
g_h
0t_
t45
k_y
3t_
n0_
p1p
15}
Concatenating them gives the original 36‑character secret.
Putting the chunks together:
MCTF25{p1p1ng_h0t_t45k_y3t_n0_p1p15}