Share
## https://sploitus.com/exploit?id=FE8307BF-7507-50DB-80F6-7328AB78C553
# tuktam

[![license](https://img.shields.io/badge/license-MIT-4e85a8)](LICENSE-MIT)
![python](https://img.shields.io/badge/python-3.9%2B-4e85a8)
![version](https://img.shields.io/badge/version-3.42-4e85a8)
![dependencies](https://img.shields.io/badge/dependencies-none-4e85a8)
![CWE](https://img.shields.io/badge/CWE-367%2F362-4e85a8)
[![CI](https://github.com/zeroscience/tuktam/actions/workflows/ci.yml/badge.svg)](https://github.com/zeroscience/tuktam/actions/workflows/ci.yml)

A focused, cross-language detector for **time-of-check / time-of-use** races
(CWE-367, CWE-362) - the bug class behind a large share of local privilege
escalations and remote DoS. It **finds** the pattern in source and then
**generates a runnable PoC that verifies the race is real**.

---

## The idea

Every TOCTOU reduces to one OS-agnostic invariant:

> A program **checks** a resource **by name / by shared state**, then **uses**
> it - instead of operating on a stable **handle** it already holds under a
> lock. An attacker mutates what the name/state refers to **in the gap**, so the
> use lands on something else (a symlinked secret, a recycled thread id).

Two concrete shapes fall out of that one invariant, and `tuktam` detects both:

| Family | Check | Use | Attacker swaps | Impact |
|---|---|---|---|---|
| **Filesystem** | `access`/`stat`/`exists` on a path | `open`/`chmod`/`unlink` on the same path | a symlink / junction / rename | privilege escalation, arbitrary read/write |
| **Thread / shared state** | `if (ctx->flag)` unlocked | `pthread_join(ctx->tid)` | the shared struct via a concurrent thread | crash / DoS, use-after-free |
| **App check-then-act** | `store.find(token)` | `store.create/consume(...)` | the record/token via a concurrent request | token reuse, duplicate accounts, double-spend |

Every engine emits the **same `Finding`** (`tuktam/model.py`). That shared schema
is the "universal" part - every engine speaks it, so one reporter and one PoC
generator serve them all.

---

## Feature summary

- **Languages:** Python (AST, precise), C/C++, Go, Java, C#/.NET, Rust,
  TypeScript/JS, Ruby, PHP (heuristic engines).
- **3 rule families:** filesystem TOCTOU (`TOCTOU-FS`), unsynchronized
  thread/state TOCTOU (`TOCTOU-STATE`, C/C++), and application non-atomic
  check-then-act (`TOCTOU-APP`, Go/TS/JS/Java/C#/Ruby/PHP).
- **Taint-lite:** follows plain `a = b` / `a := b` aliases across the gap.
- **Confidence tiers:** `HIGH` / `MEDIUM` / `LOW`, based on precision and
  aggravators (e.g. a `sleep()` widening a race window).
- **Numbered findings** + an interactive **`Y` / `N`** PoC prompt.
- **PoC generator:** emits a standalone script that verifies each race with a
  *deterministic proof* and an *empirical win-rate*. Faithful symlink mode plus a
  privilege-free fallback for Windows.
- **CI-ready output:** `text` / `json` / `sarif` (GitHub code scanning), plus a
  `--fail-on-findings` gate.
- **No dependencies, no privileges, no target execution** for scanning - pure
  Python stdlib, runs anywhere including Windows.

---

## Install & requirements

- **Python 3.8+** (stdlib only; `ast.get_source_segment` needs >= 3.8).
- No third-party runtime dependencies.

Install the `tuktam` command (isolated, recommended):

```bash
pipx install tuktam
```

Or with pip / from a clone:

```bash
pip install tuktam           # from PyPI
pip install -e .             # editable, from a checkout
```

Then run it as `tuktam `. No install needed either - the package also
runs in place:

```bash
python -m tuktam 
```

Development (tests):

```bash
pip install -e ".[dev]"
pytest
```

---

## Usage

```
python -m tuktam TARGET [options]

positional:
  TARGET                 file or directory to scan

options:
  -f, --format {text,json,sarif}   report format (default: text)
  -o, --output FILE                write the report to FILE
  --fail-on-findings               exit 1 if any finding (CI gate)
  --poc N                          non-interactively generate a PoC for finding #N
  --poc-dir DIR                    output dir for PoCs (default: poc/)
  --prompt                         force the interactive PoC prompt (even when piped)
  --no-prompt                      never show the interactive PoC prompt
  --no-banner                      suppress the startup banner
  --version                        show version and exit
```

**Samples**

```bash
python -m tuktam ./src                          # scan a tree
python -m tuktam app.py                         # scan one file
python -m tuktam ./src -f sarif -o tuktam.sarif # SARIF for GitHub code scanning
python -m tuktam ./src --fail-on-findings       # CI gate
python -m tuktam app.py --poc 3                 # PoC for finding #3, no prompt
python -m tuktam app.py --poc 3 --poc-dir out/  # ...into out/
```

After a text-mode scan on a terminal, findings are numbered and you're asked:

```
Create a PoC script for a detected vuln? [N / Y, e.g. Y3]: Y3
  [+] PoC #3 written: poc/poc_003_python_access_chmod.py
      run it: python poc/poc_003_python_access_chmod.py
```

`Y3` -> PoC for finding #3 - `N` (or empty) -> skip - bad input -> re-asks with the
valid range. Accepts `Y3`, `y 3`, `y3` (case-insensitive).

---

## Output formats

**text** (default)

```
tests/fixtures/vuln.py
  #1   [HIGH  ] TOCTOU-FS  check exists()@6 -> use open()@7  on `path`  (python)
           'exists' checks the path, then 'open' re-opens it by name - attacker
           can swap the target in between.
           fix: Open the path ONCE to a handle/fd, then operate on that fd
           (os.open(..., O_NOFOLLOW|O_EXCL) + os.fstat). Never re-resolve the name.
```

**json** - the raw `Finding` list (`file`, `check_line`, `use_line`, `path_expr`,
`check_call`, `use_call`, `language`, `confidence`, `rule_id`, `message`,
`suggestion`, `index`). Ideal for piping into other tooling.

**sarif** - SARIF 2.1.0 with a tool driver (name/version/organization) and both
rule descriptors, so GitHub code scanning renders findings inline on PRs.

---

## Desktop GUI

A stdlib **Tkinter** GUI ships alongside the CLI - same engine, still no extra
dependencies. Launch it with:

```bash
tuktam-gui            # installed
python -m tuktam.gui  # from a checkout
```

Pick a file or folder, hit Scan, and browse findings grouped by file and colored
by severity. Selecting a finding shows its message and fix plus the offending
source with the check and use lines highlighted; one click generates a PoC for
it, and the whole run exports to JSON or SARIF from the same window.

![tuktam GUI](tuktamgui.png)

*Findings grouped by file and colored by severity; the selected finding shows its
message, fix, and source with the check (amber) and use (red) lines highlighted.*

### Windows .exe

Bundle a standalone, no-Python-required executable with PyInstaller:

```bash
pip install pyinstaller
pyinstaller --onefile --windowed --name tuktam-gui packaging/tuktam_gui.py
# -> dist/tuktam-gui.exe
```

## How detection works

Scanning is **static**: the code is parsed/scanned, never executed. Files are
dispatched by extension in `tuktam/static/engine.py`:

- `.py` -> `rules_python.scan_python` (AST)
- `.c/.cpp/.h/...` -> `languages` heuristic **+** `rules_state.scan_state`
- `.go/.java/.cs/.rs` -> the `languages` heuristic engine

### Family 1 - filesystem TOCTOU (`TOCTOU-FS`)

The invariant: within one function scope, a **check-by-name** call is followed by
a **use-by-name** call on the *same* path expression, with no atomic handle in
between.

- **Python** (`rules_python.py`) uses the real **AST** - true scopes, true
  argument nodes - so it is precise. Direct hits are `HIGH`.
- **C, Go, Java, C#, Rust** share one engine (`heuristic.py`) driven by
  per-language pattern tables (`languages.py`). It uses regex for the check/use
  calls and **brace-depth scoping** so a check only pairs with a use in the same
  (or a nested) block. Method-style checks (`f.exists()`, `p.is_file()`) capture
  the *receiver* as the path; call-style checks capture the argument.

Check/use vocab per language (excerpt):

| Lang | check | use | safe form |
|---|---|---|---|
| C | `access` `stat` `lstat` | `open` `fopen` `unlink` `chmod` | `openat(...,O_NOFOLLOW)` + `fstat(fd)` |
| Python | `os.path.exists` `os.access` | `open` `os.chmod` `shutil.copy` | `os.open(...,O_EXCL)` + `os.fstat` |
| Go | `os.Stat` `os.Lstat` | `os.Open` `os.Remove` `os.WriteFile` | `os.OpenFile` handle |
| Java | `f.exists()` `Files.exists` | `new FileInputStream` `Files.delete` | `NOFOLLOW_LINKS` channel |
| C# | `File.Exists` | `File.OpenRead` `File.Delete` | `FileMode.CreateNew` |
| Rust | `fs::metadata` `p.is_file()` | `File::open` `fs::remove_file` | `OpenOptions` + `O_NOFOLLOW` |

### Family 2 - thread/state TOCTOU (`TOCTOU-STATE`)

Not every TOCTOU touches the filesystem. `rules_state.py` (C/C++) detects the
**unsynchronized shared-state race**: a thread reads a shared field in a guard,
then acts on a related handle without holding the lock other code uses for it.

Per function (brace-scoped) it looks for:

1. **Sink** - `pthread_join` / `pthread_cancel` / `pthread_detach` / `pthread_kill`
   on an `obj->field` (a thread handle).
2. **Guard** - an earlier `if/while (... obj->field ...)` on the **same** base
   object.
3. **Unsynchronized** - the function takes **no** mutex
   (`pthread_mutex_lock`, `EnterCriticalSection`, ...). If it does, the function is
   skipped (keeps false positives low).
4. **Aggravator** - a `sleep()`/`usleep()`/`Sleep()` between guard and sink -> the
   window is huge and attacker-triggerable -> `HIGH`.

This is the exact shape of **CVE-2024-11144** (see the case study below).

### Family 3 - app check-then-act (`TOCTOU-APP`)

The dominant *modern* race class (most 2026 race-condition CVEs). Not a symlink,
not a thread - application **logic**: a shared record/token is checked, then
mutated, with no transaction or lock. Two concurrent requests both pass the check
before either acts, so a one-time action happens twice (invite-token reuse,
duplicate accounts from a single-use token, double-spend).

`rules_app.py` runs over Go, TypeScript/JS, Java, C#, Ruby, and PHP (brace-scoped
per function) and flags:

1. **CHECK** - a lookup/verify call (`find*`, `first`, `exists`, `verify`, ...).
2. **ACT** - a later mutation (`create`, `insert`, `update`, `delete`,
   `consume`, `revoke`, ...).
3. **Correlated key** - the two calls share a meaningful identifier
   (`token`, `email`, `invite`, ...), so unrelated calls aren't paired.
4. **No atomicity guard** in scope - if the function uses a transaction, row lock
   (`SELECT ... FOR UPDATE`), `ON CONFLICT`, `synchronized`, mutex, or CAS, it's
   skipped.

```
tests/fixtures/app_invite.ts
  #1  [MEDIUM] TOCTOU-APP  check find()@3 -> use update()@8  on `token`  (ts)
        Non-atomic check-then-act on shared state: 'find(...)' then 'update(...)'
        share key 'token' with no transaction/lock in scope - two concurrent
        requests can both pass the check before either acts (CWE-367).
```

This family is heuristic and cross-language -> `MEDIUM` confidence. The keyed
correlation and guard suppression are what keep it precise: in the fixtures, the
transaction-wrapped `*Safe` variants produce **zero** findings.

### Taint-lite

Both engines follow **plain copies** (`a = b`, `a := b`, `let a = b`) across the
gap, so a check on one name pairs with a use of its alias. Alias-linked findings
are reported at **lower** confidence to signal the indirection. Path-building
(`a = join(dir, name)`) and reassignment ordering are intentionally *not* modeled
yet - that's full dataflow, and it's future work.

### Confidence tiers

| Tier | Meaning |
|---|---|
| **HIGH** | Python AST direct hit, **or** a state race with a `sleep()` window |
| **MEDIUM** | Heuristic direct hit, Python alias-linked hit, or an unslept state race |
| **LOW** | Heuristic alias-linked hit (indirection increases FP odds) |

---

## How PoC verification works

`tuktam/poc.py` turns a `Finding` into a **standalone, runnable Python script**.
It reconstructs the detected `check -> use` window and swaps what the target
resolves to *inside* that window, then reports two independent results:

1. **Deterministic proof** - the attacker is fired exactly once inside the gap.
   If the use lands on the SECRET target, the script prints `RACE WON` and the
   pattern is proven exploitable *in the model*.
2. **Empirical win-rate** - a free-running attacker thread races the victim over
   thousands of trials; the script reports how often it wins. This shows the race
   is winnable **without** an artificially widened window.

For **filesystem** findings the "use" is mapped to the right attack - **read**
(leak a secret), **write** (overwrite a protected file through a symlink), or
**chmod** (make a secret world-writable):

- **symlink mode** (default, faithful): follows a symlink to a protected target -
  real privilege escalation. Needs symlink privilege (Linux/macOS; Windows with
  Developer Mode/admin).
- **`--content-swap`** (privilege-free fallback): swaps file *content* instead of
  a link; auto-engages when symlinks aren't permitted. Enough to demonstrate
  **read** races on locked-down Windows.
- **`--wide`**: widens the window with a tiny sleep - a deterministic teaching demo.

For **state** findings the PoC models the thread lifecycle: a shared `{valid, tid}`
struct, a victim that checks `valid` then joins `tid` without a lock, and a
concurrent thread that recycles the context in the gap -> the join hits a stale id
-> modeled `SIGSEGV` (DoS).

For **app** findings the PoC models the shared store: two concurrent requests
both pass the check for a single-use key before either acts -> the one-time action
runs twice (`RACE WON`). `--wide` drives the empirical rate to 100%.

### "Is it 100% when the race is won?"

Short answer: **the deterministic proof is 100% conclusive about the *logic*, not
about your *production binary*.**

- When the deterministic proof prints `RACE WON`, it has *proven* that the
  check->use gap allows the use to land on an attacker-controlled target. There is
  **no false positive on the pattern** - the window is real and the logic is
  exploitable. That's the 100% claim, and it's a claim about the reconstructed
  model of the detected pattern.
- It does **not** prove that *your specific deployed program* is exploitable
  end-to-end. Real exploitation still depends on **reachability** (can an attacker
  reach that code path?), **timing** (how wide is the real window? - see the
  empirical rate), and **privileges** (symlink rights, filesystem layout).
- The **empirical win-rate** is your realism gauge: filesystem races typically win
  a single-digit-to-low-double-digit percentage of narrow-window trials, which is
  plenty for a patient attacker retrying in a loop.

So: `RACE WON` (deterministic) = "this is a genuine TOCTOU, not a false alarm."
Empirical % = "here's how winnable it is in practice." Turning that into a live
exploit against a real target is the last mile you drive by adapting the PoC's
clearly-marked `victim()` section. The generator is `tuktam/poc.py`; the CLI glue
(numbering + `Y` prompt + `--poc N`) is `tuktam/cli.py`.

---

## Real-world case study: LightFTP CVE-2024-11144

**Target:** [hfiref0x/LightFTP](https://github.com/hfiref0x/LightFTP) v2.3 -
**Advisory:** Black Duck CyRC, CVE-2024-11144 (CVSS 7.5, remote DoS) -
**Bug:** unsynchronized thread-lifecycle TOCTOU in `worker_thread_cleanup`.

**Step 1 - the filesystem engine alone finds nothing** (this is *not* a
filesystem race):

```
$ python -m tuktam LightFTP/Source
No TOCTOU patterns found.
```

**Step 2 - with the `TOCTOU-STATE` family, the scanner lands on the exact line
the advisory names (`ftpserv.c:240`):**

```
LightFTP/Source/ftpserv.c
  #1  [HIGH] TOCTOU-STATE  check context->WorkerThreadValid()@232 -> use pthread_join()@240  on `context->WorkerThreadId`  (c)
        Unsynchronized thread-state race: 'context->WorkerThreadValid' is checked,
        then 'pthread_join(context->WorkerThreadId)' runs with no mutex held and a
        sleep() widens the window - a concurrent thread can invalidate the handle
        in the gap (CWE-362/367).
  #2  [HIGH] TOCTOU-STATE  check context->WorkerThreadValid()@232 -> use pthread_cancel()@244  on `context->WorkerThreadId`  (c)
```

The vulnerable code:

```c
void worker_thread_cleanup(PFTPCONTEXT context) {
    if ( context->WorkerThreadValid == 0 ) {   // TIME OF CHECK (line 232)
        context->WorkerThreadAbort = 1;
        sleep(2);                               // 2-second window, attacker-triggerable
        err = pthread_join(context->WorkerThreadId, &retv);  // TIME OF USE (line 240)
        ...
    }
}
```

Note that `worker_thread_start()` right below *does* take `pthread_mutex_lock(&context->MTLock)`
for the same state - but `worker_thread_cleanup()` doesn't. That asymmetry is the
bug: an EPRT-command anomaly triggers cleanup while a fresh connection reuses the
context, so `WorkerThreadId` changes during the `sleep(2)` and `pthread_join`
crashes the server.

**Step 3 - the patch analysis.** On the *fixed*
v2.3.1 the scanner **still flags the pattern** (now at lines 253/257):

```
LightFTP/Source/ftpserv.c
  #1  [HIGH] TOCTOU-STATE  check context->WorkerThreadValid()@232 -> use pthread_join()@253 ...
```

That's correct: the official fix **narrows** the window (re-check
`WorkerThreadValid == -1` before join, close sockets earlier, split the sleep) but
**never adds a mutex** to `worker_thread_cleanup`. The unsynchronized access
remains - the fix reduces the odds rather than eliminating the race. `tuktam`'s
suggestion is exactly the durable fix: hold `context->MTLock` across *both* the
check and the join.

**Step 4 - the generated PoC verifies the DoS model:**

```
$ python -m tuktam LightFTP/Source/ftpserv.c --poc 1
$ python poc/poc_001_c_context_WorkerThreadValid_pthread_join.py
TOCTOU state PoC  --  context->WorkerThreadValid -> pthread_join
================================================================
[*] Deterministic proof:
    [+] RACE WON (DoS): pthread_join on invalid tid 62715 -> SIGSEGV
    [+] check 'context->WorkerThreadValid' then 'pthread_join' is UNSYNCHRONIZED and exploitable.
[*] Empirical race: modeled cleanup calls hit an invalid join.
```

The PoC is a faithful *model*; a live exploit drives the real server (open a
session, trigger the EPRT anomaly while racing a fresh connection). The scanner
found it, explained it, out-analyzed the patch, and produced a verification - the
whole `find -> explain -> verify` loop on a real CVE.

---

## Module reference

```
tuktam/
โ”œโ”€โ”€ __init__.py          version/author/vendor constants + BANNER
โ”œโ”€โ”€ __main__.py          enables `python -m tuktam`
โ”œโ”€โ”€ cli.py               arg parsing, finding numbering, Y/N PoC prompt, --poc
โ”œโ”€โ”€ model.py             Finding dataclass - the one schema every engine emits
โ”œโ”€โ”€ report.py            text / json / sarif renderers
โ”œโ”€โ”€ poc.py               PoC generator (filesystem + state + app templates)
โ”œโ”€โ”€ static/
โ”‚   โ”œโ”€โ”€ engine.py        walks a path, dispatches files by extension
โ”‚   โ”œโ”€โ”€ rules_python.py  Python AST engine (TOCTOU-FS) + taint-lite  -> precise
โ”‚   โ”œโ”€โ”€ heuristic.py     generic regex + brace-scope engine + taint-lite
โ”‚   โ”œโ”€โ”€ languages.py     per-language pattern tables (C/Go/Java/C#/Rust)
โ”‚   โ”œโ”€โ”€ rules_c.py       back-compat shim over the heuristic engine
โ”‚   โ”œโ”€โ”€ rules_state.py   thread/shared-state engine (TOCTOU-STATE, C/C++)
โ”‚   โ””โ”€โ”€ rules_app.py     app check-then-act engine (TOCTOU-APP, Go/TS/JS/Java/C#/...)
tests/fixtures/          vulnerable + safe samples across the supported languages
```

---

## Limitations

- Non-Python engines are line/brace **heuristics**, not full parsers -> occasional
  false positives/negatives. Python (AST) is precise.
- **Taint-lite** follows only plain `a = b` copies - not path-building or
  reassignment ordering. Full dataflow is future work.
- `unlink`-style deletes in the filesystem PoC are demonstrated via destructive
  overwrite-through-symlink; a true directory-swap `unlink` race needs a dedicated
  harness (noted in the generated PoC).
- Static analysis reports *potential* races. The PoC proves the *pattern* is
  exploitable; end-to-end exploitation of a specific binary still depends on
  reachability, timing, and privileges.

---

## License

MIT ยฉ Zero Science Lab

---

*tuktam v3.42 (build 1700, "Kumanovo").*