@@ -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
790902func contains (slice []string , value string ) bool {
791903 for _ , v := range slice {
0 commit comments