When I arrived at UC Santa Cruz went to connect to the WiFi network, I noticed the recommended setup script seemed awfully strange. I was both curious and slightly concerned, so I did some poking around before blindly running anything. One thing led to another and I ended up knee-deep in an investigation into the script’s inner workings.

In the rest of this post I hope to summarize how it works and showcase my most interesting discoveries. Hopefully you’ll find this as entertaining as I did!

note: the setup script was not written by UCSC, but rather by a contractor used by many universities to automate the setup of Eduroam connections - more on that later. I will be talking about the Linux version only.

Innocent Shell Script

There’s two methods listed on the UCSC website for connecting to WiFi on Linux: “Preferred Setup (using JoinNow)”, and “Manual Setup”. I downloaded the automated setup script just to check it out, even though I knew I’d wanna use the manual method eventually. How bad could it be?

SecureW2_JoinNow.run
#!/bin/sh

die () {
    [ ! -z "$1" ] && echo "Fatal: $1"
    [ ! -z "$tmpdir" -a -d "$tmpdir" ] && ${RM} -Rf "$tmpdir"
    exit 1
}

missing () {
    echo 'Executable `'$1'` seems to be missing, not executable or cannot be located with `which`.'
    echo ''
    echo 'Please install this program using your distribution-specific package manager (e.g. `apt-get` or `yum`).'
    echo 'If this does not solve the issue, you can try editing this script by hand to provide the proper'
    echo 'executable locations, or request your network administrator to contact SecureW2 Support.'
    die
}

dontrunasroot () {
    echo 'This utility is not designed to run as root.'
    echo 'Please start it as a regular user.'
    die
}

findutil () {
    for u in "$@"; do \
        p="$(${WHICH} ${WHICHPARAMS} "$u" 2> /dev/null)"
        [ ! -z "$p" ] && break
    done
    [ -z "$p" ] && missing "$1"
    return 0
}

whichdetect () {
    [ -z "${WHICH}" ] && WHICH="which"
    while true; do \
        WHICHPARAMS="$@"
        ${WHICH} ${WHICHPARAMS} which > /dev/null 2>&1
        if [ $? -eq 0 ]; then \
            findutil which && WHICH="$p"
            return 0
        fi
        [ -z "$1" ] && missing "which"
        shift
    done
}

# Call twice: make sure to get correct flags for actual binary
whichdetect --skip-functions --skip-alias
whichdetect --skip-functions --skip-alias

findutil whoami     && WHOAMI="$p"
[ "$(${WHOAMI})" = "root" ] && dontrunasroot

findutil mkdir      && MKDIR="$p"
findutil rm         && RM="$p"
findutil tar        && TAR="$p"
findutil gzip       && GZIP="$p"
findutil pwd        && PWD="$p"
findutil sed        && SED="$p"
findutil readlink   && READLINK="$p"
findutil python \
         python2 \
         python3    && PYTHON="$p"

tmpdir="/tmp/securew2-joinnow-$$.tmp"
archive="$(${READLINK} -f "$0")"

${MKDIR} -p "$tmpdir" || die "Error creating temporary directory $tmpdir"
cd $tmpdir || die "Error switching working directory to $tmpdir"
${SED} '0,/^#ARCHIVE#$/d' "$archive" | ${GZIP} -d | ${TAR} x || die "Error extracting embedded archive"
${PYTHON} main.py "$@"
retval=$?
${RM} -Rf "$tmpdir"
exit $retval

