Skip to content
CWE-98A01:2021 – Broken Access Control

Methodology

An order of operations that answers the questions that change what you do next, before spending time on payloads that the answers would have ruled out.

The order matters

Most time lost on this bug class goes to trying payloads against a target where the answer was already determined. There is no point cycling through encodings against a real allowlist, or trying filter chains against a sink with a directory prefix.

So the order below is arranged by how much each answer eliminates. Four questions decide almost everything:

  1. Does the sink execute, or only read?
  2. Is your input at the start of the string?
  3. Is a suffix appended?
  4. Is the check on the input string, or on the resolved path?

One good source read answers all four at once, which is why it comes early.

1. Find the parameters

Look for values that name a resource rather than describe one. The names repeat across applications:

page file path template tpl view include inc document doc folder dir root pg style theme lang locale module mod action cat content name download report attachment img image src url load read show detail type layout skin conf config

Do not only look in the query string. Check the POST body, cookies, JSON fields, path segments, and headers the application reads. A lang cookie reaching a template loader is the same bug with less traffic through it.

And look at what the values look like: a parameter whose normal value is home or en or invoice.pdf is naming a file even if the parameter is called t. A parameter whose normal value is 47 probably is not.

2. Confirm the sink, cheaply

HTTP
# Four requests. Compare responses rather than looking for success.

# a. The baseline. Record length, status, and timing.
GET /index.php?page=home

# b. A path that certainly does not exist. The DIFFERENCE from the baseline
#    is the oracle you will use for everything else.
GET /index.php?page=zzzznonexistentzzzz

# c. Absolute. If this works, nothing is prepended — every wrapper is live.
GET /index.php?page=/etc/passwd

# d. Padded traversal.
GET /index.php?page=../../../../../../../../etc/passwd

# Read the errors. An error naming the full attempted path answers the
# prefix and suffix questions in one request and is worth more than the
# next ten payloads:
#
#   failed to open stream: No such file or directory
#     in /var/www/html/index.php on line 4
#   include(/var/www/html/pages/zzzz.php)
#                          ^^^^^^   ^^^^
#                          prefix   suffix

3. Read the source

On PHP, this is the request that changes the engagement:

?page=php://filter/convert.base64-encode/resource=index.php

It tells you the sink function, the prefix, the suffix, every filter applied, and the names of every file the application includes next. See Reading Source with php://filter.

If it returns base64, you have also just confirmed that wrappers are reachable, which means filter chains are available and you have a path to code execution.

If it does not work, that is equally informative — it means either a prefix is in the way, a suffix breaks the resource, or the sink is not PHP. Each of those redirects you somewhere specific.

On other runtimes there is no equivalent, so substitute: look for exposed .git, a source map, a stack trace, a composer.lock or package.json, or a debug endpoint. Failing that, work from the error messages.

4. Establish impact before escalating

Reading /etc/passwd proves arbitrary read. It is not impact and it should not be the only evidence in a report.

Go for the things that mean something, in this order:

  1. Application config. config.php, .env, credentials.yml.enc plus master.key, appsettings.json, settings.py. Database credentials and framework signing keys.
  2. /proc/self/environ. On a container this is frequently better than the config file — see /proc, environ, and File Descriptors.
  3. Cloud and orchestrator credentials. /var/run/secrets/kubernetes.io/serviceaccount/token, ~/.aws/credentials, mounted secret files.
  4. Anything that forges identity. secret_key_base, APP_KEY, a JWT signing key. A read primitive that yields one of these is usually worth more than a shell, and it is quieter.

Stop and write up once you have something that demonstrates real impact. Escalating to code execution is often unnecessary and is always noisier — see the note in The Temp-File Race.

5. Escalate, if it is in scope

In order of cost — try them in this order, not in order of how impressive they are:

TechniqueCostNeeds
Filter chainOne requestExecuting sink, input at position 0, no suffix
Upload + zip:// or phar://A few requestsAn upload feature and a known path
phar:// deserializationA few requestsAn upload and a gadget chain
Session poisoningA few requestsFile sessions, a controllable value
Log poisoningA few requestsA readable, small log
Temp-file raceHundreds of requestsConcurrency; very loud
Classic RFIOne requestallow_url_include=On, which it will not be

The filter chain is first because it is one quiet request and needs nothing from the environment. The temp-file race is last because it is loud enough to matter operationally and can fill /tmp on the target.

6. Write it up

A report on this bug class needs six things, and the ones people leave out are the third and fourth:

  1. The exact request, including the method and any body.
  2. The response, trimmed to the evidence.
  3. The sink type — executes or reads. This decides the severity and the reader cannot infer it.
  4. The configuration you observed — runtime, version, and the relevant settings. "Confirmed on PHP 8.3 with a stock php.ini" is a sentence that survives triage.
  5. What you reached, and what it is worth. Not /etc/passwd.
  6. What you did not try, and why. "Escalation to RCE via a filter chain is available on this configuration and was not attempted, per the rules of engagement" tells the reader the true severity without you having to demonstrate it.

For the fix, recommend the allowlist first and describe open_basedir and friends as defence in depth. See Defense in Depth. Recommending allow_url_include=Off as a remediation is recommending a setting that is already off and was never relevant — it is the fastest way to signal that the report was written from a 2010 cheatsheet.