nebbia 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nebbia-1.0.0/PKG-INFO +106 -0
- nebbia-1.0.0/README.md +79 -0
- nebbia-1.0.0/nebbia/__init__.py +37 -0
- nebbia-1.0.0/nebbia/__main__.py +138 -0
- nebbia-1.0.0/nebbia/core.py +253 -0
- nebbia-1.0.0/nebbia.egg-info/PKG-INFO +106 -0
- nebbia-1.0.0/nebbia.egg-info/SOURCES.txt +11 -0
- nebbia-1.0.0/nebbia.egg-info/dependency_links.txt +1 -0
- nebbia-1.0.0/nebbia.egg-info/entry_points.txt +2 -0
- nebbia-1.0.0/nebbia.egg-info/requires.txt +7 -0
- nebbia-1.0.0/nebbia.egg-info/top_level.txt +3 -0
- nebbia-1.0.0/pyproject.toml +74 -0
- nebbia-1.0.0/setup.cfg +4 -0
nebbia-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nebbia
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LDAP filter obfuscation tool
|
|
5
|
+
Author: Fasertio
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ldap,obfuscation,ast,security,active-directory
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
24
|
+
Requires-Dist: black; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# nebbia
|
|
29
|
+
|
|
30
|
+
LDAP filter obfuscation tool.
|
|
31
|
+
Analyze the filter using **Abstract Syntax Tree**, applies structural transformation and return an equivalent query.
|
|
32
|
+
Useful for sneaky query to the Domain Controller or edit tools that queries against the domain (like Certipy).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nebbia
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
dev:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/Fasertio/nebbia
|
|
46
|
+
cd nebbia
|
|
47
|
+
pip install -e ".[dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Library usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from nebbia import obfuscate, parse, serialize
|
|
56
|
+
|
|
57
|
+
query = "(&(objectClass=person)(uid=jdoe)(!(locked=true)))"
|
|
58
|
+
result = obfuscate(query)
|
|
59
|
+
print(result)
|
|
60
|
+
# es: (&((!(!(\75Id=\6A\64\6F\65))))(oBjEcTcLaSs=\70\65\72son)(!(loCkeD=true)))
|
|
61
|
+
|
|
62
|
+
r1 = obfuscate(query, seed=42)
|
|
63
|
+
r2 = obfuscate(query, seed=42)
|
|
64
|
+
assert r1 == r2
|
|
65
|
+
|
|
66
|
+
from nebbia import parse, serialize, NodeType
|
|
67
|
+
|
|
68
|
+
ast = parse("(sAMAccountName=krbtgt)")
|
|
69
|
+
print(ast)
|
|
70
|
+
# ASTNode(FILTER, 'sAMAccountName'='krbtgt')
|
|
71
|
+
|
|
72
|
+
ast.value = "administrator"
|
|
73
|
+
print(serialize(ast))
|
|
74
|
+
# (sAMAccountName=administrator)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
|
|
83
|
+
#single query
|
|
84
|
+
nebbia "(uid=jdoe)"
|
|
85
|
+
|
|
86
|
+
#multiple
|
|
87
|
+
nebbia "(&(uid=admin)(objectClass=person))" --count 3
|
|
88
|
+
|
|
89
|
+
#deterministic output
|
|
90
|
+
nebbia "(cn=krbtgt)" --seed 42
|
|
91
|
+
|
|
92
|
+
#disable ANSI color
|
|
93
|
+
nebbia "(uid=test)" --no-color
|
|
94
|
+
|
|
95
|
+
#stdin
|
|
96
|
+
echo "(uid=admin)" | nebbia
|
|
97
|
+
|
|
98
|
+
#python module
|
|
99
|
+
python -m nebbia "(uid=jdoe)" --count 2
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
nebbia-1.0.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# nebbia
|
|
2
|
+
|
|
3
|
+
LDAP filter obfuscation tool.
|
|
4
|
+
Analyze the filter using **Abstract Syntax Tree**, applies structural transformation and return an equivalent query.
|
|
5
|
+
Useful for sneaky query to the Domain Controller or edit tools that queries against the domain (like Certipy).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install nebbia
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
dev:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/Fasertio/nebbia
|
|
19
|
+
cd nebbia
|
|
20
|
+
pip install -e ".[dev]"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Library usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from nebbia import obfuscate, parse, serialize
|
|
29
|
+
|
|
30
|
+
query = "(&(objectClass=person)(uid=jdoe)(!(locked=true)))"
|
|
31
|
+
result = obfuscate(query)
|
|
32
|
+
print(result)
|
|
33
|
+
# es: (&((!(!(\75Id=\6A\64\6F\65))))(oBjEcTcLaSs=\70\65\72son)(!(loCkeD=true)))
|
|
34
|
+
|
|
35
|
+
r1 = obfuscate(query, seed=42)
|
|
36
|
+
r2 = obfuscate(query, seed=42)
|
|
37
|
+
assert r1 == r2
|
|
38
|
+
|
|
39
|
+
from nebbia import parse, serialize, NodeType
|
|
40
|
+
|
|
41
|
+
ast = parse("(sAMAccountName=krbtgt)")
|
|
42
|
+
print(ast)
|
|
43
|
+
# ASTNode(FILTER, 'sAMAccountName'='krbtgt')
|
|
44
|
+
|
|
45
|
+
ast.value = "administrator"
|
|
46
|
+
print(serialize(ast))
|
|
47
|
+
# (sAMAccountName=administrator)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
|
|
56
|
+
#single query
|
|
57
|
+
nebbia "(uid=jdoe)"
|
|
58
|
+
|
|
59
|
+
#multiple
|
|
60
|
+
nebbia "(&(uid=admin)(objectClass=person))" --count 3
|
|
61
|
+
|
|
62
|
+
#deterministic output
|
|
63
|
+
nebbia "(cn=krbtgt)" --seed 42
|
|
64
|
+
|
|
65
|
+
#disable ANSI color
|
|
66
|
+
nebbia "(uid=test)" --no-color
|
|
67
|
+
|
|
68
|
+
#stdin
|
|
69
|
+
echo "(uid=admin)" | nebbia
|
|
70
|
+
|
|
71
|
+
#python module
|
|
72
|
+
python -m nebbia "(uid=jdoe)" --count 2
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nebbia
|
|
3
|
+
===============
|
|
4
|
+
LDAP filter obfuscation tool.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
---------------
|
|
8
|
+
>>> from nebbia import obfuscate
|
|
9
|
+
>>> obfuscate("(&(uid=jdoe)(objectClass=person))")
|
|
10
|
+
'(&(oBjEcTcLaSs=\\\\70\\\\65\\\\72\\\\73\\\\6F\\\\6E)(uId=\\\\6A\\\\64\\\\6F\\\\65))'
|
|
11
|
+
|
|
12
|
+
>>> from nebbia import parse, serialize
|
|
13
|
+
>>> ast = parse("(uid=admin)")
|
|
14
|
+
>>> serialize(ast)
|
|
15
|
+
'(uid=admin)'
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .core import (
|
|
19
|
+
ASTNode,
|
|
20
|
+
LDAPParseError,
|
|
21
|
+
NodeType,
|
|
22
|
+
obfuscate,
|
|
23
|
+
parse,
|
|
24
|
+
serialize,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"obfuscate",
|
|
29
|
+
"parse",
|
|
30
|
+
"serialize",
|
|
31
|
+
"ASTNode",
|
|
32
|
+
"NodeType",
|
|
33
|
+
"LDAPParseError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.0"
|
|
37
|
+
__author__ = "Fasertio"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entrypoint CLI.
|
|
3
|
+
|
|
4
|
+
Usage
|
|
5
|
+
--------
|
|
6
|
+
# via module
|
|
7
|
+
python -m nebbia "(uid=jdoe)"
|
|
8
|
+
|
|
9
|
+
# pip installation
|
|
10
|
+
nebbia "(uid=jdoe)"
|
|
11
|
+
nebbia "(uid=jdoe)" --count 3
|
|
12
|
+
nebbia "(uid=jdoe)" --seed 42
|
|
13
|
+
nebbia --demo
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from .core import LDAPParseError, obfuscate
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
25
|
+
p = argparse.ArgumentParser(
|
|
26
|
+
prog="nebbia",
|
|
27
|
+
description=(
|
|
28
|
+
"LDAP filter obfuscation tool.\n"
|
|
29
|
+
"Structural AST transformation applied."
|
|
30
|
+
),
|
|
31
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
32
|
+
epilog=(
|
|
33
|
+
"Example of usage:\n"
|
|
34
|
+
' nebbia "(uid=jdoe)"\n'
|
|
35
|
+
' nebbia "(&(uid=admin)(objectClass=person))" --count 3\n'
|
|
36
|
+
' nebbia "(cn=krbtgt)" --seed 42 --no-color'
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
p.add_argument(
|
|
40
|
+
"query",
|
|
41
|
+
nargs="?",
|
|
42
|
+
help="LDAP filter.",
|
|
43
|
+
)
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"-c", "--count",
|
|
46
|
+
type=int,
|
|
47
|
+
default=1,
|
|
48
|
+
metavar="N",
|
|
49
|
+
help="Number of variant (default: 1).",
|
|
50
|
+
)
|
|
51
|
+
p.add_argument(
|
|
52
|
+
"-s", "--seed",
|
|
53
|
+
type=int,
|
|
54
|
+
default=None,
|
|
55
|
+
metavar="SEED",
|
|
56
|
+
help="Output seed.",
|
|
57
|
+
)
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--no-color",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Disable color output ANSI.",
|
|
62
|
+
)
|
|
63
|
+
p.add_argument(
|
|
64
|
+
"-V", "--version",
|
|
65
|
+
action="version",
|
|
66
|
+
version="nebbia 1.0.0",
|
|
67
|
+
)
|
|
68
|
+
return p
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_RESET = "\033[0m"
|
|
72
|
+
_BOLD = "\033[1m"
|
|
73
|
+
_CYAN = "\033[36m"
|
|
74
|
+
_GREEN = "\033[32m"
|
|
75
|
+
_YELLOW = "\033[33m"
|
|
76
|
+
_RED = "\033[31m"
|
|
77
|
+
_DIM = "\033[2m"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _c(text: str, *codes: str, use_color: bool = True) -> str:
|
|
81
|
+
if not use_color:
|
|
82
|
+
return text
|
|
83
|
+
return "".join(codes) + text + _RESET
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _run_query(
|
|
87
|
+
query: str,
|
|
88
|
+
count: int,
|
|
89
|
+
seed: int | None,
|
|
90
|
+
use_color: bool,
|
|
91
|
+
) -> int:
|
|
92
|
+
"""Obfuscated variables for single query"""
|
|
93
|
+
sep = _c("─" * 60, _DIM, use_color=use_color)
|
|
94
|
+
print(sep)
|
|
95
|
+
print(_c("INPUT ", _BOLD, _CYAN, use_color=use_color) + query)
|
|
96
|
+
|
|
97
|
+
ok = True
|
|
98
|
+
for i in range(1, count + 1):
|
|
99
|
+
current_seed = None if seed is None else seed + i - 1
|
|
100
|
+
try:
|
|
101
|
+
result = obfuscate(query, seed=current_seed)
|
|
102
|
+
label = f"RESULT {i}" if count > 1 else "RESULT "
|
|
103
|
+
print(_c(f"{label:<11}", _BOLD, _GREEN, use_color=use_color) + result)
|
|
104
|
+
except LDAPParseError as exc:
|
|
105
|
+
print(_c("ERROR ", _BOLD, _RED, use_color=use_color) + str(exc))
|
|
106
|
+
ok = False
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
return 0 if ok else 1
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main(argv: list[str] | None = None) -> None:
|
|
113
|
+
parser = _build_parser()
|
|
114
|
+
args = parser.parse_args(argv)
|
|
115
|
+
|
|
116
|
+
use_color = not args.no_color and sys.stdout.isatty()
|
|
117
|
+
|
|
118
|
+
if args.query:
|
|
119
|
+
rc = _run_query(args.query, count=args.count, seed=args.seed, use_color=use_color)
|
|
120
|
+
sys.exit(rc)
|
|
121
|
+
|
|
122
|
+
#stdin
|
|
123
|
+
if not sys.stdin.isatty():
|
|
124
|
+
exit_code = 0
|
|
125
|
+
for line in sys.stdin:
|
|
126
|
+
line = line.strip()
|
|
127
|
+
if line:
|
|
128
|
+
rc = _run_query(line, count=args.count, seed=args.seed, use_color=use_color)
|
|
129
|
+
if rc:
|
|
130
|
+
exit_code = rc
|
|
131
|
+
sys.exit(exit_code)
|
|
132
|
+
|
|
133
|
+
parser.print_help()
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nebbia.core
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import random
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
#AST Node
|
|
15
|
+
class NodeType(Enum):
|
|
16
|
+
AND = auto()
|
|
17
|
+
OR = auto()
|
|
18
|
+
NOT = auto()
|
|
19
|
+
FILTER = auto() # (attr op value)
|
|
20
|
+
PRESENT = auto() # (attr=*)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ASTNode:
|
|
25
|
+
type: NodeType
|
|
26
|
+
children: list[ASTNode] = field(default_factory=list)
|
|
27
|
+
attribute: Optional[str] = None
|
|
28
|
+
operator: Optional[str] = None
|
|
29
|
+
value: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
if self.type == NodeType.FILTER:
|
|
33
|
+
return f"ASTNode(FILTER, {self.attribute!r}{self.operator}{self.value!r})"
|
|
34
|
+
if self.type == NodeType.PRESENT:
|
|
35
|
+
return f"ASTNode(PRESENT, {self.attribute!r}=*)"
|
|
36
|
+
return f"ASTNode({self.type.name}, children={len(self.children)})"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
#Parser
|
|
40
|
+
|
|
41
|
+
class LDAPParseError(ValueError):
|
|
42
|
+
"""Malformed LDAP query"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LDAPParser:
|
|
46
|
+
"""LDAP Parser"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, query: str) -> None:
|
|
49
|
+
self.src = query.strip()
|
|
50
|
+
self.pos = 0
|
|
51
|
+
|
|
52
|
+
def _peek(self) -> str:
|
|
53
|
+
return self.src[self.pos] if self.pos < len(self.src) else ""
|
|
54
|
+
|
|
55
|
+
def _consume(self, ch: str) -> None:
|
|
56
|
+
if self.pos >= len(self.src):
|
|
57
|
+
raise LDAPParseError(
|
|
58
|
+
f"Unexpected line end '{ch}'"
|
|
59
|
+
)
|
|
60
|
+
if self.src[self.pos] != ch:
|
|
61
|
+
ctx = self.src[max(0, self.pos - 5): self.pos + 5]
|
|
62
|
+
raise LDAPParseError(
|
|
63
|
+
f"Expected '{ch}', found '{self.src[self.pos]}' "
|
|
64
|
+
f"at pos {self.pos} (context: '...{ctx}...')"
|
|
65
|
+
)
|
|
66
|
+
self.pos += 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse(self) -> ASTNode:
|
|
70
|
+
node = self._parse_filter()
|
|
71
|
+
if self.pos != len(self.src):
|
|
72
|
+
raise LDAPParseError(
|
|
73
|
+
f"Input not used at pos {self.pos}: '{self.src[self.pos:]}'"
|
|
74
|
+
)
|
|
75
|
+
return node
|
|
76
|
+
|
|
77
|
+
def _parse_filter(self) -> ASTNode:
|
|
78
|
+
self._consume("(")
|
|
79
|
+
ch = self._peek()
|
|
80
|
+
if ch == "&":
|
|
81
|
+
node = self._parse_compound(NodeType.AND)
|
|
82
|
+
elif ch == "|":
|
|
83
|
+
node = self._parse_compound(NodeType.OR)
|
|
84
|
+
elif ch == "!":
|
|
85
|
+
node = self._parse_not()
|
|
86
|
+
else:
|
|
87
|
+
node = self._parse_simple()
|
|
88
|
+
self._consume(")")
|
|
89
|
+
return node
|
|
90
|
+
|
|
91
|
+
def _parse_compound(self, ntype: NodeType) -> ASTNode:
|
|
92
|
+
self.pos += 1
|
|
93
|
+
node = ASTNode(type=ntype)
|
|
94
|
+
while self._peek() == "(":
|
|
95
|
+
node.children.append(self._parse_filter())
|
|
96
|
+
return node
|
|
97
|
+
|
|
98
|
+
def _parse_not(self) -> ASTNode:
|
|
99
|
+
self.pos += 1
|
|
100
|
+
node = ASTNode(type=NodeType.NOT)
|
|
101
|
+
node.children.append(self._parse_filter())
|
|
102
|
+
return node
|
|
103
|
+
|
|
104
|
+
def _parse_simple(self) -> ASTNode:
|
|
105
|
+
try:
|
|
106
|
+
end = self.src.index(")", self.pos)
|
|
107
|
+
except ValueError:
|
|
108
|
+
raise LDAPParseError("Expected ')'")
|
|
109
|
+
|
|
110
|
+
expr = self.src[self.pos:end]
|
|
111
|
+
self.pos = end
|
|
112
|
+
|
|
113
|
+
m = re.match(r"^([^=<>~!]+)=\*$", expr)
|
|
114
|
+
if m:
|
|
115
|
+
return ASTNode(type=NodeType.PRESENT, attribute=m.group(1))
|
|
116
|
+
|
|
117
|
+
m = re.match(r"^([^=<>~!]+)(>=|<=|~=|=)(.*)$", expr)
|
|
118
|
+
if m:
|
|
119
|
+
return ASTNode(
|
|
120
|
+
type=NodeType.FILTER,
|
|
121
|
+
attribute=m.group(1),
|
|
122
|
+
operator=m.group(2),
|
|
123
|
+
value=m.group(3),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
raise LDAPParseError(f"Simple filter not recognized: '{expr}'")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _case_randomize(s: str) -> str:
|
|
130
|
+
"""Random mixed-case"""
|
|
131
|
+
return "".join(c.upper() if random.random() > 0.5 else c.lower() for c in s)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _hex_encode_full(value: str) -> str:
|
|
135
|
+
"""LDAP escape \\HH."""
|
|
136
|
+
return "".join(f"\\{ord(c):02X}" for c in value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _hex_encode_partial(value: str) -> str:
|
|
140
|
+
"""Alphanumeric encoding"""
|
|
141
|
+
return "".join(
|
|
142
|
+
f"\\{ord(c):02X}" if c.isalnum() and random.random() > 0.4 else c
|
|
143
|
+
for c in value
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _obfuscate_value(value: str) -> str:
|
|
148
|
+
strategy = random.choice(["plain", "full_hex", "partial_hex"])
|
|
149
|
+
if strategy == "full_hex":
|
|
150
|
+
return _hex_encode_full(value)
|
|
151
|
+
if strategy == "partial_hex":
|
|
152
|
+
return _hex_encode_partial(value)
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _double_negation(node: ASTNode) -> ASTNode:
|
|
157
|
+
return ASTNode(
|
|
158
|
+
type=NodeType.NOT,
|
|
159
|
+
children=[ASTNode(type=NodeType.NOT, children=[node])],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _shuffle_children(node: ASTNode) -> ASTNode:
|
|
164
|
+
"""AND/OR child mix"""
|
|
165
|
+
if node.type in (NodeType.AND, NodeType.OR) and node.children:
|
|
166
|
+
random.shuffle(node.children)
|
|
167
|
+
return node
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _transform(node: ASTNode, depth: int = 0) -> ASTNode:
|
|
171
|
+
"""Recursive transformation"""
|
|
172
|
+
|
|
173
|
+
# ricorsione sui figli
|
|
174
|
+
node.children = [_transform(c, depth + 1) for c in node.children]
|
|
175
|
+
|
|
176
|
+
# ── offuscamento lessicale ───────────────
|
|
177
|
+
if node.attribute:
|
|
178
|
+
node.attribute = _case_randomize(node.attribute)
|
|
179
|
+
|
|
180
|
+
if node.type == NodeType.FILTER and node.value is not None:
|
|
181
|
+
node.value = _obfuscate_value(node.value)
|
|
182
|
+
|
|
183
|
+
# ── trasformazioni strutturali ───────────
|
|
184
|
+
if node.type in (NodeType.AND, NodeType.OR):
|
|
185
|
+
node = _shuffle_children(node)
|
|
186
|
+
# doppia negazione su un figlio casuale con prob 30 %
|
|
187
|
+
if node.children and random.random() < 0.30:
|
|
188
|
+
idx = random.randrange(len(node.children))
|
|
189
|
+
node.children[idx] = _double_negation(node.children[idx])
|
|
190
|
+
|
|
191
|
+
elif node.type == NodeType.FILTER and depth > 0 and random.random() < 0.20:
|
|
192
|
+
node = _double_negation(node)
|
|
193
|
+
|
|
194
|
+
return node
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def serialize(node: ASTNode) -> str:
|
|
198
|
+
"""ASTNode conversion"""
|
|
199
|
+
match node.type:
|
|
200
|
+
case NodeType.AND:
|
|
201
|
+
return "(&" + "".join(serialize(c) for c in node.children) + ")"
|
|
202
|
+
case NodeType.OR:
|
|
203
|
+
return "(|" + "".join(serialize(c) for c in node.children) + ")"
|
|
204
|
+
case NodeType.NOT:
|
|
205
|
+
return "(!" + serialize(node.children[0]) + ")"
|
|
206
|
+
case NodeType.PRESENT:
|
|
207
|
+
return f"({node.attribute}=*)"
|
|
208
|
+
case NodeType.FILTER:
|
|
209
|
+
return f"({node.attribute}{node.operator}{node.value})"
|
|
210
|
+
case _:
|
|
211
|
+
raise LDAPParseError(f"Unknown node: {node.type}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
#public function
|
|
215
|
+
def obfuscate(query: str, seed: Optional[int] = None) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
query : str
|
|
220
|
+
seed : int, optional
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
str
|
|
225
|
+
|
|
226
|
+
Raises
|
|
227
|
+
------
|
|
228
|
+
LDAPParseError
|
|
229
|
+
|
|
230
|
+
Examples
|
|
231
|
+
--------
|
|
232
|
+
>>> from nebbia import obfuscate
|
|
233
|
+
>>> obfuscate("(uid=jdoe)", seed=42)
|
|
234
|
+
'(uId=\\\\6A\\\\64\\\\6F\\\\65)'
|
|
235
|
+
"""
|
|
236
|
+
if seed is not None:
|
|
237
|
+
random.seed(seed)
|
|
238
|
+
ast = LDAPParser(query).parse()
|
|
239
|
+
ast = _transform(ast)
|
|
240
|
+
return serialize(ast)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def parse(query: str) -> ASTNode:
|
|
244
|
+
"""
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
query : str
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
ASTNode
|
|
252
|
+
"""
|
|
253
|
+
return LDAPParser(query).parse()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nebbia
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LDAP filter obfuscation tool
|
|
5
|
+
Author: Fasertio
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ldap,obfuscation,ast,security,active-directory
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
24
|
+
Requires-Dist: black; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# nebbia
|
|
29
|
+
|
|
30
|
+
LDAP filter obfuscation tool.
|
|
31
|
+
Analyze the filter using **Abstract Syntax Tree**, applies structural transformation and return an equivalent query.
|
|
32
|
+
Useful for sneaky query to the Domain Controller or edit tools that queries against the domain (like Certipy).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nebbia
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
dev:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/Fasertio/nebbia
|
|
46
|
+
cd nebbia
|
|
47
|
+
pip install -e ".[dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Library usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from nebbia import obfuscate, parse, serialize
|
|
56
|
+
|
|
57
|
+
query = "(&(objectClass=person)(uid=jdoe)(!(locked=true)))"
|
|
58
|
+
result = obfuscate(query)
|
|
59
|
+
print(result)
|
|
60
|
+
# es: (&((!(!(\75Id=\6A\64\6F\65))))(oBjEcTcLaSs=\70\65\72son)(!(loCkeD=true)))
|
|
61
|
+
|
|
62
|
+
r1 = obfuscate(query, seed=42)
|
|
63
|
+
r2 = obfuscate(query, seed=42)
|
|
64
|
+
assert r1 == r2
|
|
65
|
+
|
|
66
|
+
from nebbia import parse, serialize, NodeType
|
|
67
|
+
|
|
68
|
+
ast = parse("(sAMAccountName=krbtgt)")
|
|
69
|
+
print(ast)
|
|
70
|
+
# ASTNode(FILTER, 'sAMAccountName'='krbtgt')
|
|
71
|
+
|
|
72
|
+
ast.value = "administrator"
|
|
73
|
+
print(serialize(ast))
|
|
74
|
+
# (sAMAccountName=administrator)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
|
|
83
|
+
#single query
|
|
84
|
+
nebbia "(uid=jdoe)"
|
|
85
|
+
|
|
86
|
+
#multiple
|
|
87
|
+
nebbia "(&(uid=admin)(objectClass=person))" --count 3
|
|
88
|
+
|
|
89
|
+
#deterministic output
|
|
90
|
+
nebbia "(cn=krbtgt)" --seed 42
|
|
91
|
+
|
|
92
|
+
#disable ANSI color
|
|
93
|
+
nebbia "(uid=test)" --no-color
|
|
94
|
+
|
|
95
|
+
#stdin
|
|
96
|
+
echo "(uid=admin)" | nebbia
|
|
97
|
+
|
|
98
|
+
#python module
|
|
99
|
+
python -m nebbia "(uid=jdoe)" --count 2
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
nebbia/__init__.py
|
|
4
|
+
nebbia/__main__.py
|
|
5
|
+
nebbia/core.py
|
|
6
|
+
nebbia.egg-info/PKG-INFO
|
|
7
|
+
nebbia.egg-info/SOURCES.txt
|
|
8
|
+
nebbia.egg-info/dependency_links.txt
|
|
9
|
+
nebbia.egg-info/entry_points.txt
|
|
10
|
+
nebbia.egg-info/requires.txt
|
|
11
|
+
nebbia.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
# ── Metadati pacchetto ───────────────────────────────────────────────────
|
|
6
|
+
[project]
|
|
7
|
+
name = "nebbia"
|
|
8
|
+
version = "1.0.0"
|
|
9
|
+
description = "LDAP filter obfuscation tool"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["ldap", "obfuscation", "ast", "security", "active-directory"]
|
|
14
|
+
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Fasertio" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Information Technology",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Security",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=7",
|
|
38
|
+
"pytest-cov",
|
|
39
|
+
"black",
|
|
40
|
+
"ruff",
|
|
41
|
+
"mypy",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# ── Entrypoint CLI ───────────────────────────────────────────────────────
|
|
45
|
+
[project.scripts]
|
|
46
|
+
nebbia = "nebbia.__main__:main"
|
|
47
|
+
|
|
48
|
+
# ── Setuptools config ────────────────────────────────────────────────────
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["."]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
nebbia = ["py.typed"]
|
|
54
|
+
|
|
55
|
+
# ── Tool: black ──────────────────────────────────────────────────────────
|
|
56
|
+
[tool.black]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = ["py310"]
|
|
59
|
+
|
|
60
|
+
# ── Tool: ruff ───────────────────────────────────────────────────────────
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
select = ["E", "F", "I", "UP"]
|
|
64
|
+
|
|
65
|
+
# ── Tool: mypy ───────────────────────────────────────────────────────────
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.10"
|
|
68
|
+
strict = true
|
|
69
|
+
ignore_missing_imports = true
|
|
70
|
+
|
|
71
|
+
# ── Tool: pytest ─────────────────────────────────────────────────────────
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
addopts = "-v --tb=short"
|
nebbia-1.0.0/setup.cfg
ADDED