basicbuster 0.1.0__py3-none-any.whl
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.
- basicbuster/__init__.py +3 -0
- basicbuster/cli.py +69 -0
- basicbuster/core.py +191 -0
- basicbuster-0.1.0.dist-info/METADATA +133 -0
- basicbuster-0.1.0.dist-info/RECORD +9 -0
- basicbuster-0.1.0.dist-info/WHEEL +5 -0
- basicbuster-0.1.0.dist-info/entry_points.txt +2 -0
- basicbuster-0.1.0.dist-info/licenses/LICENSE +21 -0
- basicbuster-0.1.0.dist-info/top_level.txt +1 -0
basicbuster/__init__.py
ADDED
basicbuster/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from basicbuster import __version__
|
|
7
|
+
from basicbuster.core import DEFAULT_TIMEOUT, run_attack
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
p = argparse.ArgumentParser(
|
|
12
|
+
prog="basicbuster",
|
|
13
|
+
description="HTTP Basic Authentication wordlist testing (authorized use only).",
|
|
14
|
+
)
|
|
15
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
16
|
+
p.add_argument("--url", required=True, help="Target URL protected by HTTP Basic Auth.")
|
|
17
|
+
p.add_argument(
|
|
18
|
+
"--username",
|
|
19
|
+
metavar="FILE",
|
|
20
|
+
help="Path to username wordlist (one per line).",
|
|
21
|
+
)
|
|
22
|
+
p.add_argument(
|
|
23
|
+
"--password",
|
|
24
|
+
metavar="FILE",
|
|
25
|
+
help="Path to password wordlist (one per line).",
|
|
26
|
+
)
|
|
27
|
+
p.add_argument(
|
|
28
|
+
"--userpass",
|
|
29
|
+
metavar="FILE",
|
|
30
|
+
help="Combined file: user:pass per line.",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument(
|
|
33
|
+
"--mode",
|
|
34
|
+
choices=("pitchfork", "clusterbomb"),
|
|
35
|
+
default="clusterbomb",
|
|
36
|
+
help="pitchfork: zip usernames with passwords; clusterbomb: all pairs (default).",
|
|
37
|
+
)
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"--timeout",
|
|
40
|
+
type=float,
|
|
41
|
+
default=DEFAULT_TIMEOUT,
|
|
42
|
+
help=f"Per-request timeout in seconds (default: {DEFAULT_TIMEOUT}).",
|
|
43
|
+
)
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--proxy",
|
|
46
|
+
metavar="URL",
|
|
47
|
+
help="HTTP(S) proxy for all requests (e.g. http://127.0.0.1:8080).",
|
|
48
|
+
)
|
|
49
|
+
p.add_argument(
|
|
50
|
+
"--quiet",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Less verbose output (no probe line, no summary).",
|
|
53
|
+
)
|
|
54
|
+
return p
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
parser = build_parser()
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
try:
|
|
61
|
+
code = run_attack(args)
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
print("\n[!] Interrupted.", file=sys.stderr)
|
|
64
|
+
code = 130
|
|
65
|
+
sys.exit(code)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
basicbuster/core.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from requests import Response
|
|
10
|
+
from requests.auth import HTTPBasicAuth
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_TIMEOUT = 15.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _proxy_dict(proxy: str | None) -> dict[str, str] | None:
|
|
17
|
+
if not proxy:
|
|
18
|
+
return None
|
|
19
|
+
return {"http": proxy, "https": proxy}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _read_lines(path: Path) -> list[str]:
|
|
23
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
24
|
+
return [line.strip() for line in text.splitlines() if line.strip()]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_basic_auth(
|
|
28
|
+
url: str,
|
|
29
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
30
|
+
proxies: dict[str, str] | None = None,
|
|
31
|
+
) -> bool:
|
|
32
|
+
try:
|
|
33
|
+
r = requests.get(
|
|
34
|
+
url,
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
allow_redirects=False,
|
|
37
|
+
proxies=proxies,
|
|
38
|
+
)
|
|
39
|
+
except requests.RequestException:
|
|
40
|
+
return False
|
|
41
|
+
if r.status_code != 401:
|
|
42
|
+
return False
|
|
43
|
+
www_auth = r.headers.get("WWW-Authenticate") or ""
|
|
44
|
+
return "basic" in www_auth.lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def try_login(
|
|
48
|
+
url: str,
|
|
49
|
+
username: str,
|
|
50
|
+
password: str,
|
|
51
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
52
|
+
proxies: dict[str, str] | None = None,
|
|
53
|
+
) -> requests.Response:
|
|
54
|
+
return requests.get(
|
|
55
|
+
url,
|
|
56
|
+
auth=HTTPBasicAuth(username, password),
|
|
57
|
+
timeout=timeout,
|
|
58
|
+
allow_redirects=False,
|
|
59
|
+
proxies=proxies,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def pitchfork(
|
|
64
|
+
url: str,
|
|
65
|
+
usernames: Iterable[str],
|
|
66
|
+
passwords: Iterable[str],
|
|
67
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
68
|
+
proxies: dict[str, str] | None = None,
|
|
69
|
+
) -> list[tuple[str, str, Response | None, str | None]]:
|
|
70
|
+
u = list(usernames)
|
|
71
|
+
p = list(passwords)
|
|
72
|
+
if not u or not p:
|
|
73
|
+
return []
|
|
74
|
+
n = min(len(u), len(p))
|
|
75
|
+
results: list[tuple[str, str, Response | None, str | None]] = []
|
|
76
|
+
for i in range(n):
|
|
77
|
+
try:
|
|
78
|
+
resp = try_login(url, u[i], p[i], timeout=timeout, proxies=proxies)
|
|
79
|
+
results.append((u[i], p[i], resp, None))
|
|
80
|
+
except requests.RequestException as e:
|
|
81
|
+
results.append((u[i], p[i], None, str(e)))
|
|
82
|
+
return results
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def clusterbomb(
|
|
86
|
+
url: str,
|
|
87
|
+
usernames: Iterable[str],
|
|
88
|
+
passwords: Iterable[str],
|
|
89
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
90
|
+
proxies: dict[str, str] | None = None,
|
|
91
|
+
) -> list[tuple[str, str, Response | None, str | None]]:
|
|
92
|
+
results: list[tuple[str, str, Response | None, str | None]] = []
|
|
93
|
+
for user, pw in itertools.product(usernames, passwords):
|
|
94
|
+
try:
|
|
95
|
+
resp = try_login(url, user, pw, timeout=timeout, proxies=proxies)
|
|
96
|
+
results.append((user, pw, resp, None))
|
|
97
|
+
except requests.RequestException as e:
|
|
98
|
+
results.append((user, pw, None, str(e)))
|
|
99
|
+
return results
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def userpass_mode(
|
|
103
|
+
url: str,
|
|
104
|
+
combos: Iterable[tuple[str, str]],
|
|
105
|
+
mode: str,
|
|
106
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
107
|
+
proxies: dict[str, str] | None = None,
|
|
108
|
+
) -> list[tuple[str, str, Response | None, str | None]]:
|
|
109
|
+
pairs = list(combos)
|
|
110
|
+
if not pairs:
|
|
111
|
+
return []
|
|
112
|
+
if mode == "pitchfork":
|
|
113
|
+
users = [u for u, _ in pairs]
|
|
114
|
+
pws = [p for _, p in pairs]
|
|
115
|
+
return pitchfork(url, users, pws, timeout=timeout, proxies=proxies)
|
|
116
|
+
users = [u for u, _ in pairs]
|
|
117
|
+
pws = [p for _, p in pairs]
|
|
118
|
+
return clusterbomb(url, users, pws, timeout=timeout, proxies=proxies)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _emit_success(user: str, pw: str, status: int) -> None:
|
|
122
|
+
print(f"[+] SUCCESS {user}:{pw} (HTTP {status})")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _emit_probe(ok: bool, url: str) -> None:
|
|
126
|
+
if ok:
|
|
127
|
+
print(f"[*] Target appears to use HTTP Basic Auth: {url}")
|
|
128
|
+
else:
|
|
129
|
+
print(f"[!] Target may not require HTTP Basic Auth (or unreachable): {url}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run_attack(args: Namespace) -> int:
|
|
133
|
+
url = args.url
|
|
134
|
+
timeout = float(args.timeout)
|
|
135
|
+
verbose = not args.quiet
|
|
136
|
+
proxies = _proxy_dict(args.proxy)
|
|
137
|
+
|
|
138
|
+
uses_basic = check_basic_auth(url, timeout=timeout, proxies=proxies)
|
|
139
|
+
if verbose:
|
|
140
|
+
_emit_probe(uses_basic, url)
|
|
141
|
+
|
|
142
|
+
results: list[tuple[str, str, Response | None, str | None]]
|
|
143
|
+
|
|
144
|
+
if args.userpass:
|
|
145
|
+
path = Path(args.userpass)
|
|
146
|
+
if not path.is_file():
|
|
147
|
+
print(f"[!] File not found: {path}")
|
|
148
|
+
return 2
|
|
149
|
+
lines = _read_lines(path)
|
|
150
|
+
combos: list[tuple[str, str]] = []
|
|
151
|
+
for line in lines:
|
|
152
|
+
if ":" not in line:
|
|
153
|
+
print(f"[!] Invalid line (expected user:pass): {line!r}")
|
|
154
|
+
return 2
|
|
155
|
+
user, _, rest = line.partition(":")
|
|
156
|
+
combos.append((user, rest))
|
|
157
|
+
results = userpass_mode(
|
|
158
|
+
url, combos, args.mode, timeout=timeout, proxies=proxies
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
if not args.username or not args.password:
|
|
162
|
+
print("[!] Provide --userpass or both --username and --password files.")
|
|
163
|
+
return 2
|
|
164
|
+
up = Path(args.username)
|
|
165
|
+
pp = Path(args.password)
|
|
166
|
+
if not up.is_file() or not pp.is_file():
|
|
167
|
+
print("[!] Username or password file not found.")
|
|
168
|
+
return 2
|
|
169
|
+
users = _read_lines(up)
|
|
170
|
+
pws = _read_lines(pp)
|
|
171
|
+
if args.mode == "pitchfork":
|
|
172
|
+
results = pitchfork(url, users, pws, timeout=timeout, proxies=proxies)
|
|
173
|
+
else:
|
|
174
|
+
results = clusterbomb(
|
|
175
|
+
url, users, pws, timeout=timeout, proxies=proxies
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
found = False
|
|
179
|
+
for user, pw, resp, err in results:
|
|
180
|
+
if resp is None:
|
|
181
|
+
if verbose and err:
|
|
182
|
+
print(f"[-] request failed for {user}:{pw} — {err}")
|
|
183
|
+
continue
|
|
184
|
+
if resp.status_code == 200:
|
|
185
|
+
_emit_success(user, pw, resp.status_code)
|
|
186
|
+
found = True
|
|
187
|
+
|
|
188
|
+
if not found and verbose:
|
|
189
|
+
print("[*] No credentials returned HTTP 200.")
|
|
190
|
+
|
|
191
|
+
return 0 if found else 1
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: basicbuster
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for HTTP Basic Authentication testing with wordlists.
|
|
5
|
+
Author: dhina016
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dhina016/basicbuster
|
|
8
|
+
Project-URL: Repository, https://github.com/dhina016/basicbuster
|
|
9
|
+
Project-URL: Issues, https://github.com/dhina016/basicbuster/issues
|
|
10
|
+
Keywords: security,http,basic-auth,pentest,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: requests>=2.28.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# basicbuster
|
|
32
|
+
|
|
33
|
+
**basicbuster** is a command-line tool for testing HTTP Basic Authentication with username and password wordlists. It can detect Basic Auth on a target URL, run pitchfork or clusterbomb-style attempts, and report credentials that return HTTP `200`.
|
|
34
|
+
|
|
35
|
+
## Disclaimer
|
|
36
|
+
|
|
37
|
+
Use **only** on systems and applications you are **explicitly authorized** to test. Unauthorized access to computer systems is illegal. The authors are not responsible for misuse.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Python 3.10+
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
After the package is [published on PyPI](https://pypi.org/project/basicbuster/), install from any machine:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install basicbuster
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
From a git checkout (repository root):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Editable install while developing:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Publishing to PyPI (maintainers)
|
|
64
|
+
|
|
65
|
+
The name **basicbuster** is available on PyPI. Two common options:
|
|
66
|
+
|
|
67
|
+
**A — GitHub Release (recommended)**
|
|
68
|
+
After you [enable trusted publishing](https://docs.pypi.org/trusted-publishers/) for this repo on PyPI:
|
|
69
|
+
|
|
70
|
+
1. In PyPI: your account → **Publishing** → add a pending publisher for `dhina016/basicbuster`, workflow file `publish.yml`.
|
|
71
|
+
2. On GitHub: create a **Release** (tag `v0.1.0` or similar) and publish it. The **Publish to PyPI** workflow uploads the built sdist and wheel automatically.
|
|
72
|
+
|
|
73
|
+
**B — Manual upload**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install build twine
|
|
77
|
+
python -m build
|
|
78
|
+
twine upload dist/*
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use a [PyPI API token](https://pypi.org/help/#apitoken) when `twine` prompts for credentials (or set `TWINE_USERNAME=__token__` and `TWINE_PASSWORD=pypi-...`).
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
basicbuster --url URL (--username FILE --password FILE | --userpass FILE) [--mode MODE] [--timeout SECONDS] [--proxy URL]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **`--url`**: Target URL (resource protected by HTTP Basic Auth).
|
|
90
|
+
- **`--username` / `--password`**: Separate wordlists, one value per line.
|
|
91
|
+
- **`--userpass`**: Combined file with `user:pass` per line (password may contain `:`; only the first `:` separates user and password).
|
|
92
|
+
- **`--mode`**:
|
|
93
|
+
- `pitchfork` — pair `username[i]` with `password[i]`.
|
|
94
|
+
- `clusterbomb` — try every username with every password (default).
|
|
95
|
+
- **`--timeout`**: Request timeout in seconds (default: `15`).
|
|
96
|
+
- **`--quiet`**: Minimal output.
|
|
97
|
+
- **`--proxy`**: Forward all traffic through an HTTP(S) proxy (e.g. Burp: `http://127.0.0.1:8080`).
|
|
98
|
+
|
|
99
|
+
Successful guesses print as:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
[+] SUCCESS user:pass (HTTP 200)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Examples
|
|
106
|
+
|
|
107
|
+
Separate wordlists, clusterbomb (default):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
basicbuster --url https://example.com/secret --username users.txt --password passwords.txt
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Pitchfork (zip by line index):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
basicbuster --url https://example.com/secret --username users.txt --password passwords.txt --mode pitchfork
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Combined user:pass file:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
basicbuster --url https://example.com/secret --userpass combo.txt
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Through a local intercepting proxy:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
basicbuster --url https://example.com/secret --userpass combo.txt --proxy http://127.0.0.1:8080
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
basicbuster/__init__.py,sha256=MZYK5fsWzsWA-Bi1q-BzenLsLWqDpT788_4qupZusCw,68
|
|
2
|
+
basicbuster/cli.py,sha256=2EZHtgedsYvDHVUi5xtm4renOJyp-rs4F-f-f0EPzkE,1892
|
|
3
|
+
basicbuster/core.py,sha256=ECGDFgs1uJS7ZcE1fKeAAihzZj7dvB2p-UdG8Z_zK4U,5738
|
|
4
|
+
basicbuster-0.1.0.dist-info/licenses/LICENSE,sha256=zr5k77QlLas2jRWWi2fUzrL_ThQhwTCuU1Fo_RaK8hA,1065
|
|
5
|
+
basicbuster-0.1.0.dist-info/METADATA,sha256=87W6kd_bCJmY4U7GJD9lM-Mw0e5jwnOSJjmSiPiOpTI,4209
|
|
6
|
+
basicbuster-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
basicbuster-0.1.0.dist-info/entry_points.txt,sha256=OnGO_pJCVCmkGmdqOTxNWE0HgDBGiCRuPeKFARetiIg,53
|
|
8
|
+
basicbuster-0.1.0.dist-info/top_level.txt,sha256=mNWro0o-3D7_0Q4iwctz4bf50-AljZI5y7b2QSvvU6I,12
|
|
9
|
+
basicbuster-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dhina016
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
basicbuster
|