#ARCHIVE#
‹<àf
SecureW2_JoinNow.tar
ì=ksÛ8’ó9¿‚3S)R3Š"ù™I•¶N‘™DIò89ÇÅ¢%8æF"µ|øq[û߯
I¼HIq2WwuüÐºÑht7äÏÓ
“Öúñ§ö´á9:8Àÿ;LJmñ,9<jïýÔ9Øß?>::nwŽ~jw:‡G?YíGRùdIêǖõ‰ƒ¯uõ6•ÿ/}‚Õ:ŠSëI×~’<ãFÅ[LžÝÄÑʺört`ñ¯×G2¼ðÖOn—Áu^šÜúVd×ë8š“$ÉËÆÑš„Mk<»Mk:;ÏšVß_.ÉbÌjºqÅ<%é}ì¯s`|ç%ÁŠÍ-	Y?Ë	¾'×I±Z‹ë,öR´?$é}=óCÿ‰¡É‡GV—„q´\æÕØ_Mk6>›fALb5ä5ûQ’yŔî¦åÒ
+¦îܬQ»äWò˜@µ‚ì>‰Óà&˜û)çâÚ_ú‹ \›‚ÚßXILð/Ld5ûË([LxY^3‰²xNV¬9À2ò^^–°ªYх7Yš‚
`ŸVË[²\—°´èåMðe#~c
¥Ïž=;ë}ôÞNÝaïÌõ¦ƒÿt­®ÕÙ;¶ðùÕz³°¢ÐZùÖÞá¡uý˜Â0€e« übÁ($©5‡ZV2'¡‘ÝX{¼…ʘ5¿õcŽ}|6_«Ge¸Sp¹ñú¶<qÇ£ÉÌs'“ÑÄëNªa½ð̝N{ïŠrZ˜oy^©ç9	YÞ4aXb̦EØø®@Dµ¼5|°Z˧$.V].¤ P–¤±CßVpc…QjI‚
“Öúñ§ö´á9:8Àÿ;LJmñ,9<jïýÔ9Øß?>::nwŽ~jw:‡G?YíGRùdIêǖõ‰ƒ¯uõ6•ÿ/}‚Õ:ŠSëI×~’<ãFÅ[LžÝÄÑʺört`ñ¯×G2¼ðÖOn—Áu^šÜúVd×ë8š“$ÉËÆÑš„Mk<»Mk:;ÏšVß_.ÉbÌjºqÅ<%é}ì¯s`|ç%ÁŠÍ-	Y?Ë	¾'×I±Z‹ë,öR´?}=óCÿ‰¡É‡GV—„q´ÕØ_Mk6>›fALb5ä5ûQ’yŔî¦åÒ
+¦îܬQ»äWò˜@µ‚ì>‰Óà&˜û)çâÚ_ú‹ ›‚ÚßXILð/Ld5ûË([LxY^3‰²xNV¬9À2ò^^–°ªYх7Yš‚
RY²”PÁ³fP…rTn†Îæör­„ ¢¡¿µSx7ég©‚6³0YìS=ºñ˜¬Ëçɕå<Ov¥nO/ö¼qï´w2ò‚ÁpæN†½Óí4ZTd*T†nhÂ¥Ø]N9KÎ÷Q»q~+4{{®h©Ÿí¸s>u¡´7ì»§§îÉv<êÚE'l_æ>V²¬´M«Ó9VIFý­sé£]ÿfUޘ؋^6
½Ù§±ë GCWÕ|ºV»ŽqÊש…;õ?Lûתp{µpSw6t/ƽéôb49áöká`j}sê^œö´öjáz'':…;¬…{Óë8›ør´ŸÓÙh¢E×:ÞDgߝÌoýÞÌá^ÕÂõÁã˜ôLtþQw^çIo¦A¸×Ì`8õNO§£·³‹žÐQ
¬—˜þh8tûº¨¡§´IdƓÑÇO€àíàÝù¤‡E°^fNS*4ƒ‰{
çÄáøËþXd	Ì¡Ÿ[žG{îyðú+âyE€)}\''Ìêv­ LKúðá…HCIm´Þš`8
h­ý<±U1{n9ÈOZœíÓw‚h½„QÏú®•–lÍ@@Aš´cV1xE‡ŠqeZå7%T˜¤Yrú5	b¸[ž#v:ê¼HÁ®0ß$„EÝ4~”ù¼iò+"¹ßóØ«GyÞh
RY²”PÁ³fP…rTn†Îæör­„ ¢¡¿µSx7ég©‚6³0YìS=ºñ˜¬Ëçɕå<Ov¥nO/ö¼qï´w2ò‚ÁpæN†½Óí4ZTd*T†nhÂ¥Ø]N9KÎ÷Q»q~+4{{®h©Ÿí¸s>u¡´7ì»§§îÉv<êÚE'l_æ>V²¬´M«Ó9VIFý­sé£]ÿfUޘ؋^6


... [ 194 LINES REMOVED ] ...

This is the script I was greeted with.

In the beginning, it’s pretty sensible! Detections for coreutils, warnings to not run as root, error messages, etc; all things a portable bash script should have. But wait a minute; why does it need gzip and tar? Why is it looking for Python, and where is this supposed “main.py”?

Scrollisng to the bottom of the script, I saw a bunch of binary data. What, you might be asking, is a bash script doing with binary data embedded at the end of it? Well, lets take a closer look at these lines of the script (some omitted for clarity):

archive="$(${READLINK} -f "$0")"
${SED} '0,/^#ARCHIVE#$/d' "$archive" | ${GZIP} -d | ${TAR} x || die "Error extracting embedded archive"
${PYTHON} main.py "$@"

In order of operations:

  1. Firstly, readlink -f "$0" gets the absolute path to "$0", which in Bash is the path to the current script.
  2. Next, it calls sed on that absolute path, with the argument '0,/^#ARCHIVE#$/d', which tells sed, “from line 0 of the file to the first line with the text #ARCHIVE#, delete everything” (it does this in memory, not in-place on the file).
  3. It then pipes that output (the remaining text) into gzip -d, asking gzip (a compression utility) to treat the incoming data blob from stdin as a compressed (.gz) file and to decompress it.
  4. Lastly, it pipes that output into tar x, asking tar (an archiving utility) to treat the incoming data from stdin as an archive file containing multiple inner files, and to extract them into the current directory (a temporary location).
  5. Finally, it runs the extracted main.py file using the Python interpreter it found earlier.

In other words: the binary data at the bottom of the SecureW2_JoinNow.run bash script is a python script!

Interesting. Let’s check it out!

Python Jumpscare

Not wanting to run the script blindly just yet, I executed the commands manually. And…

SecureW2_JoinNow-python-jumpscare

Good lord, what is all this?! All you have to do is connect me to the WiFi.. right? (exasperated emoji)

The first thing I did, even before browsing the python, was open SecureW2.cloudconfig. More binary data! This time though it was clear there was some important text embedded, as I could clearly read fragments of what looked to be dialog labels for some GUI:

SecureW2_cloudconfig

I spent some more time digging before running anything. Following the main.py entrypoint to PaladinClient led me straight to the first intersting thing (simplified for clarity):

class PaladinLinuxClient(object):
    """SecureW2 JoinNow Linux Client Implementation"""
    CONFIG_FILE = 'SecureW2.cloudconfig'

    @staticmethod
    def decipher(config_file):
        with open(config_file) as config:
            p = Popen('openssl smime -verify -inform der -noverify', stdin=config, stdout=PIPE, shell=True)
            config_data = p.communicate()[0]
            return bytearray(config_data).decode('utf-8')

    @staticmethod
    def strip_namespace(xml_document):
        """Removes xmlns attribute from XML file to avoid having to prefix all nodes"""
        return re.sub('xmlns="[^"]+"', '', xml_document)

    def load_config(self, xml_document):
        """Parses the XML config file"""
        root = ET.fromstring(xml_document)

        # Find organization node
        self.organization = (root.findall('organization') + [ None ])[0]
        # Find (first) deviceconfig node
        self.devicecfg = root.find('configurations/deviceConfiguration')
        ...

I saw that during it’s __init__ PaladinClient…

  1. runs openssl (straight from PATH!) in order to somehow decode SecureW2.cloudconfig, passing it to openssl as stdin
  2. treats the resulting bytearray as an XML document, getting important information like deviceConfiguration and organization from it

Hidden XML

Checking the filetype of SecureW2.cloudconfig revealed it to be a binary data format (DER) that contains a certificate:

$ file SecureW2.cloudconfig
SecureW2.cloudconfig: DER Encoded PKCS#7 Signed Data

Wikipedia says that PKCS#7 is just a standard file format for encrypted data, and that DER is one of the specifications for how to encode data as binary into that file.

Openssl can help prove that there is in fact a certificate hiding inside the SecureW2.cloudconfig file:

$ openssl pkcs7 -in SecureW2.cloudconfig -inform DER -print_certs -out cert.pem
$ cat cert.pem
subject=C=NL, ST=Overijssel, L=Enschede, O=SecureW2, OU=Development, CN=license.securew2.com
issuer=C=NL, ST=Overijssel, L=Enschede, O=SecureW2, OU=Development, CN=license.securew2.com
-----BEGIN CERTIFICATE-----
MIIDXDCCAsWgAwIBAgIJA..

However, the wiki page mentioned data storage too, and Openssl can also extract it:

$ openssl smime -verify -inform der -noverify < SecureW2.cloudconfig > dump.xml
$ cat dump.xml
<?xml version="1.0" encoding="UTF-8"?><paladinResponse..

There’s the XML hiding inside SecureW2.cloudconfig! Looking around, it contains both important organizational information (OAuth URLs, org name, certificats, etc) as well as what looks to be UI dialog texts (localizations). I verified this by also downloading the setup script for Dartmouth University (one of the other schools contracting with Eduroam for wifi), and it produced a very similar out.xml, except with differing <actions> depending on the unique networks of each school (eg. UCSC-Guest, ResWifi).

At this point I was curious enough to try running the script. After being prompted for login I saw that ~/.joinnow/ was created, with a file 08d..4e2.pem:

$ cat ~/.joinnow/08d[..]4e2.pem
-----BEGIN CERTIFICATE-----
MIIEoTCCA4mg..

which matches exactly what’s in the XML (simplified):

<action type="8">
    <certificate>
        <alias>08d..4e2</alias>
        <data>MIIEoTCCA4mg..</data>
    </certificate>
</action>

So clearly PaladinClient (whatever that even is) is using the XML data to figure out how to connect you to the eduroam network and make it persist on the machine. I still have no idea what type="8" means, nor where that certificate actually comes into play during the connection process (it’s not the same as the 802-1x ca-certificate!).

Declarative NixOS config

As I said in the beginning, I knew I’d want to make it work manually eventually, so I don’t have to rely on stateful imperative scripts that I’ll forget how to use eventually. Here is the config that I came up with, which I’ve been running for the past year:

# Hardcoded NetworkManager configurations. First created manually in nmtui (or other tools),
# then converted to nix code via https://github.com/janik-haag/nm2nix. These exist alongside
# the imperatively created networks.
# Config spec: https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html
networking.networkmanager.ensureProfiles.profiles =
let
  mkUCSCProfile = ssid: {
    wifi = {
      inherit ssid;
      mode = "infrastructure";
    };
    "802-1x" = {
      ca-cert = "${pkgs.fetchurl { 
        url = "https://its.ucsc.edu/wireless/docs/ca.crt";
        hash = "sha256-[..]"; 
      }}";
      anonymous-identity = "anon";
      domain-suffix-match = "ucsc.edu";
      eap = "peap;";
      identity = "jnystrom@ucsc.edu";
      password-flags = 0; # Store password # TODO: this doesn't actually remember the password for you
      phase2-auth = "mschapv2";
    };
    connection = {
      id = "UCSC ${ssid} (nixconf)";
      type = "wifi";
      autoconnect = true;
    };
    wifi-security.key-mgmt = "wpa-eap";
    proxy = { };
  };
in
{
  "UCSC eduroam (nixconf)" = lib.mkIf (config.host.isMobile) (mkUCSCProfile "eduroam");
  "UCSC ResWiFi (nixconf)" = lib.mkIf (config.host.isMobile) (mkUCSCProfile "ResWiFi");
};

This config has an issue though that the SecureW2_JoinNow.run script doesn’t, which is that even though the password-flags is set to keep, Network Manager asks me for my password every time I try to join. To be figured out eventually :)