Step 103. Natas 6~10 — Reading Server Code and Command Injection

Step 103. Natas 6~10 — Reading Server Code and Command Injection

Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★☆☆ | Estimated time: 3 hours

Prerequisites: you’ll use the web reconnaissance routine from Step 102, the Python from Steps 41~46, and the server knowledge from Step 94.

  • What you need: a browser and developer tools, Python interactive mode (for reversing encodings), and the reconnaissance routine from Step 102.
  • ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
  • Note: Natas is a legal learning platform officially operated by OverTheWire. In particular, never type the command-injection inputs you learn today into any search box outside the practice ground — from that moment it’s no longer an experiment but an attack.

Through Step 102 we looked behind documents. From now on we look behind the logic. Starting at Natas 6, pages come with a "view sourcecode" button — reading the server’s PHP code is the solution itself. The server receives a request, runs code, and returns a result (you built one yourself in Step 94). If you can read that code, you can arrive at the answer not by "guessing" but by "reasoning." It’s fine if you’ve never learned PHP — all reading requires is code sense, and you already know Python and C.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Find and read include paths and comparison logic in PHP source
  • Explain the principle of opening unintended files with ../ path traversal
  • Reverse-compute a server’s transformation (encoding) logic in Python to restore the answer
  • Succeed at command injection and explain its two required conditions
  • Explain the limits of denylist (blacklist) defense and the true nature of hidden inputs

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment PHP is "read-only" (no installation needed); experiments use Python + a Linux shell (WSL)
Today’s commands grep -i pattern file, the shell’s # (comment), Python base64, bytes.fromhex, [::-1]
Concepts needed minimal PHP syntax ($variable, include, $_POST, system()), path traversal (../), encoding reversal, command injection, the limits of blacklists
Today’s artifact decode_lab.py — an encoding reverser; an injection experiment record (one pair: "my input / final command")

2-1. Minimal PHP Syntax for Reading

PHP is a language long beloved on web servers. For reading, four things are enough.

$secret = "abcd";                    // variables start with $
include "includes/secret.inc";       // splice another file in right here
if ($_POST["key"] == $secret) {...}  // compare with the value that came via POST
system("grep $word dictionary.txt"); // execute an OS command — danger sign!

$_GET and $_POST are inputs that arrived with the request, include splices in a file, and system()/passthru() are functions that execute commands on the server. When you see the last kind, an attacker’s eyes light up — if input flows in there, it becomes a command.

2-2. Path Traversal — The Ladder of ../

A form like ../../etc/passwd is called path traversal. .. means "one folder up." When a server splices in a file like include "pages/" + input, stacking ../../../../ as the input climbs back up the server’s file structure and reaches files outside the place it meant to show you. Since you can’t climb above the root (topmost level), writing plenty of ../ does no harm.

2-3. Command Injection — The Moment a Search Box Becomes a Terminal

Suppose the server code looks like this.

system("grep " + input + " dictionary.txt")

If the input is hello, you get grep hello dictionary.txt — a normal search. But what if the input is hello /etc/passwd #? The command becomes grep hello /etc/passwd # dictionary.txt, rummaging through a completely different file. The input became not data but part of the command. Exactly the same root as SQL injection — the mixing of code and data.

PHP’s system() hands the string wholesale to a shell (/bin/sh -c). So a space becomes an argument separator, # becomes "everything after this is a comment," and ; becomes "one command ends, the next begins." This shell syntax is our weapon.

2-4. Reversing Encodings — Transformations Can Be Undone

If the server code has logic saying "transform the input this way and compare," running that transformation in reverse yields the answer. A transformation is not encryption (Step 50’s Base64 lesson). Just rewind one step at a time in Python interactive mode.


3. Follow Along

3-1. Natas 6 → 7: Following the include Path

This page has a "View sourcecode" link. Click it and read the PHP.

How to read it: the code has a line like this — include "includes/secret.inc";. A declaration that the secret lives in this file.

Input: type that path directly into the address bar.

http://natas6.natas.labs.overthewire.org/includes/secret.inc

Screen example: the contents of the file holding the password.

Why: an include’s target is just a file on the server, and without access restrictions anyone can open it directly. Read the code, then go where the code points — today’s basic move.

3-2. Natas 7 → 8: The Path-Traversal Ladder

Reading the hint (a comment in the source) tells you the password is at /etc/natas_webpass/natas8. Notice that the page’s links take the form index.php?page=home.

Input: turn the page parameter into a ladder.

http://natas7.natas.labs.overthewire.org/index.php?page=../../../../etc/natas_webpass/natas8

