At the end of last year I bought a server and was immediately faced with a problem: Servers tend to be loud. At the time this was a big problem for me because I was living in a tiny apartment and the server would run about 10 ft. from where I was sleeping. When I was picking parts, I expected fan noise to be an issue and tried to counter the problem by buying be-quiet! fans and a be-quiet! case. Still the fans were clearly audible even when the server was idling.

To understand why we need to talk about a bit about the motherboard I picked - the Supermicro H12SSL-NT. On this motherboard the fans are not actually controlled by the operating system but a separate chip called the ASPEED AST2500 BMC. It’s not possible to modify the firmware directly, but it exposes a web panel that can be used to figure it. Taking a look at the sensor section quickly revealed the problem: screenshot of the sensor section

The dual 10Gb network controller regularly reaches 70°C (158°F) causing the fans to spin up. On the “Optimal” fan mode this results in the fans ramping up to about 30%. This may not seem like much but in an otherwise completely silent room I could easily hear the fans at night. Other than that the absolute lowest duty the BMC will ever set the fans to is 20% which again is unfortunately still enough to be audible.

The H12SSL-NT supports up to 7 fans in 2 zones with 4 different fan modes, but unfortunately it’s not possible to manually adjust any fan curves. Not satisfied with this I came up with the following plan:

  1. Get RCE on the BMC.
  2. Patch the curves on the running system.
  3. Profit

Achieving remote code execution

I downloaded the latest firmware from Supermicro’s website, ran binwalk and threw some of the binaries into Ghidra. Eventually I discovered some code responsible for sending out email notifications that looked vulnerable to command injection. Simplified the code looks like this:

snprintf(buffer, 0x1000,
         "echo \'%s\' | %s --host=%s --port=%d --domain=%s --timeout=1 --auth=off --from=%s %s",
         message,
         "/bin/msmtp",
         host,
         port,
         domain,
         from,
         sender);
run_shellcmd(buffer);

host, port, domain, from and sender can be controlled by an authenticated attacker by simply changing the values in the email notification settings.

This is already useful for me and my server because I’m obviously an admin on my own machine and can change the settings to exploit this vulnerability, but I decided to dig a little deeper to see if the bug can also be triggered by an unauthenticated attacker. There are several event types that will trigger an email to be sent out. They can be grouped into three categories: alerts, resource changes and status changes. These events can also be seen in the Maintenance Event Log. screenshot of the maintenance event log showing several entries such as for logins, ntp server configuration and fan mode changes

The login messages quickly piqued my interest because an unauthenticated attacker might be able to influence them and they would likely end up in the message parameter. After a bit of trial and error and discovering that spaces are apparently not allowed in the username I was able to come up with '`ping${IFS}-c${IFS}4${IFS}192.168.13.4` and was able to confirm code execution.

# tcpdump -i enp9s0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp9s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:42:07.012482 IP 192.168.13.12 > desktop: ICMP echo request, id 2058, seq 0, length 64
12:42:07.012500 IP desktop > 192.168.13.12: ICMP echo reply, id 2058, seq 0, length 64
12:42:08.004739 IP 192.168.13.12 > desktop: ICMP echo request, id 2058, seq 1, length 64
12:42:08.004757 IP desktop > 192.168.13.12: ICMP echo reply, id 2058, seq 1, length 64
12:42:09.004745 IP 192.168.13.12 > desktop: ICMP echo request, id 2058, seq 2, length 64
12:42:09.004760 IP desktop > 192.168.13.12: ICMP echo reply, id 2058, seq 2, length 64
12:42:10.004776 IP 192.168.13.12 > desktop: ICMP echo request, id 2058, seq 3, length 64
12:42:10.004805 IP desktop > 192.168.13.12: ICMP echo reply, id 2058, seq 3, length 64

The exploit attempt as seen in the logs: a log line containing the username

At this point I responsibly disclosed the vulnerability to Supermicro. This vulnerability has been assigned CVE-2023-35861.

Now, in this blog post I’ll just use this to run my own code on the BMC to patch the fan curves, but a malicious attacker could potentially do a lot more damage as they have essentially full control over the server, so the actual impact is higher than it might seem at first.

Patching the fan curves

While exploring the firmware I gained a good understanding of the internal workings of the BMC and had already discovered how the fan curves are implemented. For each fan zone there’s a list of sensors in them. Bundled with each sensor are 4 temperature and duty values for each fan mode. As an example the network controller that caused me troubles has the following values by default:

zones:
  - MB_10G:
      Standard:
        65: 20
        73: 40
        84: 80
        90: 100
      Full:
        65: 100
        73: 100
        84: 100
        90: 100
      Optimal:
        65: 20
        73: 45
        84: 81
        90: 100
      HeavyIO:
        65: 20
        73: 45
        84: 81
        90: 100

(The firmware does not actually represent the values in yaml.)

These values are found in a shared library called libipmi.so and are used by a process called ipmi_sensor. To patch the curves I wrote fan-recurve - a tool can be used to dump and patch the curves in a running system. It uses the ptrace API to attach to the process and locate and modify the fan curves in place.

Results

Now, did all of this actually work? Yes, it helped a lot.

Before the fans looked like this … A screenshot of the fan section: FAN1: 420, FAN2: 560, FAN3: 560, FAN5: 840, FANA: 1960 … and now they look like this. A screenshot of the fan section: FAN1: critical, FAN2: critical, FAN3: 420, FAN5: critical, FANA: 3500

Note that the BMC marks some of the fans as “Critical” but this just means that they’ve spun all the way down to 0 rpm, so this is actually a good thing and exactly what I wanted in the first place. For the network controller I installed a tiny 40mm fan blowing air onto its heat sink. That fan is the only fan in zone 2 (FANA), so I changed the fan curves in zone 2 to be higher than what they usually are to keep the network controller nice and cool.

In case anyone’s worried that spinning fans to 0 rpm will cause problems, here are the temperatures with my modified fan curves, they’re completely fine. CPU Temp: 40, System Temp: 34, Peripheral Temp: 49, MB_10G_LAN Temp: 63, VRMCpu Temp: 45, VRMSoc Temp: 43, VRMABCD Temp: 43, VRMEFGH Temp: 46, P1_DIMMA~D Temp: 38, P1_DIMME~H Temp: 38