Skip to content

Commit e4e9d60

Browse files
committed
Merge testing: zerodays PoC + per-CVE tagging, launcher SVG icons
2 parents d67b7a9 + 1188453 commit e4e9d60

4 files changed

Lines changed: 237 additions & 30 deletions

File tree

internal/api/ui/pages/module-registry.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -509,13 +509,27 @@
509509
},
510510
detail(r) {
511511
const target = s(r.host || r.target || '');
512-
return buildFields([
513-
['Target', target, { isLink: true }],
514-
['Finding', s(r.title || r.finding || ''), { full: true }],
515-
['Module', s(r.module || '')],
516-
['Severity', s(r.severity)],
517-
['URL', s(r.url || r.link || ''), { isLink: true }],
518-
]);
512+
const raw = (r.raw && typeof r.raw === 'object') ? r.raw : {};
513+
const fields = [
514+
['Target', target, { isLink: true }],
515+
['Finding', s(r.title || r.finding || ''), { full: true }],
516+
['Module', s(r.module || '')],
517+
['Severity', s(r.severity)],
518+
['URL', s(r.url || r.link || ''), { isLink: true }],
519+
];
520+
// Enriched findings (e.g. zerodays React2Shell / MongoDB) carry an explicit
521+
// CVE plus the request/response PoC. Show which bug it is + the proof.
522+
const cve = s(raw.cve || '');
523+
const req = s(raw.request || '');
524+
const resp = s(raw.response || '');
525+
if (cve || req || resp) {
526+
fields.splice(1, 0, ['Vulnerability', s(cve || raw['template-id'] || ''), { full: true }]);
527+
if (raw.type) fields.push(['Variant', s(raw.type)]);
528+
if (raw.status_code != null && s(raw.status_code) !== '') fields.push(['Response status', s(raw.status_code)]);
529+
if (req) fields.push(['PoC — Request', req, { full: true, code: true }]);
530+
if (resp) fields.push(['PoC — Response', resp, { full: true, code: true }]);
531+
}
532+
return buildFields(fields);
519533
},
520534
},
521535