Screen example: the next password is displayed on the page.

How to read it: the server expected "a file inside the pages folder," but the ../ ladder carried it outside the server’s intended file structure.

Why: when you see a parameter that splices in a file, think ladder — that’s the path-traversal conditioned reflex.

3-3. Natas 8 → 9: Reversing the Encoding Logic (Local Measurement)

Looking at the source, the server’s verification is built like this.

$encodedSecret = "...(hex string)...";
function encodeSecret($secret) {
    return bin2hex(strrev(base64_encode($secret)));
}
if ($_POST["secret"] == $encodedSecret) { ... }

How to read it: it compares the input after ① base64 encoding → ② reversing (strrev) → ③ hex conversion (bin2hex). But the expected value ($encodedSecret) is written right there in the source!

This logic reproduces completely in Python, no PHP needed. Let’s play both the server role and the attacker role (decode_lab.py):

"""Natas 8 style: reversing the encoding logic."""
import base64

# Same as the server-side logic (PHP): base64_encode -> strrev -> bin2hex
def encode_secret(secret: str) -> str:
    step1 = base64.b64encode(secret.encode())   # ① base64
    step2 = step1[::-1]                          # ② reverse
    step3 = step2.hex()                          # ③ hex
    return step3

# 1) As the 'server', we encode the answer
answer = "natas8_style_secret_abc123"
encoded = encode_secret(answer)
print("The $encodedSecret written in the server source:", encoded)

# 2) Attacker: rewind the stolen string in reverse order
step1 = bytes.fromhex(encoded)   # inverse of ③: hex -> bytes
step2 = step1[::-1]              # inverse of ②: reverse again
step3 = base64.b64decode(step2)  # inverse of ①: decode base64
print("Reversal result:", step3.decode())

Output (measured 2026-09-09):

The $encodedSecret written in the server source: 3d4d6a4d784d6d59683946646c4a33596c4e33586c785765304e3358344d585930466d62
Reversal result: natas8_style_secret_abc123

How to read it: the reversal of a transformation A→B→C is always C→B→A, in reverse order. On the actual level, copy the hex string from the source and drop it into the same three lines — done.

Why: transformations rewind without a key. "Obfuscation" only slows you down; it can’t stop you — because the answer is in the source.

3-4. Natas 9 → 10: First Command Injection Success

This page has a form that "searches for words," and the source carries that danger sign — the input flows into a grep command.

Input: into the form’s text box:

.* /etc/natas_webpass/natas10 #

Screen example: the target file’s contents — the next password printed like a list.

How to read it: the final command the server built is grep -i .* /etc/natas_webpass/natas10 # dictionary.txt. .* is a regex matching "every line," and # comments out the rest, nullifying the original search target (dictionary.txt). The search box became a terminal.

Why: you just executed a command the server never intended on the server. This is command injection, a regular on the throne of web vulnerabilities.

3-5. Reproduce It in Your Lab — The grep Injection Experiment (Local Measurement)

This attack’s principle reproduces with nothing but a Linux shell. Build a mini environment in WSL or a Linux terminal.

Input:

mkdir -p /tmp/natas_lab && cd /tmp/natas_lab
printf 'applenbananancatn' > dictionary.txt
printf 'natas10_fake_password_XYZ987n' > webpass.txt

First, a normal search (measured 2026-09-09):

$ grep -i apple dictionary.txt
apple

Now, just as the server’s system() does, let’s hand the assembled string wholesale to a shell. (sh -c "string" is how PHP’s system() behaves.)

$ key=".* /tmp/natas_lab/webpass.txt #"
$ cmd="grep -i $key dictionary.txt"
$ echo "$cmd"
grep -i .* /tmp/natas_lab/webpass.txt # dictionary.txt
$ sh -c "$cmd"
grep: ..: Is a directory
/tmp/natas_lab/webpass.txt:natas10_fake_password_XYZ987

(Measured 2026-09-09, Ubuntu 24.04 WSL.)

How to read it: with # treated as a comment, the trailing dictionary.txt vanished from the results, and the contents of the inserted webpass.txt were printed. The error line up front (..: Is a directory) happened because the shell also interpreted .* as a filename pattern, adding the . and .. folders as arguments — even with errors mixed in, the secret line prints exactly. On a real server, depending on the working directory, this error may not appear.

Why: seeing with your own eyes "the final command containing my input" is half of understanding injection. In your write-up, don’t record just the input — record this pair: my input / final command.

3-6. Natas 10 → 11: Slipping Through the Filter

Same problem, but this time there’s a filter — it blocks characters like ;, |, &.

Input: try the same input as in 3-4.

