Computer & OS Fundamentals · beginner · ~10 min
## What you will learn - Explain what a **shell** is and trace the exact steps it takes to turn a typed command into a running process. - Define an **environment variable** and describe how a child process inherits the environment from its parent. - Read, set, export, and unset environment variables in a POSIX shell (bash/zsh), and explain the difference between a *shell variable* and an *environment variable*. - Describe how **PATH** controls command lookup, and explain why a writable directory early in PATH is a real privilege-escalation risk (PATH hijacking). - Identify other security-sensitive variables (`LD_PRELOAD`, `LD_LIBRARY_PATH`) and explain why secrets stored in the environment can leak (for example via `/proc/<pid>/environ`).
Almost everything you do on a computer eventually runs as a program: a process that the operating system starts, schedules, and tears down. In the lesson What an operating system does you saw that the OS is the layer that manages processes, memory, files, and hardware. The command line is the most direct way for a human to ask the OS to start those processes and to control how they behave.
The program you type into is the shell. A shell reads a line of text, figures out which program you meant, starts it as a new process, hands it any arguments, connects its input and output, and waits for it to finish. Graphical apps do the same thing under the hood when you double-click an icon — the shell just makes the mechanics visible and scriptable.
Every process also carries a small dictionary of configuration called its environment: a list of KEY=value strings. The environment is how a parent process passes settings down to the children it starts, without putting everything on the command line. Your language (LANG), your home directory (HOME), and — most importantly — the list of directories searched for commands (PATH) all live there.
The environment matters beyond convenience. Because PATH decides which file runs when you type a command, and because loader variables like LD_PRELOAD can decide which library code loads into a program, the environment is a place where small misconfigurations turn into security problems. We will introduce each idea in plain language first, then build up the terminology you will see in documentation and in security write-ups.
LD_PRELOAD injection are textbook local privilege-escalation techniques. Understanding why they work is the first step to configuring systems that resist them./proc/<pid>/environ. Knowing the leakage paths is what lets you avoid them.Each major idea below is taught on its own: what it is, how it works, when it helps, when it hurts, and a pitfall to avoid.
Definition. A shell is a program that reads commands as text and runs them. Common shells are bash and zsh on Linux/macOS and PowerShell on Windows.
Plain language. Think of the shell as a translator and dispatcher. You type ls -l; the shell breaks that into a program name (ls) and an argument (-l), locates the program, starts it, and shows you what it prints.
How it works internally. For a typical command the shell:
$HOME and *.txt into their real values.PATH if the name has no slash).fork then exec).You type: grep error log.txt
|
+------v------------------------+
| shell: split into words | -> ["grep", "error", "log.txt"]
| expand variables/globs |
| find "grep" via PATH | -> /usr/bin/grep
| fork() a child process |
| exec("/usr/bin/grep", args) |
| wait for it to finish |
+-------------------------------+
|
child process runs grep, prints matches, exits with a status code
When to use / not to use. Use the shell to launch, combine, and automate programs. Do not use shell string-building to assemble untrusted input into commands (that is the road to command injection — a topic for later security lessons).
Pitfall. Forgetting that the shell expands $VAR and * before the program ever sees the arguments. rm $FILES behaves very differently from rm "$FILES" when FILES contains spaces.
Knowledge check: In your own words, what are the five things the shell does between you pressing Enter and the program starting?
Definition. The environment is a set of KEY=value strings attached to a process. Each process gets its own copy.
Plain language. It is a small settings dictionary that travels with a program. Instead of passing a dozen flags on every command, programs read defaults from the environment.
How it works internally — inheritance. When a process starts a child, the child receives a copy of the parent's environment. Changes the child makes affect only the child's copy, never the parent's. This one-way, copy-on-start rule explains most confusing behaviour.
login shell env: HOME=/home/sam PATH=... EDITOR=vim
| starts (fork+exec)
v
your bash session env: COPY of above (+ anything you export)
| starts
v
program you run env: COPY of bash's env
Shell variable vs. environment variable. Setting X=5 creates a shell variable that only the current shell sees. Running export X (or export X=5) promotes it into the environment so that child processes inherit it. This distinction trips up almost every beginner.
When to use / not to use. Use the environment for configuration that several programs or scripts share (locale, paths, non-secret config). Avoid relying on it for secrets in shared or long-lived processes, because the value is visible to children and to anything that can read the process.
Pitfall. Setting a variable, then starting a program from a different shell or a GUI launcher, and wondering why the program didn't see it — the new process inherited a different environment.
Knowledge check (predict the output): In bash you run NAME=Ada then bash -c 'echo $NAME'. What prints, and why? (Hint: was NAME exported?)
Definition. PATH is a colon-separated list of directories the shell searches, in order, to find a command whose name contains no slash.
Plain language. When you type python, the shell does not magically know where Python lives. It walks each directory in PATH from left to right and runs the first matching executable it finds.
PATH = /usr/local/bin : /usr/bin : /bin
(1) (2) (3)
Type: python
look in /usr/local/bin -> found! run /usr/local/bin/python (stops here)
/usr/bin and /bin are never checked
How it works internally. The shell performs the lookup once per command (often using a cache). Order matters: the first match wins, so a directory earlier in the list shadows the same-named program later in the list.
Why it is security-sensitive — PATH hijacking. Suppose a privileged script runs ls (no slash) and PATH begins with a directory that a normal user can write to. An attacker drops a file named ls there; now the privileged script runs the attacker's ls instead of the real /bin/ls. The quiz for this lesson is exactly this idea. Defences: keep writable directories out of PATH (especially ., the current directory), put trusted system directories first, and in scripts call critical tools by absolute path (/bin/ls).
When to use / not to use. Add a directory to PATH so you can run your own tools by name. Never add a world-writable or current directory (.) to PATH, and especially never at the front.
Pitfall. Appending vs. prepending. PATH=$PATH:/my/tools searches your directory last (safer); PATH=/my/tools:$PATH searches it first (can shadow system commands — convenient but riskier).
Knowledge check (find the risk): A teammate adds export PATH=".:$PATH" to a shared server's profile "so scripts in the current folder just run." Why is that dangerous?
Definition. On Linux these variables tell the dynamic linker (the loader) where to find shared libraries and which libraries to load before all others.
Plain language. Where PATH decides which program runs, these decide which library code gets pulled into a program. LD_PRELOAD forces a chosen library to load first, so its functions can override the normal ones.
Why it is a concern. If an attacker can set LD_PRELOAD for a privileged program, they can substitute their own version of a common function and run their code inside that program. This is why secure programs ignore these variables when running with elevated privileges (set-user-ID binaries). Treat them as an injection surface, not as everyday config.
When to use / not to use. Developers legitimately use LD_PRELOAD for debugging, profiling, and shims in their own processes. Do not set it globally, and never trust it inside privileged code.
Pitfall. Exporting LD_LIBRARY_PATH system-wide to "fix" a missing library — it changes library resolution for every program and can break or weaken many of them.
Definition. On Linux, /proc/<pid>/environ is a virtual file exposing a running process's environment.
Plain language. A process's environment is not a secret store. Its own user (and root) can read these variables back out long after the program started.
Why it matters. A credential such as a database password placed in an environment variable can be recovered from /proc, from child processes that inherited it, and sometimes from crash dumps or logs. Prefer secret files with strict permissions or a dedicated secrets manager, and scrub secrets from any logging.
Knowledge check: Two reasons a long-lived secret is safer in a permission-restricted file than in an exported environment variable.
# Read a variable (the $ expands it)
echo "$HOME" # quote to keep spaces intact
# Show the whole environment
printenv # or: env
# Shell variable: only THIS shell sees it
GREETING=hello
# Environment variable: children inherit it too
export GREETING # promote existing shell var
export EDITOR=vim # create + export in one step
# Set a variable for ONE command only (no lasting change)
LANG=C sort names.txt # 'sort' sees LANG=C; your shell does not change
# Safely extend PATH (append => searched LAST => safer)
export PATH="$PATH:$HOME/bin"
# Remove a variable
unset GREETING
Key points the syntax encodes: $NAME reads, plain NAME=value writes a shell variable, export moves it into the environment so children inherit it, and NAME=value command sets it for just that one command's process.
The command line (the shell) is how operators drive a system precisely. It is also where most penetration-testing tooling lives.
A shell — such as bash, zsh, or PowerShell — reads commands, runs programs, and connects their input and output.
A command has the form program arg1 arg2. The shell finds the program, starts a process for it, and waits for it to finish.
Every process inherits a set of environment variables: key/value strings such as HOME=/root or LANG=en_US.UTF-8.
They configure a program's behaviour without needing command-line flags.
PATH is a colon-separated list of directories that the shell searches to find a command.
When you type nmap, the shell looks through each PATH directory in order until it finds a match.
PATH is security-sensitive. If a writable or attacker-controlled directory comes early in PATH, a malicious ls placed there will run instead of the real one. This is a genuine privilege-escalation technique, known as PATH hijacking.
HOME, USER, PWD — basic session information.LD_PRELOAD / LD_LIBRARY_PATH — loader controls, and another injection vector.AWS_SECRET_ACCESS_KEY sitting in a process's environment is a credential leak, recoverable via /proc/<pid>/environ.Below is a complete, self-contained shell session. It is a normal, safe lab demonstration you can run on your own machine (no root required) to see inheritance and PATH lookup with your own eyes.
#!/usr/bin/env bash
# demo.sh — observe shell variables vs environment variables, and PATH lookup.
set -u # treat use of an unset variable as an error (catches typos)
echo "== 1. shell variable vs exported variable =="
PLAIN="only-this-shell" # shell variable (NOT exported)
export SHARED="children-see-me" # environment variable (exported)
# A child shell inherits SHARED but not PLAIN:
bash -c 'echo " child sees PLAIN=[${PLAIN:-<empty>}] SHARED=[${SHARED:-<empty>}]"'
echo
echo "== 2. set a variable for ONE command only =="
GREETING="hi" bash -c 'echo " one-shot child: GREETING=[$GREETING]"'
echo " parent after: GREETING=[${GREETING:-<unset>}]" # still unset here
echo
echo "== 3. how PATH finds a command =="
echo " PATH first entries: $(echo "$PATH" | cut -d: -f1-3)"
command -v ls # prints the exact path the shell would run for 'ls'
echo
echo "== 4. PATH order decides the winner (safe local demo) =="
tmp="$(mktemp -d)" # a private temp directory we own
printf '#!/bin/sh\necho "FAKE ls (from %s)"\n' "$tmp" > "$tmp/ls"
chmod +x "$tmp/ls" # make our stand-in executable
PATH="$tmp:$PATH" command -v ls # our directory is FIRST -> it wins
PATH="$tmp:$PATH" ls # runs the FAKE ls, proving the point
rm -rf "$tmp" # clean up the temp directory
What it does. Section 1 shows that only exported variables reach a child process. Section 2 shows the NAME=value command form sets a variable for exactly one process. Section 3 uses command -v to print which file the shell would actually run for ls. Section 4 safely demonstrates PATH order: by putting a temp directory we own at the front of PATH, our harmless stand-in ls shadows the real one — the exact mechanism behind PATH hijacking, shown here only on files we created and then delete.
Expected output (paths and your real first PATH entries will differ):
== 1. shell variable vs exported variable ==
child sees PLAIN=[<empty>] SHARED=[children-see-me]
== 2. set a variable for ONE command only ==
one-shot child: GREETING=[hi]
parent after: GREETING=[<unset>]
== 3. how PATH finds a command ==
PATH first entries: /usr/local/bin:/usr/bin:/bin
/bin/ls
== 4. PATH order decides the winner (safe local demo) ==
/tmp/tmp.XXXXXX/ls
FAKE ls (from /tmp/tmp.XXXXXX)
Edge cases. If a command name contains a slash (./ls, /bin/ls), PATH is not consulted at all. command -v reports built-ins (like cd) differently from on-disk programs. With set -u, reading a truly unset variable aborts the script — that is why the demo uses ${VAR:-<default>} to read safely.
Walkthrough of the demo, in execution order.
| Step | Line | What happens |
|---|---|---|
| 1 | set -u |
Turns unset-variable use into an error, so typos surface immediately. |
| 2 | PLAIN="only-this-shell" |
Creates a shell variable. It lives in this shell's memory only; it is not in the environment. |
| 3 | export SHARED="children-see-me" |
Creates a variable and marks it for export, so it becomes part of the environment children inherit. |
| 4 | bash -c '...' |
Starts a child bash. The child got a copy of the environment, which includes SHARED but not PLAIN. So it prints PLAIN=[<empty>] SHARED=[children-see-me]. |
| 5 | GREETING="hi" bash -c '...' |
The NAME=value command form sets GREETING only in the environment of that one child process. The child sees hi. |
| 6 | echo ... GREETING ... |
Back in the parent, GREETING was never set here, so it reads as <unset>. This proves the one-shot assignment did not leak upward. |
| 7 | command -v ls |
Asks the shell to resolve ls through PATH and print the exact path it would execute (for example /bin/ls). |
| 8 | mktemp -d |
Creates a fresh private directory we own — a safe sandbox for the demo. |
| 9 | printf ... > "$tmp/ls" + chmod +x |
Writes a tiny stand-in program named ls and makes it executable. |
| 10 | PATH="$tmp:$PATH" command -v ls |
With our directory placed first, the lookup now resolves ls to $tmp/ls — proof that order decides the winner. |
| 11 | PATH="$tmp:$PATH" ls |
Actually runs the stand-in, printing FAKE ls .... The real /bin/ls is shadowed. |
| 12 | rm -rf "$tmp" |
Cleans up so nothing dangerous is left behind. |
The key mental model: the environment is copied on process start and inherited downward only, and PATH lookup picks the first match by directory order. Steps 4 and 6 demonstrate the first rule; steps 10–11 demonstrate the second.
# WRONG
API_URL=http://localhost:8080
my_program # my_program does NOT see API_URL
Why it is wrong. APP_URL=... makes a shell variable. Children inherit only environment variables. my_program is a child, so it never sees the value.
# CORRECT
export API_URL=http://localhost:8080
my_program # now it is in the environment and is inherited
How to catch it. Run printenv API_URL — if it prints nothing, the variable is not exported.
# WRONG
FILES="report final.txt"
rm $FILES # becomes: rm report final.txt (two files!)
Why it is wrong. Without quotes the shell word-splits the value on spaces, so rm receives two arguments.
# CORRECT
rm "$FILES" # one argument: the file literally named 'report final.txt'
How to prevent it. Quote every variable expansion by default: "$VAR".
# WRONG (and risky)
export PATH=".:$PATH" # current directory searched FIRST
Why it is wrong. Any directory you cd into can now override system commands. A malicious file named ls in a downloaded folder would run instead of the real ls. This is the PATH-hijacking scenario from this lesson's quiz.
# CORRECT
export PATH="$PATH:$HOME/bin" # trusted, owner-only dir, appended (searched LAST)
How to recognize it. Audit with echo "$PATH"; flag ., empty entries (a leading or doubled : also means "current directory"), and any world-writable directory.
# WRONG
export DB_PASSWORD=hunter2 # visible to children and via /proc/<pid>/environ
Why it is wrong. The value is inherited by every child and readable from /proc.
# BETTER
chmod 600 secrets.env # owner-only file
# load it only into the one process that needs it, never export globally
How to prevent it. Keep secrets in permission-restricted files or a secrets manager; never echo or log them.
"command not found" even though the program is installed.
echo "$PATH" — is the program's directory listed?command -v progname (or which progname) — does it resolve at all?ls -l /path/to/prog should show an x bit."My variable is empty in the program but set in my shell."
printenv VAR. Empty output means it is a shell variable only — add export.sudo, or a GUI launcher gives a different (often reset) environment.sudo scrubs many variables by design; use sudo -E (carefully) or pass values explicitly."The wrong version of a command runs."
command -v prog shows which file is selected. If it is unexpected, a directory earlier in PATH is shadowing it. Inspect PATH order."Variable changed inside a script but not after it finished."
source script.sh (run it . ./script.sh) instead of executing it.Useful questions to ask: Is this variable exported? Which process is actually reading it (parent or child)? Did a new shell/sudo/GUI reset the environment? For PATH issues: what does command -v resolve to, and what is the directory order?
This is a concept lesson, so the concerns are about robustness and security rather than C memory bugs.
/usr/bin/sort) or set a known-good PATH at the top of the script. Keep . and world-writable directories out of PATH.LD_PRELOAD/LD_LIBRARY_PATH precisely because those are injection surfaces. Do not re-enable them for privileged processes./proc/<pid>/environ, and potentially by crash reporters. Keep secrets in permission-restricted files or a secrets manager; never log or echo them.$VAR causes word-splitting and glob expansion — a robustness bug that can become a security bug when the value is attacker-influenced. Quote by default.The PATH demonstration in this lesson runs entirely on files you create and delete in your own temporary directory on your own machine. Do this kind of experimentation only on systems you own or are explicitly authorized to test. Never place stand-in programs in directories used by other users or by privileged processes.
-e KEY=value), Kubernetes, and CI runners (GitHub Actions, GitLab) inject configuration and credentials through the environment. Understanding inheritance explains why a value set in one job step may not reach the next.PATH lets you run language version managers, build tools, and your own scripts by name. LD_PRELOAD is used legitimately for profiling and debugging shims.LD_PRELOAD injection are standard items on local privilege-escalation checklists; defenders audit PATH entries and file permissions to close them.Beginner rules
"$VAR".export only when a child process needs the value...printenv NAME and command -v cmd to verify what is actually set/selected.Advanced habits
PATH explicitly at the top and call critical tools by absolute path.NAME=value command one-shot form over global exports when only one command needs a setting.Objective. Get comfortable reading variables.
Requirements. Print your HOME, USER, and PATH. Then list every environment variable and count how many there are.
Hints. echo "$HOME", printenv, and pipe into wc -l to count.
Concepts. Reading variables, the environment.
Objective. See inheritance with your own eyes.
Requirements. Create a shell variable COLOR=blue (do not export). Start a child shell with bash -c 'echo "$COLOR"' and observe. Then export COLOR and repeat.
Expected. First run prints an empty line; after export, it prints blue.
Concepts. Shell vs. environment variable, inheritance.
Objective. Set a variable for exactly one command.
Requirements. Run LANG=C sort on a small file with mixed-case names and compare to running sort normally. Confirm with printenv LANG afterward that your shell's LANG is unchanged.
Hints. Use the NAME=value command form.
Concepts. One-shot environment, locale effect on tools.
Objective. Evaluate a PATH for risk.
Requirements. Print your PATH split one directory per line. Identify any entry that is ., empty, or world-writable, and explain in a sentence why each is risky. Then write a corrected PATH that appends only an owner-only ~/bin.
Hints. echo "$PATH" | tr ':' '\n'; ls -ld <dir> shows permissions.
Concepts. PATH order, PATH hijacking, defensive configuration.
Objective. Reproduce PATH order winning, then show the fix.
Requirements. In a temp directory you own, create an executable file named date that prints a marker. Run date with your temp dir prepended to PATH and show your version wins. Then show two defences: (a) calling the real tool by absolute path, and (b) appending instead of prepending. Clean up afterward.
Constraints. Operate only on files you create in your own temp directory; delete them when done.
Hints. mktemp -d, chmod +x, command -v date, /bin/date.
Concepts. PATH lookup order, hijacking mechanism, absolute paths, defensive PATH construction.
KEY=value pairs. Children inherit a copy of the parent's environment, downward only. A shell variable becomes an environment variable only when you export it.$VAR reads, NAME=value sets a shell variable, export NAME puts it in the environment, NAME=value command sets it for one command, unset NAME removes it..) enables PATH hijacking — the lesson's key security point.LD_PRELOAD, LD_LIBRARY_PATH) decide which library code loads and are an injection surface; privileged programs ignore them./proc/<pid>/environ. Keep secrets in permission-restricted files or a secrets manager.export, unquoted expansions, prepending writable directories to PATH, and storing secrets in the environment.