240 lines
5.0 KiB
Perl
240 lines
5.0 KiB
Perl
|
||
package Email::SpoofingDemo::DNS::ZoneFile;
|
||
|
||
use strict;
|
||
use warnings;
|
||
use utf8;
|
||
use v5.10;
|
||
|
||
use Exporter qw(import);
|
||
|
||
use File::Slurp;
|
||
use Try::Tiny;
|
||
|
||
our @EXPORT_OK = qw(get_txt get_zone_file import_zone_file remove_txt set_txt
|
||
zone_file_path);
|
||
|
||
sub safe_system {
|
||
my @argv = @_;
|
||
my $retval = system @argv;
|
||
die $! if ($retval == -1);
|
||
if ($retval != 0) {
|
||
my $status = $retval >> 8;
|
||
die join(" ", @argv) . " exited with status $status\n";
|
||
};
|
||
return 1;
|
||
}
|
||
|
||
sub is_dynamic_zone {
|
||
my ($zone, $dynamic_zones) = @_;
|
||
|
||
return grep { $_ eq $zone } @$dynamic_zones;
|
||
}
|
||
|
||
sub rndc_freeze {
|
||
my ($zone) = @_;
|
||
|
||
safe_system('rndc', 'freeze', $zone);
|
||
}
|
||
|
||
sub rndc_thaw {
|
||
my ($zone) = @_;
|
||
|
||
safe_system('rndc', 'thaw', $zone);
|
||
}
|
||
|
||
sub rndc_reload {
|
||
my ($zone) = @_;
|
||
|
||
safe_system('rndc', 'reload', $zone);
|
||
}
|
||
|
||
sub encode_txt {
|
||
my ($text) = @_;
|
||
|
||
my $chunk_length = 255;
|
||
my $text_bytes = $text;
|
||
utf8::encode($text_bytes);
|
||
|
||
my @parts;
|
||
for (my $i = 0; $i < length($text_bytes); $i += $chunk_length) {
|
||
my $part = substr($text_bytes, $i, 255);
|
||
$part =~ s/[\x00-\x1f"\x7f-\xff]/sprintf('\\%03d', ord($0))/eg;
|
||
push @parts, qq{"$part"};
|
||
}
|
||
|
||
return join(" ", @parts);
|
||
}
|
||
|
||
sub relative_to_absolute {
|
||
my ($zone, $relative_owner) = @_;
|
||
|
||
if ($relative_owner eq '') {
|
||
return $zone;
|
||
}
|
||
else {
|
||
return "$relative_owner.$zone";
|
||
}
|
||
}
|
||
|
||
sub nsupdate {
|
||
my ($zone, @instructions) = @_;
|
||
|
||
open(my $pipe, '|-', 'nsupdate') or die "Can’t fork: $!";
|
||
say $pipe qq{zone $zone};
|
||
say $pipe $_ foreach (@instructions);
|
||
close($pipe);
|
||
|
||
return (($? >> 8) == 0);
|
||
}
|
||
|
||
sub get_txt {
|
||
my ($zone, $relative_owner, $filter, $sublabels_ok) = @_;
|
||
|
||
$filter //= qr{.*};
|
||
$sublabels_ok //= 0;
|
||
|
||
my @dig_opts = qw(+nottlid +noclass +norecurse +noall +answer @127.0.0.1);
|
||
|
||
my $owner_suffix = relative_to_absolute($zone, $relative_owner);
|
||
my $qname = ($sublabels_ok ? $zone : $owner_suffix);
|
||
my $qtype = ($sublabels_ok ? 'AXFR' : 'TXT');
|
||
my $owner_match_re = ($sublabels_ok ? qr/^([^.]+\.)*$owner_suffix\.$/i : qr/^$qname\.$/i);
|
||
|
||
my @result;
|
||
|
||
open(my $pipe, '-|', 'dig', @dig_opts, $qtype, $qname) or die "Can’t fork: $!";
|
||
|
||
while (<$pipe>) {
|
||
chomp;
|
||
|
||
next if /^;/;
|
||
next if /^\s*$/;
|
||
|
||
my ($owner, $type, $data) = split(" ", $_, 3);
|
||
|
||
unless ($type eq 'TXT') {
|
||
next;
|
||
}
|
||
unless ($owner =~ $owner_match_re) {
|
||
next;
|
||
}
|
||
|
||
my $concat_data = ($data =~ s/"((?:[^\\"]|\\"|\\\d{3})*)"\s*/$1/gr);
|
||
unless ($concat_data =~ $filter) {
|
||
next;
|
||
}
|
||
|
||
push @result, { owner => ($owner =~ s/\.$//r), txt => $concat_data };
|
||
}
|
||
|
||
close($pipe);
|
||
|
||
if (wantarray) {
|
||
return @result;
|
||
}
|
||
else {
|
||
return $result[0];
|
||
}
|
||
}
|
||
|
||
sub set_txt {
|
||
my ($zone, $relative_owner, $txt) = @_;
|
||
my $owner = relative_to_absolute($zone, $relative_owner);
|
||
my $txt_encoded = encode_txt($txt);
|
||
|
||
nsupdate(
|
||
$zone,
|
||
"update delete $owner 0 IN TXT",
|
||
"update add $owner IN TXT $txt_encoded",
|
||
"send");
|
||
}
|
||
|
||
sub remove_txt {
|
||
my ($zone, $relative_owner) = @_;
|
||
my $owner = relative_to_absolute($zone, $relative_owner);
|
||
|
||
nsupdate(
|
||
$zone,
|
||
"update delete $owner 0 IN TXT",
|
||
"send");
|
||
}
|
||
|
||
sub check_zone {
|
||
my ($zone_contents, $origin) = @_;
|
||
|
||
open(my $pipe, '|-', 'named-checkzone', $origin, '-') or die "Can’t fork: $!";
|
||
print $pipe $zone_contents;
|
||
close($pipe);
|
||
|
||
return (($? >> 8) == 0);
|
||
}
|
||
|
||
sub zone_file_path {
|
||
my ($zone, $base_dir, $force) = @_;
|
||
|
||
my @components = reverse(split(/\./, $zone));
|
||
my $path = $base_dir . '/' . (join('/', @components)) . ".zone";
|
||
|
||
if (not -f $path and not $force) {
|
||
return undef;
|
||
}
|
||
return $path;
|
||
}
|
||
|
||
sub get_zone_file {
|
||
my ($zone, $base_dir, $dynamic_zones) = @_;
|
||
|
||
my $path = zone_file_path($zone, $base_dir);
|
||
return undef if not defined $path;
|
||
|
||
rndc_freeze($zone) if is_dynamic_zone($zone, $dynamic_zones);
|
||
|
||
my $contents;
|
||
try {
|
||
$contents = read_file($path);
|
||
}
|
||
finally {
|
||
rndc_thaw($zone) if is_dynamic_zone($zone, $dynamic_zones);
|
||
};
|
||
|
||
return $contents;
|
||
}
|
||
|
||
sub import_zone_file {
|
||
my ($zone, $base_dir, $contents, $dynamic_zones) = @_;
|
||
|
||
if ($contents =~ /^\s*$/) {
|
||
warn "Zone to import is empty";
|
||
return undef;
|
||
}
|
||
|
||
my $path = zone_file_path($zone, $base_dir);
|
||
if (not defined $path) {
|
||
warn "No zone file for $zone in $base_dir";
|
||
return undef;
|
||
}
|
||
|
||
if (not check_zone($contents, $zone)) {
|
||
warn "Zone file contents do not pass syntax check";
|
||
return undef;
|
||
}
|
||
|
||
rndc_freeze($zone) if is_dynamic_zone($zone, $dynamic_zones);
|
||
|
||
try {
|
||
write_file($path, $contents);
|
||
rndc_reload($zone) unless is_dynamic_zone($zone, $dynamic_zones);
|
||
}
|
||
catch {
|
||
die $_;
|
||
}
|
||
finally {
|
||
rndc_thaw($zone) if is_dynamic_zone($zone, $dynamic_zones);
|
||
};
|
||
|
||
return 1;
|
||
}
|
||
|
||
1;
|