Screen example: it passes as-is — password acquired.

How to read it: our input contains no ;|&. It’s built only of .*, spaces, and #. A filter blocks only the characters the developer imagined an attacker would use.

Why: the structural limit of "denylist" (blacklist) defense. A defense that enumerates what to block is always open to attacks outside the enumeration. The longer the filter list you see, the more attack candidates you should think you have — the answer is what’s not on the list.

3-7. hidden input — Merely Invisible, Still Sent

Other Natas problems (and countless real sites) have forms like this.

<input type="hidden" name="admin" value="0">

Is this "hidden" value safe? hidden only means "don’t draw it on screen" — it’s still carried in the request. It’s a browser rendering rule, not a transmission rule. Find this tag in developer tools Elements, change the value to 1, and submit the form — or use curl with -d "admin=1" — and the server receives the altered value. Step 102’s First Principle holds exactly here too — a value entrusted to the client belongs to the client.

3-8. Follow Along Recap — Today’s Five Techniques

Before tidying up the passwords you acquired, fix the techniques themselves in place with names.

  1. include tracing — open the file the code splices in, directly (Natas 6)
  2. Path traversal — load the ../ ladder onto a file parameter (Natas 7)
  3. Encoding reversal — rewind the transformation logic in reverse order (Natas 8)
  4. Command injection — promote input into command syntax (Natas 9)
  5. Filter bypass — achieve the same goal with a structure not on the denylist (Natas 10)

These five aren’t independent techniques — they’re five expressions of a single habit: "read the server code first." A finger that looks for the source button the moment a page opens — that’s the first reflex of someone who’s been through this stretch.


4. Missions & Exercises

Mission — A "Distrust the Client" Casebook and Injection Reproduction

  1. Clear all of natas6~10, and excerpt from each level’s server code "the one line that became the hole" into your write-up
  2. Run decode_lab.py from 3-3 yourself, confirm that encoding and reversal mesh, and keep it in your wiki in a reusable form
  3. Reproduce the grep injection experiment from 3-5 and write the "my input / final command" pair in your write-up
  4. Write distrust-the-client.md in your wiki — organize 4 cases (header manipulation, cookie tampering, hidden input, JS validation bypass) in a table of "what the server trusted / what the attacker did"

Exercises

Exercise 1. State the two pieces of information the single line include "includes/secret.inc"; gives an attacker.

Exercise 2. Explain why it’s fine to write ../ "generously, many times" in path traversal.

Exercise 3. When the transformation order is base64 → reverse → hex, state the reversal’s order and the Python function for each step.

Exercise 4. State the two conditions for command injection to work (input side / server side), and explain what # does in .* /etc/natas_webpass/natas10 #.


5. Model Answers & Completion Criteria

Mission Model Answer

Per-level "the one line that became the hole" (server solutions based on the Screen examples):

natas6:  include "includes/secret.inc";        → open that path directly
natas7:  the page parameter is used as a file path → ?page=../../../../etc/natas_webpass/natas8
natas8:  $encodedSecret is exposed in the source   → restore the original by reversal (3-3)
natas9:  input combined into system/passthru       → .* /etc/natas_webpass/natas10 #
natas10: only ; | & filtered                       → the same input passes as-is

The model write-up format for injection (based on the 3-5 measurement):

My input:      .* /tmp/natas_lab/webpass.txt #
Final command: grep -i .* /tmp/natas_lab/webpass.txt # dictionary.txt
Result:        the target file's secret line printed (dictionary.txt ignored, being after #)

How to verify: ① does decode_lab.py‘s "Reversal result" match the original string? ② in the local grep experiment, are dictionary.txt’s lines (apple, etc.) absent from the results while only webpass.txt’s line appears — if so, the # comment worked. ③ are all four rows of the casebook table filled? All "yes" means complete.

Exercise Answers

Answer 1. ① the fact that the secret is inside this file, and ② that file’s path on the server. Without access restrictions, you can type that path into the address bar and open it directly. One line of code is itself a map.

Answer 2. Because .. means "one level up," and at the root (/), climbing further up just keeps you at the root. So when you don’t know the depth, writing 6~8 generous ../ has no side effects.

Answer 3. The reversal must be in C→B→A reverse order: inverse of ③ bytes.fromhex() (hex→bytes) → inverse of ② [::-1] (reverse again) → inverse of ① base64.b64decode(). Get the order wrong and you get broken bytes or an error like binascii.Error: Incorrect padding (in the 3-3 experiment, getting the order wrong produced exactly this error — measured 2026-09-09).

