│ Category: General Skills
│ Difficulty: Easy
│ Points: 100
│ Author: Imattas aka Zemi
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
│ Can you reverse a series of Linux text transformations to recover the original
flag?
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
We are given a file (or a command pipeline) that shows how a flag was
transformed using a series of standard Linux text-processing utilities. Our task
is to reverse each transformation in the correct order to recover the original
picoCTF{...} flag.
This is a classic "General Skills" challenge that tests familiarity with Linux
command-line text-processing tools. The challenge provides a transformed string
and either shows or implies the sequence of transformations that were applied.
To recover the flag, we must apply the inverse of each transformation in reverse
order.
:: Common Linux text transformations and their inverses
Forward Command What It Does Inverse Command
base64 Base64 encode base64 -d
rev Reverse each line charact... rev (self-inverse)
tac Reverse line order (last ... tac (self-inverse)
tr 'a-z' 'A-Z' Lowercase to uppercase tr 'A-Z' 'a-z'
tr 'A-Za-z' 'N-ZA-Mn-za-m' ROT13 tr 'A-Za-z'
'N-ZA-Mn-za-m...
xxd Hex dump xxd -r
xxd -p Plain hex dump xxd -r -p
gzip / zlib Compression gzip -d / zlib-flate
-unc...
sed 's/old/new/g' String substitution sed 's/new/old/g'
tr 'abc' 'xyz' Character transliteration tr 'xyz' 'abc'
cut -c N- Remove first N-1 characters (cannot directly undo
wit...
sort Sort lines alphabetically (cannot directly undo)
fold -w N Wrap lines to N characters tr -d 'n' (rejoin)
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
The challenge is solvable because the forward transformations are all (or
mostly) reversible. Each encoding/cipher applied to the flag has a well-known
inverse, so the entire pipeline can be undone provided we apply the inverses in
the right (reverse) order. The key strategy:
1. Read the challenge instructions carefully. The transformations are usually
given explicitly (e.g., as a shell pipeline or a script).
2. Start from the final output and work backwards, applying the inverse of each
transformation.
3. Verify at each step that the intermediate result looks reasonable (e.g.,
valid base64, readable ASCII).
4. The final result should be a valid picoCTF{...} flag.
When the exact pipeline is unknown, the solver can detect likely encodings
(base64, hex/xxd, case) and brute-force combinations of inverse transformations
until the flag pattern appears.
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
:: Step 1: Examine the provided file/instructions
The challenge typically provides:
- A transformed data file (e.g., transformed.txt)
- The sequence of commands used to transform it (e.g., in a script or
description)
For example, if the challenge says the flag was transformed with:
-- bash --
cat flag.txt | base64 | rev | tr 'a-z' 'A-Z' > transformed.txt
:: Step 2: Reverse the pipeline
Apply the inverse of each command in reverse order:
-- bash --
cat transformed.txt | tr 'A-Z' 'a-z' | rev | base64 -d
Breaking this down:
1. Last transformation was `tr 'a-z' 'A-Z'` (uppercase) -> Undo with tr 'A-Z'
'a-z' (lowercase)
2. Second transformation was `rev` (reverse) -> Undo with rev (self-inverse)
3. First transformation was `base64` (encode) -> Undo with base64 -d (decode)
:: Step 3: Handle multi-step or nested transformations
If the challenge involves multiple rounds or nested encodings, keep peeling
layers:
-- bash --
# Example: double base64 + rev + ROT13
cat data.txt | tr 'A-Za-z' 'N-ZA-Mn-za-m' | rev | base64 -d | base64 -d
:: Step 4: Verify the flag
The output should match the pattern picoCTF{...}.
The accompanying solver reads the transformed data, applies common inverse
transformations, and uses pattern detection to automatically determine and
reverse the correct sequence.
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Undo - picoCTF 2026
Category: General Skills | Points: 100
Automated solver that reverses Linux text transformations to recover the flag.
Usage:
python3 solve.py <transformed_file>
python3 solve.py <transformed_file> --transformations "base64 rev tr_upper rot13"
echo "TRANSFORMED_STRING" | python3 solve.py -
If the transformations are known, pass them with --transformations in the ORDER
THEY WERE APPLIED (the script will reverse them automatically).
If not specified, the script will try to auto-detect and brute-force common
transformation sequences.
Supported transformations:
base64 - base64 encoding
rev - character reversal per line
tac - line order reversal
tr_upper - lowercase -> uppercase
tr_lower - uppercase -> lowercase
rot13 - ROT13 cipher
xxd - hex dump (xxd format)
xxd_plain - plain hex dump
fold_N - fold/wrap at N characters (e.g., fold_80)
"""
import argparse
import base64
import codecs
import itertools
import re
import sys
FLAG_PATTERN = re.compile(r'picoCTF\{[^}]+\}')
def undo_base64(data):
"""Reverse base64 encoding."""
# Strip whitespace that might have been introduced
cleaned = data.strip().replace("\n", "").replace("\r", "").replace(" ", "")
# Add padding if needed
padding = 4 - len(cleaned) % 4
if padding != 4:
cleaned += "=" * padding
try:
decoded = base64.b64decode(cleaned)
return decoded.decode("utf-8", errors="replace")
except Exception:
return None
def undo_rev(data):
"""Reverse character order per line (inverse of `rev`)."""
lines = data.split("\n")
return "\n".join(line[::-1] for line in lines)
def undo_tac(data):
"""Reverse line order (inverse of `tac`)."""
lines = data.split("\n")
# Remove trailing empty line if present
if lines and lines[-1] == "":
lines = lines[:-1]
return "\n".join(reversed(lines))
def undo_tr_upper(data):
"""Reverse tr 'a-z' 'A-Z' (convert uppercase back to lowercase)."""
return data.lower()
def undo_tr_lower(data):
"""Reverse tr 'A-Z' 'a-z' (convert lowercase back to uppercase)."""
return data.upper()
def undo_rot13(data):
"""Reverse ROT13 (self-inverse)."""
return codecs.decode(data, "rot_13")
def undo_xxd(data):
"""Reverse xxd hex dump format."""
result = []
for line in data.strip().split("\n"):
# xxd format: "00000000: 7069 636f 4354 467b picoCTF{"
parts = line.split(":")
if len(parts) >= 2:
hex_part = parts[1].split(" ")[0].strip()
hex_clean = hex_part.replace(" ", "")
try:
result.append(bytes.fromhex(hex_clean).decode("utf-8", errors="replace"))
except ValueError:
continue
return "".join(result) if result else None
def undo_xxd_plain(data):
"""Reverse xxd -p (plain hex dump)."""
cleaned = data.strip().replace("\n", "").replace("\r", "").replace(" ", "")
try:
return bytes.fromhex(cleaned).decode("utf-8", errors="replace")
except ValueError:
return None
def undo_fold(data):
"""Reverse fold (rejoin wrapped lines)."""
return data.replace("\n", "")
# Map of transformation names to their undo functions
UNDO_MAP = {
"base64": undo_base64,
"rev": undo_rev,
"tac": undo_tac,
"tr_upper": undo_tr_upper,
"tr_lower": undo_tr_lower,
"rot13": undo_rot13,
"xxd": undo_xxd,
"xxd_plain": undo_xxd_plain,
"fold": undo_fold,
}
def check_flag(data):
"""Check if data contains a picoCTF flag."""
if data is None:
return None
match = FLAG_PATTERN.search(data)
return match.group(0) if match else None
def detect_encoding(data):
"""Try to detect what encoding/transformation was last applied."""
detections = []
stripped = data.strip()
# Check for xxd format (lines starting with hex offset)
if re.match(r'^[0-9a-f]{8}:', stripped, re.MULTILINE):
detections.append("xxd")
# Check for plain hex (only hex characters and newlines)
hex_clean = stripped.replace("\n", "").replace("\r", "").replace(" ", "")
if re.match(r'^[0-9a-fA-F]+$', hex_clean) and len(hex_clean) % 2 == 0:
detections.append("xxd_plain")
# Check for base64 (valid base64 chars, possibly with = padding)
b64_clean = stripped.replace("\n", "").replace("\r", "")
if re.match(r'^[A-Za-z0-9+/]+=*$', b64_clean) and len(b64_clean) >= 4:
detections.append("base64")
# Check if all uppercase (might be tr_upper)
alpha_only = re.sub(r'[^a-zA-Z]', '', stripped)
if alpha_only and alpha_only == alpha_only.upper():
detections.append("tr_upper")
# Check if all lowercase
if alpha_only and alpha_only == alpha_only.lower():
detections.append("tr_lower")
return detections
def apply_undo_sequence(data, sequence):
"""Apply a sequence of undo operations (in reverse order of application)."""
current = data
for transform in reversed(sequence):
# Handle fold_N pattern
if transform.startswith("fold_"):
undo_func = undo_fold
elif transform in UNDO_MAP:
undo_func = UNDO_MAP[transform]
else:
return None
result = undo_func(current)
if result is None:
return None
current = result
return current
def brute_force_transformations(data, max_depth=4):
"""Try all combinations of transformations up to max_depth."""
transforms = ["base64", "rev", "tac", "tr_upper", "rot13", "xxd_plain", "fold"]
# First check the raw data
flag = check_flag(data)
if flag:
return flag, []
# Start with most likely single transforms, then increase depth
for depth in range(1, max_depth + 1):
print(f"[*] Trying depth {depth} ({len(transforms)**depth} combinations)...")
for combo in itertools.product(transforms, repeat=depth):
result = apply_undo_sequence(data, list(combo))
if result is not None:
flag = check_flag(result)
if flag:
return flag, list(combo)
return None, []
def smart_peel(data, max_depth=6):
"""Intelligently peel transformations one at a time using detection."""
current = data
applied = []
for depth in range(max_depth):
# Check if we already have the flag
flag = check_flag(current)
if flag:
return flag, applied
# Detect likely encodings
detections = detect_encoding(current)
# Also always try rev and rot13 since they're hard to detect
to_try = list(dict.fromkeys(detections + ["rev", "rot13", "tac", "fold"]))
found_progress = False
for transform in to_try:
undo_func = UNDO_MAP.get(transform)
if not undo_func:
continue
result = undo_func(current)
if result is None:
continue
# Check if this transformation produced a flag
flag = check_flag(result)
if flag:
applied.append(transform)
return flag, applied
# Check if the result looks "more decoded" (contains more printable chars
# or is closer to the flag pattern)
if "pico" in result.lower() or "ctf{" in result.lower():
current = result
applied.append(transform)
found_progress = True
print(f" [~] Applied undo_{transform}: partial flag visible")
break
if not found_progress:
# Try each and see if we get closer (heuristic)
for transform in to_try:
undo_func = UNDO_MAP.get(transform)
if not undo_func:
continue
result = undo_func(current)
if result and result != current:
# Recurse with this option
sub_flag = check_flag(result)
if sub_flag:
applied.append(transform)
return sub_flag, applied
break # No progress possible
return None, applied
def main():
parser = argparse.ArgumentParser(
description="Undo solver - picoCTF 2026"
)
parser.add_argument(
"input",
help="Path to the transformed file, or '-' for stdin"
)
parser.add_argument(
"--transformations", "-t",
help="Space-separated list of transformations in order applied "
"(e.g., 'base64 rev tr_upper rot13')",
default=None
)
parser.add_argument(
"--brute-force", "-b",
action="store_true",
help="Brute-force all transformation combinations (slow but thorough)"
)
parser.add_argument(
"--max-depth", "-d",
type=int, default=4,
help="Maximum transformation depth for brute-force (default: 4)"
)
args = parser.parse_args()
# Read input
if args.input == "-":
data = sys.stdin.read()
else:
try:
with open(args.input, "r", errors="ignore") as f:
data = f.read()
except FileNotFoundError:
print(f"[!] File not found: {args.input}")
sys.exit(1)
print(f"[*] Undo Solver - picoCTF 2026")
print(f"[*] Input length: {len(data)} characters")
print(f"[*] First 100 chars: {data[:100]!r}\n")
# Check if flag is already visible in raw data
flag = check_flag(data)
if flag:
print(f"[+] Flag found directly in input: {flag}")
return
# Method 1: If transformations are specified, apply them in reverse
if args.transformations:
transforms = args.transformations.strip().split()
print(f"[*] Applying inverse of: {' | '.join(transforms)}")
result = apply_undo_sequence(data, transforms)
if result:
flag = check_flag(result)
if flag:
print(f"\n{'='*60}")
print(f"[+] FLAG FOUND: {flag}")
print(f"{'='*60}")
return
else:
print(f"[-] Result: {result[:200]!r}")
print(f"[-] No flag pattern found in result")
else:
print(f"[-] Transformation sequence failed")
return
# Method 2: Smart detection-based peeling
print(f"[*] Attempting smart detection-based decoding...")
detections = detect_encoding(data)
print(f" Detected encodings: {detections if detections else 'none obvious'}\n")
flag, applied = smart_peel(data)
if flag:
print(f"\n{'='*60}")
print(f"[+] FLAG FOUND: {flag}")
print(f"[+] Transformations undone: {' -> '.join(applied)}")
print(f"{'='*60}")
return
# Method 3: Brute-force if requested or as fallback
if args.brute_force or not applied:
print(f"\n[*] Trying brute-force approach (max depth: {args.max_depth})...")
flag, combo = brute_force_transformations(data, args.max_depth)
if flag:
print(f"\n{'='*60}")
print(f"[+] FLAG FOUND: {flag}")
print(f"[+] Original transformation sequence: {' | '.join(combo)}")
print(f"[+] Undo sequence: {' | '.join(reversed(combo))}")
print(f"{'='*60}")
return
# If nothing worked
print(f"\n[-] Could not automatically recover the flag.")
print(f"[*] Suggestions:")
print(f" 1. Specify the transformations with --transformations")
print(f" Example: python3 solve.py data.txt -t 'base64 rev tr_upper'")
print(f" 2. Try with --brute-force --max-depth 5 for deeper search")
print(f" 3. Manually examine the data and identify transformations")
print(f" 4. Common pipelines to try manually:")
print(f" cat data.txt | rev | base64 -d")
print(f" cat data.txt | tr 'A-Z' 'a-z' | rev | base64 -d")
print(f" cat data.txt | tr 'A-Za-z' 'N-ZA-Mn-za-m' | base64 -d")
print(f" cat data.txt | xxd -r -p")
print(f" cat data.txt | base64 -d | rev")
print(f" cat data.txt | base64 -d | base64 -d")
if __name__ == "__main__":
main()
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- To undo a transformation pipeline, apply each operation's inverse in reverse
order.
- Many Linux text tools are self-inverse (rev, tac, rot13, nibble-style
transforms), which simplifies reversal.
- Recognizing encodings by shape — base64 charset, xxd offset prefixes,
all-upper/all-lower casing — lets you auto-detect the last applied layer.
- When the pipeline is unknown, a bounded brute-force over common inverse
combinations, checking for the picoCTF{...} pattern, reliably recovers the flag.
- cut/sort are lossy and cannot be undone, so a solvable challenge avoids them.
- Verify intermediate results look reasonable (valid base64/readable ASCII) at
each peel step.