1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Deserialize)]
13#[non_exhaustive]
14pub struct NewTaskResponse {
15 pub success: bool,
17 pub taskid: Option<String>,
19 pub message: Option<String>,
21}
22
23#[derive(Debug, Clone, Deserialize)]
25#[non_exhaustive]
26pub struct BasicResponse {
27 pub success: bool,
29 pub message: Option<String>,
31}
32
33#[derive(Debug, Clone, Deserialize)]
35#[non_exhaustive]
36pub struct StatusResponse {
37 pub success: bool,
39 pub status: Option<String>,
41 pub returncode: Option<i32>,
43 pub message: Option<String>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49#[non_exhaustive]
50pub struct SqlmapDataChunk {
51 pub r#type: i32,
53 pub value: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Deserialize)]
59#[non_exhaustive]
60pub struct DataResponse {
61 pub success: bool,
63 pub data: Option<Vec<SqlmapDataChunk>>,
65 pub error: Option<Vec<String>>,
67 pub message: Option<String>,
69}
70
71#[derive(Debug, Clone, Deserialize, Serialize)]
73#[non_exhaustive]
74pub struct LogEntry {
75 pub message: String,
77 pub level: String,
79 pub time: String,
81}
82
83#[derive(Debug, Clone, Deserialize)]
85#[non_exhaustive]
86pub struct LogResponse {
87 pub success: bool,
89 pub log: Option<Vec<LogEntry>>,
91 pub message: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct SqlmapFinding {
101 pub parameter: String,
103 pub vulnerability_type: String,
105 pub payload: String,
107 pub details: serde_json::Value,
109}
110
111impl SqlmapFinding {
112 pub fn new(
114 parameter: impl Into<String>,
115 vulnerability_type: impl Into<String>,
116 payload: impl Into<String>,
117 details: serde_json::Value,
118 ) -> Self {
119 Self {
120 parameter: parameter.into(),
121 vulnerability_type: vulnerability_type.into(),
122 payload: payload.into(),
123 details,
124 }
125 }
126}
127
128impl fmt::Display for SqlmapFinding {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(
131 f,
132 "[SQLi] {vtype} on param '{param}' - payload: {payload}",
133 vtype = self.vulnerability_type,
134 param = self.parameter,
135 payload = self.payload,
136 )
137 }
138}
139
140fn is_confirmed_finding(vulnerability_type: &str, payload: &str) -> bool {
141 !payload.is_empty() && vulnerability_type != "unknown"
142}
143
144fn technique_entries(data: &serde_json::Value) -> Vec<&serde_json::Value> {
145 if let Some(arr) = data.as_array() {
146 return arr.iter().collect();
147 }
148 if let Some(map) = data.as_object() {
149 let mut entries: Vec<_> = map.iter().collect();
150 entries.sort_by(
151 |(a, _), (b, _)| match (a.parse::<u64>(), b.parse::<u64>()) {
152 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
153 (Ok(_), Err(_)) => std::cmp::Ordering::Less,
154 (Err(_), Ok(_)) => std::cmp::Ordering::Greater,
155 (Err(_), Err(_)) => a.cmp(b),
156 },
157 );
158 return entries.into_iter().map(|(_, v)| v).collect();
159 }
160 vec![]
161}
162
163fn push_technique_finding(
164 findings: &mut Vec<SqlmapFinding>,
165 parameter: &str,
166 tech: &serde_json::Value,
167) {
168 let Some(tech_obj) = tech.as_object() else {
169 return;
170 };
171 let vulnerability_type = tech_obj
172 .get("technique")
173 .or_else(|| tech_obj.get("title"))
174 .and_then(|v| v.as_str())
175 .unwrap_or("unknown")
176 .to_string();
177 let payload = tech_obj
178 .get("payload")
179 .and_then(|v| v.as_str())
180 .unwrap_or("")
181 .to_string();
182 if is_confirmed_finding(&vulnerability_type, &payload) {
183 findings.push(SqlmapFinding {
184 parameter: parameter.to_string(),
185 vulnerability_type,
186 payload,
187 details: tech.clone(),
188 });
189 }
190}
191
192impl DataResponse {
193 pub fn findings(&self) -> Vec<SqlmapFinding> {
199 let Some(ref chunks) = self.data else {
200 return vec![];
201 };
202 let mut findings = Vec::new();
203
204 for chunk in chunks {
205 if chunk.r#type != 1 {
206 continue;
207 }
208 let Some(arr) = chunk.value.as_array() else {
209 continue;
210 };
211 for item in arr {
212 let Some(obj) = item.as_object() else {
213 continue;
214 };
215 let parameter = obj
216 .get("parameter")
217 .and_then(|v| v.as_str())
218 .unwrap_or("unknown")
219 .to_string();
220
221 if let Some(data_val) = obj.get("data") {
222 if data_val.is_array() || data_val.is_object() {
223 for tech in technique_entries(data_val) {
224 push_technique_finding(&mut findings, ¶meter, tech);
225 }
226 continue;
227 }
228 }
229
230 let vulnerability_type = obj
232 .get("type")
233 .or_else(|| obj.get("title"))
234 .and_then(|v| v.as_str())
235 .unwrap_or("unknown")
236 .to_string();
237 let payload = obj
238 .get("payload")
239 .and_then(|v| v.as_str())
240 .unwrap_or("")
241 .to_string();
242 if is_confirmed_finding(&vulnerability_type, &payload) {
243 findings.push(SqlmapFinding {
244 parameter,
245 vulnerability_type,
246 payload,
247 details: item.clone(),
248 });
249 }
250 }
251 }
252
253 findings
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261#[non_exhaustive]
262pub enum OutputFormat {
263 Json,
265 JsonPretty,
267 Csv,
269 Markdown,
271 Plain,
273}
274
275pub fn format_findings(findings: &[SqlmapFinding], format: OutputFormat) -> String {
277 match format {
278 OutputFormat::Json => match serde_json::to_string(findings) {
279 Ok(json) => json,
280 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
281 },
282 OutputFormat::JsonPretty => match serde_json::to_string_pretty(findings) {
283 Ok(json) => json,
284 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
285 },
286 OutputFormat::Csv => {
287 let mut buf = String::from("parameter,vulnerability_type,payload\n");
288 for f in findings {
289 buf.push_str(&format!(
290 "{},{},{}\n",
291 csv_escape(&f.parameter),
292 csv_escape(&f.vulnerability_type),
293 csv_escape(&f.payload),
294 ));
295 }
296 buf
297 }
298 OutputFormat::Markdown => {
299 if findings.is_empty() {
300 return "No SQL injection findings.\n".to_string();
301 }
302 let mut buf = String::from("| Parameter | Type | Payload |\n");
303 buf.push_str("|-----------|------|----------|\n");
304 for f in findings {
305 buf.push_str(&format!(
306 "| `{}` | `{}` | `{}` |\n",
307 markdown_escape(&f.parameter),
308 markdown_escape(&f.vulnerability_type),
309 markdown_escape(&f.payload),
310 ));
311 }
312 buf
313 }
314 OutputFormat::Plain => {
315 if findings.is_empty() {
316 return "No SQL injection findings detected.\n".to_string();
317 }
318 let mut buf = format!("=== {} SQLi Finding(s) ===\n\n", findings.len());
319 for (i, f) in findings.iter().enumerate() {
320 buf.push_str(&format!(
321 "#{} {} on param '{}'\n Payload: {}\n\n",
322 i + 1,
323 f.vulnerability_type,
324 f.parameter,
325 f.payload,
326 ));
327 }
328 buf
329 }
330 }
331}
332
333fn csv_escape(value: &str) -> String {
335 if value.contains(',') || value.contains('"') || value.contains('\n') {
336 format!("\"{}\"", value.replace('"', "\"\""))
337 } else {
338 value.to_string()
339 }
340}
341
342fn markdown_escape(value: &str) -> String {
344 value.replace('|', "\\|")
345}
346
347#[derive(Debug, Clone, Serialize, Default)]
368#[non_exhaustive]
369pub struct SqlmapOptions {
370 #[serde(skip_serializing_if = "Option::is_none")]
373 pub url: Option<String>,
374
375 #[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
377 pub test_parameter: Option<String>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
382 pub dbms: Option<String>,
383
384 #[serde(rename = "technique", skip_serializing_if = "Option::is_none")]
386 pub tech: Option<String>,
387
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub level: Option<i32>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub risk: Option<i32>,
395
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub string: Option<String>,
399
400 #[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
402 pub not_string: Option<String>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub regexp: Option<String>,
407
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub code: Option<i32>,
411
412 #[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
414 pub text_only: Option<bool>,
415
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub titles: Option<bool>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
423 pub cookie: Option<String>,
424
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub headers: Option<String>,
428
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub method: Option<String>,
432
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub data: Option<String>,
436
437 #[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
439 pub random_agent: Option<bool>,
440
441 #[serde(skip_serializing_if = "Option::is_none")]
443 pub proxy: Option<String>,
444
445 #[serde(skip_serializing_if = "Option::is_none")]
448 pub prefix: Option<String>,
449
450 #[serde(skip_serializing_if = "Option::is_none")]
452 pub suffix: Option<String>,
453
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub tamper: Option<String>,
457
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub skip: Option<String>,
461
462 #[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
464 pub skip_static: Option<bool>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
469 pub threads: Option<i32>,
470
471 #[serde(skip_serializing_if = "Option::is_none")]
473 pub verbose: Option<i32>,
474
475 #[serde(skip_serializing_if = "Option::is_none")]
477 pub batch: Option<bool>,
478
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub retries: Option<i32>,
482
483 #[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
486 pub get_dbs: Option<bool>,
487
488 #[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
490 pub get_tables: Option<bool>,
491
492 #[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
494 pub get_columns: Option<bool>,
495
496 #[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
498 pub get_users: Option<bool>,
499
500 #[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
502 pub get_passwords: Option<bool>,
503
504 #[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
506 pub get_privileges: Option<bool>,
507
508 #[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
510 pub is_dba: Option<bool>,
511
512 #[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
514 pub current_user: Option<bool>,
515
516 #[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
518 pub current_db: Option<bool>,
519
520 #[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
522 pub dump_all: Option<bool>,
523
524 #[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
526 pub dump_table: Option<bool>,
527
528 #[serde(skip_serializing_if = "Option::is_none")]
530 pub search: Option<bool>,
531
532 #[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
535 pub os_shell: Option<bool>,
536
537 #[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
539 pub sql_shell: Option<bool>,
540
541 #[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
543 pub file_read: Option<String>,
544
545 #[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
547 pub file_write: Option<String>,
548
549 #[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
551 pub file_dest: Option<String>,
552
553 #[serde(skip_serializing_if = "Option::is_none")]
556 pub tor: Option<bool>,
557
558 #[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
560 pub tor_port: Option<i32>,
561
562 #[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
564 pub tor_type: Option<String>,
565
566 #[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
569 pub crawl_depth: Option<i32>,
570
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub scope: Option<String>,
574
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub forms: Option<bool>,
578
579 #[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
582 pub second_url: Option<String>,
583}
584
585#[derive(Debug, Clone, Default)]
590pub struct SqlmapOptionsBuilder {
591 inner: SqlmapOptions,
592}
593
594impl SqlmapOptions {
595 pub fn builder() -> SqlmapOptionsBuilder {
597 SqlmapOptionsBuilder::default()
598 }
599}
600
601macro_rules! builder_method {
603 ($name:ident, $field:ident, String) => {
604 pub fn $name(mut self, value: impl Into<String>) -> Self {
606 self.inner.$field = Some(value.into());
607 self
608 }
609 };
610 ($name:ident, $field:ident, bool) => {
611 pub fn $name(mut self, value: bool) -> Self {
613 self.inner.$field = Some(value);
614 self
615 }
616 };
617 ($name:ident, $field:ident, i32) => {
618 pub fn $name(mut self, value: i32) -> Self {
620 self.inner.$field = Some(value);
621 self
622 }
623 };
624}
625
626impl SqlmapOptionsBuilder {
627 builder_method!(url, url, String);
629 builder_method!(test_parameter, test_parameter, String);
630
631 builder_method!(dbms, dbms, String);
633 builder_method!(tech, tech, String);
634 builder_method!(level, level, i32);
635 builder_method!(risk, risk, i32);
636 builder_method!(string, string, String);
637 builder_method!(not_string, not_string, String);
638 builder_method!(regexp, regexp, String);
639 builder_method!(code, code, i32);
640 builder_method!(text_only, text_only, bool);
641 builder_method!(titles, titles, bool);
642
643 builder_method!(cookie, cookie, String);
645 builder_method!(headers, headers, String);
646 builder_method!(method, method, String);
647 builder_method!(data, data, String);
648 builder_method!(random_agent, random_agent, bool);
649 builder_method!(proxy, proxy, String);
650
651 builder_method!(prefix, prefix, String);
653 builder_method!(suffix, suffix, String);
654 builder_method!(tamper, tamper, String);
655 builder_method!(skip, skip, String);
656 builder_method!(skip_static, skip_static, bool);
657
658 builder_method!(threads, threads, i32);
660 builder_method!(verbose, verbose, i32);
661 builder_method!(batch, batch, bool);
662 builder_method!(retries, retries, i32);
663
664 builder_method!(get_dbs, get_dbs, bool);
666 builder_method!(get_tables, get_tables, bool);
667 builder_method!(get_columns, get_columns, bool);
668 builder_method!(get_users, get_users, bool);
669 builder_method!(get_passwords, get_passwords, bool);
670 builder_method!(get_privileges, get_privileges, bool);
671 builder_method!(is_dba, is_dba, bool);
672 builder_method!(current_user, current_user, bool);
673 builder_method!(current_db, current_db, bool);
674 builder_method!(dump_all, dump_all, bool);
675 builder_method!(dump_table, dump_table, bool);
676 builder_method!(search, search, bool);
677
678 builder_method!(os_shell, os_shell, bool);
680 builder_method!(sql_shell, sql_shell, bool);
681 builder_method!(file_read, file_read, String);
682 builder_method!(file_write, file_write, String);
683 builder_method!(file_dest, file_dest, String);
684
685 builder_method!(tor, tor, bool);
687 builder_method!(tor_port, tor_port, i32);
688 builder_method!(tor_type, tor_type, String);
689
690 builder_method!(crawl_depth, crawl_depth, i32);
692 builder_method!(scope, scope, String);
693 builder_method!(forms, forms, bool);
694
695 builder_method!(second_url, second_url, String);
697
698 pub fn build(self) -> SqlmapOptions {
700 self.inner
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 #[test]
709 fn empty_data_response_gives_no_findings() {
710 let resp = DataResponse {
711 success: true,
712 data: None,
713 error: None,
714 message: None,
715 };
716 assert!(resp.findings().is_empty());
717 }
718
719 #[test]
720 fn type_0_chunks_ignored() {
721 let resp = DataResponse {
722 success: true,
723 data: Some(vec![SqlmapDataChunk {
724 r#type: 0,
725 value: serde_json::json!("log message"),
726 }]),
727 error: None,
728 message: None,
729 };
730 assert!(resp.findings().is_empty());
731 }
732
733 #[test]
734 fn type_1_nested_techniques_expanded() {
735 let resp = DataResponse {
736 success: true,
737 data: Some(vec![SqlmapDataChunk {
738 r#type: 1,
739 value: serde_json::json!([{
740 "place": "GET",
741 "parameter": "id",
742 "data": [
743 {
744 "title": "AND boolean-based blind - WHERE or HAVING clause",
745 "payload": "id=1 AND 8888=8888"
746 },
747 {
748 "technique": "time-based blind",
749 "payload": "id=1 AND SLEEP(5)"
750 }
751 ]
752 }]),
753 }]),
754 error: None,
755 message: None,
756 };
757 let findings = resp.findings();
758 assert_eq!(findings.len(), 2);
759 assert_eq!(findings[0].parameter, "id");
760 assert_eq!(
761 findings[0].vulnerability_type,
762 "AND boolean-based blind - WHERE or HAVING clause"
763 );
764 assert_eq!(findings[0].payload, "id=1 AND 8888=8888");
765 assert_eq!(findings[1].vulnerability_type, "time-based blind");
766 assert_eq!(findings[1].payload, "id=1 AND SLEEP(5)");
767 }
768
769 #[test]
770 fn type_1_chunk_parsed_as_finding() {
771 let resp = DataResponse {
772 success: true,
773 data: Some(vec![SqlmapDataChunk {
774 r#type: 1,
775 value: serde_json::json!([{
776 "parameter": "id",
777 "type": "boolean-based blind",
778 "payload": "id=1 AND 1=1"
779 }]),
780 }]),
781 error: None,
782 message: None,
783 };
784 let findings = resp.findings();
785 assert_eq!(findings.len(), 1);
786 assert_eq!(findings[0].parameter, "id");
787 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
788 }
789
790 #[test]
791 fn builder_pattern_serializes_correctly() {
792 let opts = SqlmapOptions::builder()
793 .url("http://test.com?id=1")
794 .level(3)
795 .risk(2)
796 .batch(true)
797 .threads(4)
798 .tamper("space2comment")
799 .build();
800 let json = serde_json::to_string(&opts).expect("serialize");
801 assert!(json.contains("http://test.com"));
802 assert!(json.contains("\"level\":3"));
803 assert!(json.contains("\"threads\":4"));
804 assert!(json.contains("space2comment"));
805 assert!(!json.contains("dbms"));
807 }
808
809 #[test]
810 fn tech_serializes_as_technique() {
811 let opts = SqlmapOptions::builder().tech("BEUSTQ").build();
812 let json = serde_json::to_string(&opts).expect("serialize");
813 assert!(json.contains("\"technique\":\"BEUSTQ\""));
814 assert!(!json.contains("\"tech\""));
815 }
816
817 #[test]
818 fn type_1_chunk_edge_cases() {
819 let resp = DataResponse {
820 success: true,
821 data: Some(vec![SqlmapDataChunk {
822 r#type: 1,
823 value: serde_json::json!([
824 { "parameter": "username" },
825 "string_instead_of_object_should_be_ignored",
826 { "type": "error-based" },
827 {
828 "parameter": "id",
829 "type": "boolean-based blind",
830 "payload": "id=1 AND 1=1"
831 }
832 ]),
833 }]),
834 error: None,
835 message: None,
836 };
837 let findings = resp.findings();
838 assert_eq!(findings.len(), 1);
839 assert_eq!(findings[0].parameter, "id");
840 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
841 assert_eq!(findings[0].payload, "id=1 AND 1=1");
842 }
843
844 #[test]
845 fn new_options_fields_serialize() {
846 let opts = SqlmapOptions::builder()
847 .tor(true)
848 .tor_port(9050)
849 .tor_type("SOCKS5")
850 .crawl_depth(3)
851 .second_url("http://verify.com")
852 .tamper("between,randomcase")
853 .retries(5)
854 .dump_all(true)
855 .file_read("/etc/passwd")
856 .build();
857 let json = serde_json::to_string(&opts).expect("serialize");
858 assert!(json.contains("\"tor\":true"));
859 assert!(json.contains("\"torPort\":9050"));
860 assert!(json.contains("\"crawlDepth\":3"));
861 assert!(json.contains("\"secondUrl\""));
862 assert!(json.contains("\"fileRead\""));
863 assert!(json.contains("\"dumpAll\":true"));
864 }
865
866 #[test]
867 fn finding_display() {
868 let finding = SqlmapFinding {
869 parameter: "id".into(),
870 vulnerability_type: "boolean-based blind".into(),
871 payload: "id=1 AND 1=1".into(),
872 details: serde_json::json!({}),
873 };
874 let display = format!("{finding}");
875 assert!(display.contains("boolean-based blind"));
876 assert!(display.contains("id"));
877 }
878
879 #[test]
880 fn format_csv_output() {
881 let findings = vec![SqlmapFinding {
882 parameter: "id".into(),
883 vulnerability_type: "error-based".into(),
884 payload: "' OR 1=1--".into(),
885 details: serde_json::json!({}),
886 }];
887 let csv = format_findings(&findings, OutputFormat::Csv);
888 assert!(csv.starts_with("parameter,vulnerability_type,payload\n"));
889 assert!(csv.contains("error-based"));
890 }
891
892 #[test]
893 fn format_plain_empty() {
894 let plain = format_findings(&[], OutputFormat::Plain);
895 assert_eq!(plain, "No SQL injection findings detected.\n");
896 }
897
898 #[test]
899 fn format_markdown_output() {
900 let findings = vec![SqlmapFinding {
901 parameter: "id".into(),
902 vulnerability_type: "UNION query".into(),
903 payload: "id=1 UNION SELECT 1,2--".into(),
904 details: serde_json::json!({}),
905 }];
906 let md = format_findings(&findings, OutputFormat::Markdown);
907 assert!(md.contains("| Parameter |"));
908 assert!(md.contains("UNION query"));
909 }
910}