internal/api/ui/pages/scans-page.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@
275275
}
276276
html += `<div class="scan-launcher">
277277
<div class="scan-launcher-head">
278-
<div class="scan-launcher-title"><span class="scan-launcher-icon"></span> Quick Scan Launcher</div>
278+
<div class="scan-launcher-title"><span class="scan-launcher-icon"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-3px"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span> Quick Scan Launcher</div>
279279
<div class="scan-launcher-sub">Pick a scan type, point it at a target, tune the flags, then launch.</div>
280280
</div>
281281
<div class="launcher-grid">
@@ -325,7 +325,7 @@
325325
<textarea id="launch-target-list" class="launch-target-list" placeholder="one target per line" style="display:none">${window.esc(lUI.targetList || '')}</textarea>
326326
</div>
327327
<div class="launcher-field launcher-field-action">
328-
<button class="btn-primary" id="launch-btn"><span class="launch-btn-icon"></span><span>Launch</span></button>
328+
<button class="btn-primary" id="launch-btn"><span class="launch-btn-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="vertical-align:-1px"><polygon points="5 3 19 12 5 21 5 3"/></svg></span><span>Launch</span></button>
329329
</div>
330330
</div>
331331
<div id="launch-help" class="launch-help"></div>
@@ -343,7 +343,7 @@
343343
</div>`;
344344
html += `<div class="scans-toolbar">
345345
<div class="scans-toolbar-search">
346-
<span class="scans-toolbar-search-icon">🔍</span>
346+
<span class="scans-toolbar-search-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></span>
347347
<input type="text" id="scan-search-input" class="search-input" placeholder="Search targets or scan types…" value="${window.esc(sUI.search)}" autocomplete="off" />
348348
</div>
349349
<select id="scan-type-filter" class="input scans-toolbar-select">

internal/scanner/zerodays/zerodays.go

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ type React2ShellFinding struct {
5858
URL string
5959
Type string // e.g., "normal", "waf-bypass", "vercel-waf", "dos", "source-exposure"
6060
Severity string
61+
// PoC evidence captured by next88 (the proof, previously discarded).
62+
Request string // exact request next88 sent
63+
Response string // response that matched the vulnerable signature
64+
StatusCode int // HTTP status of the matched response
6165
}
6266

6367
// MongoDBFinding represents a MongoDB CVE-2025-14847 vulnerability finding
@@ -157,6 +161,10 @@ func Run(opts Options) (*Result, error) {
157161
Module string `json:"module"`
158162
Type string `json:"type,omitempty"`
159163
Finding string `json:"finding"`
164+
CVE string `json:"cve,omitempty"` // which bug this finding is
165+
StatusCode int `json:"status_code,omitempty"` // PoC: response status
166+
Request string `json:"request,omitempty"` // PoC: exact request sent
167+
Response string `json:"response,omitempty"` // PoC: matched response
160168
}
161169
findings := make([]zerodayFinding, 0, result.TotalVulnerable)
162170
seen := make(map[string]struct{})
@@ -185,7 +193,11 @@ func Run(opts Options) (*Result, error) {
185193
Severity: sev,
186194
Module: "zerodays",
187195
Type: vType,
188-
Finding: "Potential React2Shell exploitation path detected",
196+
Finding: react2ShellFindingText(vType),
197+
CVE: "CVE-2025-55182",
198+
StatusCode: v.StatusCode,
199+
Request: capPoC(v.Request),
200+
Response: capPoC(v.Response),
189201
})
190202
}
191203

@@ -206,7 +218,10 @@ func Run(opts Options) (*Result, error) {
206218
Severity: "high",
207219
Module: "zerodays",
208220
Type: "mongodb-memory-leak",
209-
Finding: fmt.Sprintf("Leaked %d bytes from MongoDB response", len(v.LeakedData)),
221+
Finding: fmt.Sprintf("MongoDB memory leak (CVE-2025-14847) — leaked %d bytes", len(v.LeakedData)),
222+
CVE: "CVE-2025-14847",
223+
Request: fmt.Sprintf("OP_MSG BSON probe with an oversized memory-leak request → %s", targetAddr),
224+
Response: previewLeak(v.LeakedData),
210225
})
211226
}
212227

@@ -373,12 +388,15 @@ func checkReact2Shell(opts Options) ([]React2ShellFinding, int, error) {
373388
return nil, 0, fmt.Errorf("next88 scan failed: %w", err)
374389
}
375390

376-
// Convert results to findings
377-
for url, vulnType := range results {
391+
// Convert results to findings (carrying the request/response PoC evidence)
392+
for url, res := range results {
378393
findings = append(findings, React2ShellFinding{
379-
URL: url,
380-
Type: vulnType,
381-
Severity: "HIGH", // React2Shell is a high severity RCE
394+
URL: url,
395+
Type: res.VulnType,
396+
Severity: "HIGH", // React2Shell is a high severity RCE
397+
Request: next88Request(res),
398+
Response: next88Response(res),
399+
StatusCode: next88StatusCode(res),
382400
})
383401
}
384402

@@ -389,7 +407,8 @@ func checkReact2Shell(opts Options) ([]React2ShellFinding, int, error) {
389407
dosResults, err := runNext88Scan(dosCtx, hosts, []string{"-dos-test", "-dos-requests", "100"}, opts.Threads, opts.Silent)
390408
dosCancel()
391409
if err == nil {
392-
for url, vulnType := range dosResults {
410+
for url, res := range dosResults {
411+
vulnType := res.VulnType
393412
// Check if already exists
394413
found := false
395414
for i := range findings {
@@ -403,9 +422,12 @@ func checkReact2Shell(opts Options) ([]React2ShellFinding, int, error) {
403422
}
404423
if !found {
405424
findings = append(findings, React2ShellFinding{
406-
URL: url,
407-
Type: vulnType,
408-
Severity: "HIGH",
425+
URL: url,
426+
Type: vulnType,
427+
Severity: "HIGH",
428+
Request: next88Request(res),
429+
Response: next88Response(res),
430+
StatusCode: next88StatusCode(res),
409431
})
410432
}
411433
}
@@ -419,7 +441,8 @@ func checkReact2Shell(opts Options) ([]React2ShellFinding, int, error) {
419441
sourceResults, err := runNext88Scan(sourceCtx, hosts, []string{"-check-source-exposure"}, opts.Threads, opts.Silent)
420442
sourceCancel()
421443
if err == nil {
422-
for url, vulnType := range sourceResults {
444+
for url, res := range sourceResults {
445+
vulnType := res.VulnType
423446
found := false
424447
for i := range findings {
425448
if findings[i].URL == url {
@@ -432,9 +455,12 @@ func checkReact2Shell(opts Options) ([]React2ShellFinding, int, error) {
432455
}
433456
if !found {
434457
findings = append(findings, React2ShellFinding{
435-
URL: url,
436-
Type: vulnType,
437-
Severity: "MEDIUM",
458+
URL: url,
459+
Type: vulnType,
460+
Severity: "MEDIUM",
461+
Request: next88Request(res),
462+
Response: next88Response(res),
463+
StatusCode: next88StatusCode(res),
438464
})
439465
}
440466
}
@@ -720,9 +746,10 @@ func buildMalformedMongoDBPacket(leakSize int) ([]byte, error) {
720746
return append(header, opCompressed...), nil
721747
}
722748

723-
// runNext88Scan runs next88 scan and returns results with threading support
724-
func runNext88Scan(ctx context.Context, hosts []string, args []string, requestedThreads int, silent bool) (map[string]string, error) {
725-
results := make(map[string]string)
749+
// runNext88Scan runs next88 scan and returns the full per-host results (including
750+
// the request/response PoC evidence) with threading support.
751+
func runNext88Scan(ctx context.Context, hosts []string, args []string, requestedThreads int, silent bool) (map[string]next88.ScanResult, error) {
752+
results := make(map[string]next88.ScanResult)
726753
var mu sync.Mutex
727754

728755
// Determine thread count
@@ -774,18 +801,103 @@ func runNext88Scan(ctx context.Context, hosts []string, args []string, requested
774801
return nil, fmt.Errorf("next88 scan failed: %w", err)
775802
}
776803

777-
// Convert results to map
804+
// Convert results to map, keeping the full result (request/response PoC).
778805
for _, result := range scanResults {
779806
if result.Vulnerable != nil && *result.Vulnerable {
780807
mu.Lock()
781-
results[result.Host] = result.VulnType
808+
results[result.Host] = result
782809
mu.Unlock()
783810
}
784811
}
785812

786813
return results, nil
787814
}
788815

816+
// next88StatusCode safely dereferences the optional status code.
817+
func next88StatusCode(r next88.ScanResult) int {
818+
if r.StatusCode != nil {
819+
return *r.StatusCode
820+
}
821+
return 0
822+
}
823+
824+
// next88Request/next88Response prefer the full request/response, falling back to the body-only capture.
825+
func next88Request(r next88.ScanResult) string {
826+
if strings.TrimSpace(r.Request) != "" {
827+
return r.Request
828+
}
829+
return r.RequestBody
830+
}
831+
832+
func next88Response(r next88.ScanResult) string {
833+
if strings.TrimSpace(r.Response) != "" {
834+
return r.Response
835+
}
836+
return r.ResponseBody
837+
}
838+
839+
// react2ShellFindingText builds a finding string that names the specific
840+
// sub-check(s) that fired (RCE exploitation, DoS, source-code exposure), so the
841+
// dashboard clearly tags which React2Shell issue was detected. vType may be a
842+
// comma-joined list when a single host matched multiple checks.
843+
func react2ShellFindingText(vType string) string {
844+
v := strings.ToLower(vType)
845+
var issues []string
846+
847+
hasRCE := strings.Contains(v, "normal") || strings.Contains(v, "waf") || strings.Contains(v, "vercel")
848+
if hasRCE || v == "" {
849+
rce := "RCE exploitation path"
850+
if strings.Contains(v, "waf") || strings.Contains(v, "vercel") {
851+
rce += " (WAF bypass)"
852+
}
853+
issues = append(issues, rce)
854+
}
855+
if strings.Contains(v, "dos") {
856+
issues = append(issues, "denial-of-service path")
857+
}
858+
if strings.Contains(v, "source") {
859+
issues = append(issues, "source-code / source-map exposure")
860+
}
861+
if len(issues) == 0 {
862+
issues = append(issues, "exploitation path")
863+
}
864+
return "React2Shell (CVE-2025-55182) — " + strings.Join(issues, " + ") + " detected"
865+
}
866+
867+
// capPoC trims and truncates a request/response body so the results JSON stays
868+
// small even when a host returns a large HTML error page.
869+
func capPoC(s string) string {
870+
const max = 8000
871+
s = strings.TrimSpace(s)
872+
if len(s) > max {
873+
return s[:max] + fmt.Sprintf("\n… [truncated, %d total bytes]", len(s))
874+
}
875+
return s
876+
}
877+
878+
// previewLeak renders leaked MongoDB bytes as a safe, capped printable preview
879+
// (non-printable bytes shown as '.') for use as PoC evidence.
880+
func previewLeak(b []byte) string {
881+
const max = 2048
882+
n := len(b)
883+
if n > max {
884+
b = b[:max]
885+
}
886+
var sb strings.Builder
887+
for _, c := range b {
888+
if c == '\n' || c == '\t' || (c >= 0x20 && c < 0x7f) {
889+
sb.WriteByte(c)
890+
} else {
891+
sb.WriteByte('.')
892+
}
893+
}
894+
out := sb.String()
895+
if n > max {
896+
out += fmt.Sprintf("\n… [truncated, %d total bytes leaked]", n)
897+
}
898+
return out
899+
}
900+
789901
// contains checks if a string slice contains a value
790902
func contains(slice []string, value string) bool {
791903
for _, v := range slice {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package zerodays
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/h0tak88r/AutoAR/internal/tools/next88"
8+
)
9+
10+
func TestReact2ShellFindingText(t *testing.T) {
11+
cases := map[string]string{
12+
"normal": "RCE exploitation path",
13+
"": "RCE exploitation path",
14+
"waf-bypass": "WAF bypass",
15+
"vercel-waf-bypass": "WAF bypass",
16+
"dos-test": "denial-of-service path",
17+
"source-exposure": "source-code / source-map exposure",
18+
"normal,dos-test": "RCE exploitation path + denial-of-service path",
19+
"normal,source-exposure": "source-code / source-map exposure",
20+
}
21+
for vtype, want := range cases {
22+
got := react2ShellFindingText(vtype)
23+
if !strings.Contains(got, "CVE-2025-55182") {
24+
t.Errorf("%q: missing CVE in %q", vtype, got)
25+
}
26+
if !strings.Contains(got, want) {
27+
t.Errorf("react2ShellFindingText(%q) = %q, want substring %q", vtype, got, want)
28+
}
29+
}
30+
// dos-only must NOT claim an RCE path
31+
if strings.Contains(react2ShellFindingText("dos-test"), "RCE") {
32+
t.Errorf("dos-only finding should not mention RCE")
33+
}
34+
}
35+
36+
func TestCapPoC(t *testing.T) {
37+
if got := capPoC(" hello "); got != "hello" {
38+
t.Fatalf("capPoC trim: got %q", got)
39+
}
40+
big := strings.Repeat("a", 9000)
41+
out := capPoC(big)
42+
if !strings.Contains(out, "truncated") {
43+
t.Fatalf("capPoC should mark truncation for oversized input")
44+
}
45+
if len(out) > 8100 {
46+
t.Fatalf("capPoC did not cap length: got %d", len(out))
47+
}
48+
}
49+
50+
func TestPreviewLeak(t *testing.T) {
51+
// 'A', NUL, 'B', newline, tab → printable kept, non-printable shown as '.'
52+
out := previewLeak([]byte{0x41, 0x00, 0x42, 0x0a, 0x09})
53+
if out != "A.B\n\t" {
54+
t.Fatalf("previewLeak: got %q want %q", out, "A.B\n\t")
55+
}
56+
}
57+
58+
func TestNext88EvidenceHelpers(t *testing.T) {
59+
code := 500
60+
r := next88.ScanResult{
61+
StatusCode: &code,
62+
Request: "POST / HTTP/1.1",
63+
Response: "HTTP/1.1 500",
64+
RequestBody: "fallback-req",
65+
ResponseBody: "fallback-resp",
66+
}
67+
if next88StatusCode(r) != 500 {
68+
t.Fatalf("status code: got %d", next88StatusCode(r))
69+
}
70+
if next88Request(r) != "POST / HTTP/1.1" {
71+
t.Fatalf("request prefers full Request: got %q", next88Request(r))
72+
}
73+
// Falls back to *Body when the full capture is empty.
74+
r2 := next88.ScanResult{RequestBody: "only-body", ResponseBody: "only-resp"}
75+
if next88Request(r2) != "only-body" || next88Response(r2) != "only-resp" {
76+
t.Fatalf("fallback to body failed: req=%q resp=%q", next88Request(r2), next88Response(r2))
77+
}
78+
if next88StatusCode(next88.ScanResult{}) != 0 {
79+
t.Fatalf("nil status code should be 0")
80+
}
81+
}

0 commit comments

Comments
 (0)