Answer 4. The conditions are ① the input is combined as-is into a command string, and ② the server executes that string through a shell — both are required. # means "everything after this is a comment" in the shell, so it nullifies the rest of the original command (dictionary.txt), leaving only the file we specified as the search target. In the 3-5 measurement, dictionary.txt’s lines disappearing from the results is the evidence.

Completion Criteria Checklist

  • [ ] I can find and read include paths and comparison logic in PHP source
  • [ ] I can explain the principle of ../ path traversal and the reason for "writing it generously"
  • [ ] I restored the answer by reverse-computing the encoding logic in Python
  • [ ] I understand command injection on both the server (example) and local (measured) sides
  • [ ] I’ve built the habit of writing the "my input / final command" pair in write-ups
  • [ ] I can explain the limits of filters (denylists) with the Natas 10 case
  • [ ] I can demonstrate why a hidden input belongs to the client

6. Common Pitfalls & Fixes

Wall 1. The ../ ladder doesn’t work

Symptom: an error or blank page on the path-traversal input (on the server, Screen example).
Cause: not enough of them (couldn’t climb the depth), the server filters ../, or the final filename is wrong.
Fix: write ../ generously (6~8) — you can’t climb above the root, so more is safe. If it’s filtered, experiment with variants like ....//.

Wall 2. The encoding reversal comes out garbled

Symptom (measured 2026-09-09, when the order was wrong):

binascii.Error: Incorrect padding

Cause: the reverse order is wrong. The reversal of a transformation A→B→C must be C→B→A.
Fix: in Python interactive mode, print intermediate results one step at a time and verify. Watching at which step it starts reading will reveal the order error.

Wall 3. My injection input just gets searched

Symptom: the command-injection characters end with no result.
Cause: quotes or spaces misaligned with the server’s command assembly. You must match the exact form of the server code (quote positions, options).
Fix: read the system/passthru line in the source character by character, and write "the final command containing my input" on paper. Imagining the whole command first is half of injection.

Wall 4. A strange error line is mixed in next to .*

Symptom (measured 2026-09-09):

grep: ..: Is a directory

Cause: the shell interpreted .* first as a filename pattern (glob) rather than a regex, adding the current folder’s . and .. as arguments too.
Fix: often nothing to fix — even with the error line mixed in, the target file’s contents print normally (in the 3-5 measurement too, the secret appeared below the error line). Just read the lines carrying the filename: prefix in the results.

Wall 5. Every bypass character is blocked

Symptom: ;, |, & are all refused.
Cause: a filter blocks only characters. Non-character structures (the regex .*, the comment #) may not be blocked.
Fix: design "an input that achieves the goal without connector characters." 3-6’s answer is exactly that — our input contained not a single blocked character.


7. Summary

Today’s Concepts

Concept One-line explanation
include PHP’s syntax for splicing in another file — that path is itself a map
Path traversal an attack reaching unintended files with the ../ ladder
Encoding reversal rewinding a transformation A→B→C as C→B→A — obfuscation is not defense
Command injection input promoted into command syntax — system() + input combination is the condition
# (shell comment) "ignore the rest" — a weapon that severs the original command’s tail
Blacklist a defense enumerating characters to block — always open to attacks outside the enumeration
hidden input merely not drawn on screen, still carried in the request — a client value

Today’s Commands and Code

Command/code What it does
grep -i pattern file case-insensitive search — the injection stage
.* (regex) "match every line" — turns a search into a full dump
sh -c "command string" execute a string through a shell — how PHP system() behaves
bytes.fromhex() undo ③ hex (reversal step one)
[::-1] undo ② reverse (reversal step two)
base64.b64decode() undo ① base64 (reversal step three)

An Instinct More Important Than Commands

An attacker who reads code doesn’t guess. They read out the holes in the logic. When a page opens, press the source button first and look for three kinds of lines — include, comparisons, system(): where files get spliced in, where answers get compared, where commands get executed. Those three spots are the weak parts of web server code.

On real targets that don’t show you source, this training works exactly the same — instead of code, you read reactions. The way output changes when you change input is itself the outline of the code, and the paths and function names leaking out in error messages become the map. That’s why turning off detailed errors on production servers is defense common sense. The vulnerabilities we met today — injection, path traversal, broken authentication — are regulars on the OWASP Top 10 (the international web security community’s list of dangerous vulnerabilities). What you learn in a wargame isn’t a variant; it’s active duty. And you’ve already learned the defense — never put input into command-executing functions, enumerate only what to allow rather than what to filter, and keep paths away from user input. After seeing the attacks, these sentences are no longer abstractions.


Once every box is checked, Step 103 is complete.