The affected SICAM 230 process control system is used as an integrated energy system for utility companies, and as a monitoring system for smart-grid applications.
Siemens has released 16 security advisories for various industrial control and utility products, including a warning for a critical flaw in the WibuKey digital rights management (DRM) solution that affects the SICAM 230 process control system.
SICAM 230 is used for a broad range of industrial control system (ICS) applications, including use as an integrated energy system for utility companies, and a monitoring system for smart-grid applications.
One of the flaws affecting SICAM 230 is rated critical, with a CVSS v.3 score of 10: CVE-2018-3991 allows a specially crafted TCP packet sent to port 22347/tcp to cause a heap overflow, potentially leading to remote code-execution.
Another, CVE-2018-3990, has a CVSS score of 9.3. It allows a specially crafted I/O request packet to cause a buffer overflow, resulting in kernel memory corruption and, potentially, privilege-escalation.
Users should apply the WibuKey DRM updates to v. 6.5 provided by WIBU Systems to mitigate the issues; the critical CVE-2018-3991 meanwhile can also be mitigated by blocking port 22347/tcp on an external firewall.
Other Vulnerabilities
Other flaws of note amid the 16 advisories include three denial-of-service vulnerabilities with a CVSS v3.0 score of 7.5 in the EN100 Ethernet Communication Module and SIPROTEC 5 relays.
One of those is a vulnerability that affects the network functionality of the devices (CVE-2018-16563), thus rendering them unavailable, Siemens said in its advisory.
Another denial-of-service vulnerability (CVE-2018-11451) would allow an attacker to send specially crafted packets to port 102/tcp to cause a denial-of-service condition, requiring a manual restart.
A third flaw (CVE-2018-11452) would allow an attacker to send specially crafted packets to port 102/tcp to cause a denial-of-service condition in the EN100 communication module if oscillographs are running, also requiring a manual restart.
In all three cases, as a precondition, the IEC 61850-MMS communication needs to be activated on the affected EN100 modules; but no user interaction or privileges are required to exploit them.
Meanwhile, a firmware downgrade vulnerability (CVE-2018-4838) in EN100 Ethernet Communication Module for SIPROTEC 4, SIPROTEC Compact and Reyrolle also carries a CVSS v.3.0 score of 7.5. The web interface (TCP/80) of affected devices allows an unauthenticated user to upgrade or downgrade the firmware of the device, including to older versions with known vulnerabilities.
And finally, several industrial products (the SIMATIC line, SIMOTION and SINAMICs lines, and development/evaluation kits for PROFINET) are affected by a vulnerability (CVE-2017-12741) that could allow remote attackers to conduct a denial-of-service attack by sending specially crafted packets to port 161/udp (SNMP).
Siemens has released updates for some of the affected products, is working on updates for the remaining affected products. For CVE-2018-16563, the company recommends blocking access to port 102/tcp with an external firewall until fixes are available. For some products affected by CVE-2017-12741, users can disable SNMP, which fully mitigates the vulnerability.
To understand how malware can use and manipulate Windows then, we need to better understand the inner workings of the Windows operating system. In this article, we will examine the inner workings or Windows 32-bit systems so that we can better understand how malware can use the operating system for its malicious purposes.
Windows internals could fill several textbooks (and has), so I will attempt to just cover the most important topics and only in a cursory way. I hope to leave you with enough information though, that you can effectively reverse the malware in the following articles.
Virtual Memory
Virtual memory is the idea that instead of software directly accessing the physical memory, the CPU and the operating system create an invisible layer between the software and the physical memory.
The OS creates a table that the CPU consults called the page table that directs the process to the location of the physical memory that it should use.
Processors divide memory into pages
Pages are fixed sized chunks of memory. Each entry in the page table references one page of memory. In general, 32 -bit processors use 4k sized pages with some exceptions.
Kernel v User Mode
Having a page table enables the processor to enforce rules on how memory will be accessed. For instance, page table entries often have flags that determine whether the page can be accessed from a non-privileged mode (user mode).
In this way, the operating system’s code can reside inside the process’s address space without concern that it will be accessed by non-privileged processes. This protects the operating system’s sensitive data.
This distinction between privileged vs. non-privileged mode becomes kernel (privileged) and non-privileged (user) modes.
Kernel memory Space
The kernel reserves 2gb of address space for itself. This address space contains all the kernel code, including the kernel itself and any other kernel components such as device drivers.
Paging
Paging is the process where memory regions are temporarily flushed to the hard drive when they have not been used recently. The processor tracks the time since a page of memory was last used and the oldest is flushed. Obviously, physical memory is faster and more expensive than space on the hard drive.
The windows operating system tracks when a page was last accessed and then uses that information to locate pages that haven’t been accessed in a while. Windows then flushes their content to a file. The contents of the flushed pages can then be discarded and the space used by other information. When the operating system needs to access these flushed pages, a page fault will be generated and then system then does that the information has «paged out» to a file. Then, the operating system will access the page file and pull the information back into memory to be used.
Objects and Handles
The Windows kernel manages objects using a centralized object manager component. This object manager is responsible for all kernel objects such as sections, files, and device objects, synchronization objects, processes and threads. It ONLY manages kernel objects.
GUI-related objects are managed by separate object managers that are implemented inside WIN32K.SYS
Kernel code typically accesses objects using direct pointers to the object data structures. Applications use handles for accessing individual objects
Handles
A handle is process specific numeric identifier which is an index into the processes private handle table. Each entry in the handle table contains a pointer to the underlying object, which is how the system associates handles with objects. Each handle entry also contains an access mask that determines which types of operations that can be performed on the object using this specific handle.
Processes
A process is really just an isolated memory address space that is used to run a program. Address spaces are created for every program to make sure that each program runs in its own address space without colliding with other processes. Inside a processes’ address space the system can load code modules, but must have at latest one thread running to do so.
Process Initialization
The creation of the process object and the new address space is the first step. When a new process calls the Win32 API CreateProcess, the API creates a process object and allocates a new memory address space for the process.
CreateProcess maps NTDLL.DLL and the program executable (the .exe file) into the newly created address space. CreateProcess creates the process’s first thread and allocates stack space it. The processes first thread is resumed and starts running in the LdrpInitialization function inside NTDLL.DLL
LdrpInitialization recursively traverses the primary executable’s import tables and maps them to memory every executable that is required.
At this point, control passes into LdrpRunInitializeRoutines, which is an internal NTDLL routine responsible for initializing all statically linked DLL’s currently loaded into the address space. The initialization process consists of a link each DLL’s entry point with the DLL_PROCESS_ATTACH constant. Once all the DLL’s are initialized, LdrpInitialize calls the thread’s real initialization routine, which is the BaseProcessStart function from KERNELL32.DLL. This function in turn calls the executable’s WinMain entry point, at which point the process has completed it’s initialization sequence.
Threads
At ant given moment, each processor in the system is running one thread. Instead of continuing to run a single piece of code until it completes, Windows can decide to interrupt a running thread at given given time and switch to execution of another thread.
A thread is a data structure that has a CONTEXT data structure. This CONTEXT includes;
(1) the state of the processor when the thread last ran
(2) one or two memory blocks that are used for stack space
(3) stack space is used to save off current state of thread when context switched
(4) components that manage threads in windows are the scheduler and the dispatcher
(5) Deciding which thread get s to run for how long and perform context switch
Context Switch
Context switch is the thread interruption. In some cases, threads just give up the CPU on their own and the kernel doesn’t have to interrupt. Every thread is assigned a quantum, which quantifies has long the the thread can run without interruption. Once the quantum expires, the thread is interrupted and other threads are allowed to run. This entire process is transparent to thread. The kernel then stores the state of the CPU registers before suspending and then restores that register state when the thread is resumed.
Win32 API
An API is a set of functions that the operating system makes available to application programs for communicating with the OS. The Win32 API is a large set of functions that make up the official low-level programming interface for Windows applications. The MFC is a common interface to the Win32 API.
The three main components of the Win 32 API are;
(1) Kernel or Base API’s: These are the non GUI related services such as I/O, memory, object and process an d thread management
(2) GDI API’s : these include low-level graphics services such a s those for drawing a line, displaying bitmap, etc.
(3) USER API’s : these are the higher level GUI-related services such as window management, menus, dialog boxes, user-interface controls.
System Calls
A system call is when a user mode code needs to cal a kernel mode function. This usually happens when an application calls an operating system API. User mode code invokes a special CPU instruction that tells the processor to switch to its privileged mode and call a dispatch routine. This dispatch routine then calls the specific system function requested from user mode.
PE Format
The Windows executable format is a PE (portable Executable). The term «portable» refers to format’s versatility in numerous environments and architectures.
Executable files are relocatable. This means that they could be loaded at a different virtual address each time they are loaded. An executable must coexist with other executables that are loaded in the same memory address. Other than the main executable, every program has a certain number of additional executables loaded into its address space regardless of whether it has DLL’s of its own or not.
Relocation Issues
If two excutables attempt to be loaded into the same virtual space, one must be relocated to another virtual space. each executable is module is assigned a base address and if something is already there, it must be relocated.
There are never absolute memory addresses in executable headers, those only exist in the code. To make this work, whenever there is a pointer inside the executable header, it is always a relative virtual address (RVA). Think of this as simply an offset. When the file is loaded, it is assigned a virtual address and the loaded calculates real virtual addresses out of RVA’s by adding the modules base address to an RVA.
Image Sections
An executable section is divided into individual sections in which the file’s contents are stored. Sections are needed because different areas in the file are treated differently by the memory manager when a module is loaded. This division takes place in the code section (also called text) containing the executable’s code and a data section containing the executable’s data.
When loaded, the memory manager sets the access rights on memory pages in the different sections based on their settings in the section header.
Section Alignment
Individual sections often have different access settings defined in the executable header. The memory manager must apply these access settings when an executable image is loaded. Sections must typically be page aligned when an executable is loaded into memory. It would take extra space on disk to page align sections on disk. Therefore, the PE header has two different kinds of alignment fields, section alignment and file alignment.
DLL’s
DLL’s allow a program to be broken into more than one executable file. In this way, overall memory consumption is reduced, executables are not loaded until features they implement are required. Individual components can be replaced or upgraded to modify or improve a certain aspect of the program.
DLL’s can dramatically reduce overall system memory consumption because the system can detect that a certain executable has been loaded into more than one address space, then map it into each address space instead of reloading it into a new memory location. DLL’s are different from static libraries (.lib) which linked to the executable.
Loading DLL’s
Static Linking is implemented by having each module list the the modules it uses and the functions it calls within each module. This is known as an import table (see IDA Pro tutorial). Run time linking refers to a different process whereby an executable can decide to load another executable in runtime and call a function from that executable.
PE Headers
A Portable Executable (PE) file starts with a DOS header.
«This program cannot be run in DOS mode»
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAFE_FILE_HEADER Fileheader;
IMAGE_OPTIONAL_HEADER32 OptionHeader;
} Image_NT_HEADERS32, *PIMAGE_NT_HEADERS32
This data structure references two data structures which contain the actual PE header.
Imports and Exports
Imports and Exports are the mechanisms that enable the dynamic linking process of executables. The compiler has no idea of the actual addresses of the imported functions, only in runtime will these addresses be known. To solve this issue, the linker creates a import table that lists all the functions imported by the current module by their names.
I came into work to find an unusually high number of private Slack messages. They all pointed to the same tweet.
Why would this matter to me? I gave a talk at Derbycon about hunting for bugs in MikroTik’s RouterOS. I had a 9am Sunday time slot.
You don’t want a 9am Sunday time slot at Derbycon
Now that Zerodium is paying out six figures for MikroTik vulnerabilities, I figured it was a good time to finally put some of my RouterOS bug hunting into writing. Really, any time is a good time to investigate RouterOS. It’s a fun target. Hell, just preparing this write up I found a new unauthenticated vulnerability. You could too.
Laying the Groundwork
Now I know you’re already looking up Rolex prices, but calm down, Sparky. You still have work to do. Even if you’re just planning to download a simple fuzzer and pray for a pay day, you’ll still need to read this first section.
Acquiring Software
You don’t have to rush to Amazon to acquire a router. MikroTik makes RouterOS ISOs available on their website. The ISO can be used to create a virtual host with VirtualBox or VMWare.
Naturally, Mikrotik published 6.42.12 the day I published this blog
You can also extract the system files from the ISO.
albinolobster@ubuntu:~/6.42.11$ 7z x mikrotik-6.42.11.iso
7-Zip [64] 9.20 Copyright (c) 1999-2010 Igor Pavlov 2010-11-18 p7zip Version 9.20 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,4 CPUs)
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------- 0 0x0 NPK firmware header, image size: 15616295, image name: "system", description: "" 4096 0x1000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 9818075 bytes, 1340 inodes, blocksize: 262144 bytes, created: 2018-12-21 09:18:10 9822304 0x95E060 ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV) 9842177 0x962E01 Unix path: /sys/devices/system/cpu 9846974 0x9640BE ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV) 9904147 0x972013 Unix path: /sys/devices/system/cpu 9928025 0x977D59 Copyright string: "Copyright 1995-2005 Mark Adler " 9928138 0x977DCA CRC32 polynomial table, little endian 9932234 0x978DCA CRC32 polynomial table, big endian 9958962 0x97F632 xz compressed data 12000822 0xB71E36 xz compressed data 12003148 0xB7274C xz compressed data 12104110 0xB8B1AE xz compressed data 13772462 0xD226AE xz compressed data 13790464 0xD26D00 xz compressed data 15613512 0xEE3E48 xz compressed data 15616031 0xEE481F Unix path: /var/pdb/system/crcbin/milo 3801732988
albinolobster@ubuntu:~/6.42.11$ ls -o ./_system-6.42.11.npk.extracted/squashfs-root/ total 64 drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 bin drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 boot drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 dev lrwxrwxrwx 1 albinolobster 11 Dec 21 04:18 dude -> /flash/dude drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 etc drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 flash drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 home drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 initrd drwxr-xr-x 4 albinolobster 4096 Dec 21 04:18 lib drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 nova drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 old lrwxrwxrwx 1 albinolobster 9 Dec 21 04:18 pckg -> /ram/pckg drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 proc drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 ram lrwxrwxrwx 1 albinolobster 9 Dec 21 04:18 rw -> /flash/rw drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sbin drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sys lrwxrwxrwx 1 albinolobster 7 Dec 21 04:18 tmp -> /rw/tmp drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 usr drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 var albinolobster@ubuntu:~/6.42.11$
Hack the Box
When looking for vulnerabilities it’s helpful to have access to the target’s filesystem. It’s also nice to be able to run tools, like GDB, locally. However, the shell that RouterOS offers isn’t a normal unix shell. It’s just a command line interface for RouterOS commands.
Who am I?!
Fortunately, I have a work around that will get us root. RouterOS will execute anything stored in the /rw/DEFCONF file due the way the rc.d script S12defconf is written.
Friends don’t let friends use eval
A normal user has no access to that file, but thanks to the magic of VMs and Live CDs you can create the file and insert any commands you want. The exact process takes too many words to explain. Instead I made a video. The screen recording is five minutes long and it goes from VM installation all the way through root telnet access.
With root telnet access you have full control of the VM. You can upload more tooling, attach to processes, watch logs, etc. You’re now ready to explore the router’s attack surface.
Is Anyone Listening?
You can quickly determine the network reachable attack surface thanks to the
ps
command.
Looks like the router listens on some well known ports (HTTP, FTP, Telnet, and SSH), but also some lesser known ports. btest on port 2000 is the bandwidth-test server. mproxy on 8291 is the service that WinBox interfaces with. WinBox is an administrative tool that runs on Windows. It shares all the same functionality as the Telnet, SSH, and HTTP interfaces.
Hello, I load .dll straight off the router. Yes, that has been a problem. Why do you ask?
The Real Attack Surface
The
ps
output makes it appear as if there are only a few binaries to bug hunt in. But nothing could be further from the truth. Both the HTTP server and Winbox speak a custom protocol that I’ll refer to as WinboxMessage (the actual code calls it
nv::message
). The protocol specifies which binary a message should be routed to. In truth, with all packages installed, there are about 90 different network reachable binaries that use the WinboxMessage protocol.
There’s also an easy way to figure out which binaries I’m referring to. A list can be found in each package’s /nova/etc/loader/*.x3 file. x3 is a custom file format so I wrote a parser. The example output goes on for a while so I snipped it a bit.
The x3 file also contains each binary’s “SYS TO” identifier. This is the identifier that the WinboxMessage protocol uses to determine where a message should be handled.
Me Talk WinboxMessage Pretty One Day
Knowing which binaries you should be able to reach is useful, but actually knowing how to communicate with them is quite a bit more important. In this section, I’ll walk through a couple of examples.
Getting Started
Let’s say I want to talk to /nova/bin/undo. Where do I start? Let’s start with some code. I’ve written a bunch of C++ that will do all of the WinboxMessage protocol formatting and session handling. I’ve also created a skeleton programthat you can build off of.
Winbox_Session winboxSession(ip, port); if (!winboxSession.connect()) { std::cerr << "Failed to connect to the remote host" << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS;
You can see the Winbox_Session class is responsible for connecting to the router. It’s also responsible for authentication logic as well as sending and receiving messages.
Now, from the output above, you know that /nova/bin/undo has a SYS TO identifier of 17. In order to reach undo, you need to update the code to create a message and set the appropriate SYS TO identifier (the new part is bolded).
Winbox_Session winboxSession(ip, port); if (!winboxSession.connect()) { std::cerr << "Failed to connect to the remote host" << std::endl; return EXIT_FAILURE; }
WinboxMessage msg; msg.set_to(17);
Command and Control
Each message also requires a command. As you’ll see in a little bit, each command will invoke specific functionality. There are some builtin commands (0xfe0000–0xfe00016) used by all handlers and some custom commands that have unique implementations.
Pop /nova/bin/undo into a disassembler and find the
nv::Looper::Looper
constructor’s only code cross reference.
Follow the offset to vtable that I’ve labeled undo_handler and you should see the following.
This is the vtable for undo’s WinboxMessage handling. A bunch of the functions directly correspond to the builtin commands I mentioned earlier (e.g. 0xfe0001 is handled by
nv::Handler::cmdGetPolicies
). You can also see I’ve highlighted the unknown command function. Non-builtin commands get implemented there.
Since the non-builtin commands are usually the most interesting, you’re going to jump into
cmdUnknown
. You can see it starts with a command based jump table.
It looks like the commands start at 0x80001. Looking through the code a bit, command 0x80002 appears to have a useful string to test against. Let’s see if you can reach the “nothing to redo” code path.
You need to update the skeleton code to request command 0x80002. You’ll also need to add in the send and receive logic. I’ve bolded the new part.
After compiling and executing the skeleton you should get the expected, “nothing to redo.”
albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i 10.0.0.104 -p 8291 req: {bff0005:1,uff0006:1,uff0007:524290,Uff0001:[17]} resp: {uff0003:2,uff0004:2,uff0006:1,uff0008:16646150,sff0009:'nothing to redo',Uff0001:[],Uff0002:[17]} nothing to redo albinolobster@ubuntu:~/routeros/poc/skeleton/build$
There’s Rarely Just One
In the previous example, you looked at the main handler in undo which was addressable simply as 17. However, the majority of binaries have multiple handlers. In the following example, you’ll examine /nova/bin/mproxy’s handler #2. I like this example because it’s the vector for CVE-2018–14847and it helps demystify these weird binary blobs:
My exploit for CVE-2018–14847 delivers a root shell. Just sayin’.
Hunting for Handlers
Open /nova/bin/mproxy in IDA and find the
nv::Looper::addHandler
import. In 6.42.11, there are only two code cross references to
addHandler
. It’s easy to identify the handler you’re interested in, handler 2, because the handler identifier is pushed onto the stack right before
addHandler
is called.
If you look up to where
nv::Handler*
is loaded into
edi
then you’ll find the offset for the handler’s vtable. This structure should look very familiar:
Again, I’ve highlighted the unknown command function. The unknown command function for this handler supports seven commands:
Opens a file in /var/pckg/ for writing.
Writes to the open file.
Opens a file in /var/pckg/ for reading.
Reads the open file.
Cancels a file transfer.
Creates a directory in /var/pckg/.
Opens a file in /home/web/webfig/ for reading.
Commands 4, 5, and 7 do not require authentication.
Open a File
Let’s try to open a file in /home/web/webfig/ with command 7. This is the command that the FIRST_PAYLOAD in the exploit-db screenshot uses. If you look at the handling of command 7 in the code, you’ll see the first thing it looks for is a string with the id of 1.
The string is the filename you want to open. What file in /home/web/webfig is interesting?
The real answer is “none of them” look interesting. But list contains a list of the installed packages and their version numbers.
Let’s translate the open file request into WinboxMessage. Returning to the skeleton program, you’ll want to overwrite the
set_to
and
set_command
code. You’ll also want to insert the
add_string
. I’ve bolded the new portion again.
Winbox_Session winboxSession(ip, port); if (!winboxSession.connect()) { std::cerr << "Failed to connect to the remote host" << std::endl; return EXIT_FAILURE; }
WinboxMessage msg; msg.set_to(2,2); // mproxy, second handler msg.set_command(7); msg.add_string(1, "list"); // the file to open msg.set_request_id(1); msg.set_reply_expected(true); winboxSession.send(msg);
You can see the response from the server contains u2:1818. Look familiar?
1818 is the size of the list
As this is running quite long, I’ll leave the exercise of reading the file’s content up to the reader. This very simple CVE-2018–14847 proof of concept contains all the hints you’ll need.
Conclusion
I’ve shown you how to get the RouterOS software and root a VM. I’ve shown you the attack surface and taught you how to navigate the system binaries. I’ve given you a library to handle Winbox communication and shown you how to use it. If you want to go deeper and nerd out on protocol minutiae then check out my talk. Otherwise, you now know enough to be dangerous.
CVE-2019-5736 is yet another Linux vulnerability discovered in the core runC container code. The runC tool is described as a lightweight, portable implementation of the Open Container Format (OCF) that provides container runtime.
CVE-2019-5736 Technical Details
The security flaw potentially affects several open-source container management systems. Shortly said, the flaw allows attackers to get unauthorized, root access to the host operating system, thus escaping Linux container.
In more technical terms, the vulnerability:
allows attackers to overwrite the host runc binary (and consequently obtain host root access) by leveraging the ability to execute a command as root within one of these types of containers: (1) a new container with an attacker-controlled image, or (2) an existing container, to which the attacker previously had write access, that can be attached with docker exec. This occurs because of file-descriptor mishandling, related to /proc/self/exe, as explained in the official advisory.
The CVE-2019-5736 vulnerability was unearthed by open source security researchers Adam Iwaniuk and Borys Popławski. However, it was publicly disclosed by Aleksa Sarai, a senior software engineer and runC maintainer at SUSE Linux GmbH on Monday.
“I am one of the maintainers of runc (the underlying container runtime underneath Docker, cri-o, containerd, Kubernetes, and so on). We recently had a vulnerability reported which we have verified and have a patch for,” Sarai wrote.
The researcher also said that a malicious user would be able to run any command (it doesn’t matter if the command is not attacker-controlled) as root within a container in either of these contexts:
– Creating a new container using an attacker-controlled image. – Attaching (docker exec) into an existing container which the attacker had previous write access to.
It should also be noted that CVE-2019-5736 isn’t blocked by the default AppArmor policy, nor by the default SELinux policy on Fedora[++], due to the fact that container processes appear to be running as container_runtime_t.
Nonetheless, the flaw is blocked through correct use of user namespaces where the host root is not mapped into the container’s user namespace.
Red Hat says that the flaw can be mitigated when SELinux is enabled in targeted enforcing mode, a condition which comes by default on RedHat Enterprise Linux, CentOS, and Fedora.
There’s also a patch released by the maintainers of runC available on GitHub. Please note that all projects which are based on runC should apply the patches themselves.
Who’s Affected?
Debian and Ubuntu are vulnerable to the vulnerability, as well as container systems running LXC, a Linux containerization tool prior to Docker. Apache Mesos container code is also affected.
Companies such as Google, Amazon, Docker, and Kubernetes are have also released fixes for the flaw.
In January 2019, I discovered a privilege escalation vulnerability in default installations of Ubuntu Linux. This was due to a bug in the snapd API, a default service. Any local user could exploit this vulnerability to obtain immediate root access to the system.
dirty_sockv1: Uses the ‘create-user’ API to create a local user based on details queried from the Ubuntu SSO.
dirty_sockv2: Sideloads a snap that contains an install-hook that generates a new local user.
Both are effective on default installations of Ubuntu. Testing was mostly completed on 18.10, but older verions are vulnerable as well.
The snapd team’s response to disclosure was swift and appropriate. Working with them directly was incredibly pleasant, and I am very thankful for their hard work and kindness. Really, this type of interaction makes me feel very good about being an Ubuntu user myself.
TL;DR
snapd serves up a REST API attached to a local UNIX_AF socket. Access control to restricted API functions is accomplished by querying the UID associated with any connections made to that socket. User-controlled socket peer data can be affected to overwrite a UID variable during string parsing in a for-loop. This allows any user to access any API function.
With access to the API, there are multiple methods to obtain root. The exploits linked above demonstrate two possibilities.
Background — What is Snap?
In an attempt to simplify packaging applications on Linux systems, various new competing standards are emerging. Canonical, the makers of Ubuntu Linux, are promoting their “Snap” packages. This is a way to roll all application dependencies into a single binary — similar to Windows applications.
The Snap ecosystem includes an “app store” where developers can contribute and maintain ready-to-go packages.
Management of locally installed snaps and communication with this online store are partially handled by a systemd service called “snapd”. This service is installed automatically in Ubuntu and runs under the context of the “root” user. Snapd is evolving into a vital component of the Ubuntu OS, particularly in the leaner spins like “Snappy Ubuntu Core” for cloud and IoT.
Vulnerability Overview
Interesting Linux OS Information
The snapd service is described in a systemd service unit file located at /lib/systemd/system/snapd.service.
Linux uses a type of UNIX domain socket called “AF_UNIX” which is used to communicate between processes on the same machine. This is in contrast to “AF_INET” and “AF_INET6” sockets, which are used for processes to communicate over a network connection.
The lines shown above tell us that two socket files are being created. The ‘0666’ mode is setting the file permissions to read and write for all, which is required to allow any process to connect and communicate with the socket.
We can see the filesystem representation of these sockets here:
$ ls -aslh /run/snapd*
0 srw-rw-rw- 1 root root 0 Jan 25 03:42 /run/snapd-snap.socket
0 srw-rw-rw- 1 root root 0 Jan 25 03:42 /run/snapd.socket
Interesting. We can use the Linux “nc” tool (as long as it is the BSD flavor) to connect to AF_UNIX sockets like these. The following is an example of connecting to one of these sockets and simply hitting enter.
$ nc -U /run/snapd.socket
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Connection: close
400 Bad Request
Even more interesting. One of the first things an attacker will do when compromising a machine is to look for hidden services that are running in the context of root. HTTP servers are prime candidates for exploitation, but they are usually found on network sockets.
This is enough information now to know that we have a good target for exploitation — a hidden HTTP service that is likely not widely tested as it is not readily apparent using most automated privilege escalation checks.
NOTE: Check out my work-in-progress privilege escalation tool uptux that would identify this as interesting.
Vulnerable Code
Being an open-source project, we can now move on to static analysis via source code. The developers have put together excellent documentation on this REST API available here.
The API function that stands out as highly desirable for exploitation is “POST /v2/create-user”, which is described simply as “Create a local user”. The documentation tells us that this call requires root level access to execute.
But how exactly does the daemon determine if the user accessing the API already has root?
Reviewing the trail of code brings us to this file (I’ve linked the historically vulnerable version).
This is calling one of golang’s standard libraries to gather user information related to the socket connection.
Basically, the AF_UNIX socket family has an option to enable receiving of the credentials of the sending process in ancillary data (see
man unix
from the Linux command line).
This is a fairly rock solid way of determining the permissions of the process accessing the API.
Using a golang debugger called delve, we can see exactly what this returns while executing the “nc” command from above. Below is the output from the debugger when we set a breakpoint at this function and then use delve’s “print” command to show what the variable “ucred” currently holds:
That looks pretty good. It sees my uid of 1000 and is going to deny me access to the sensitive API functions. Or, at least it would if these variables were called exactly in this state. But they are not.
Instead, some additional processing happens in this function, where connection info is added to a new object along with the values discovered above:
..and is finally parsed by this function, where that combined string is broken up again into individual parts:
func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, socket string, err error) {
...
for _, token := range strings.Split(remoteAddr, ";") {
var v uint64
...
} else if strings.HasPrefix(token, "uid=") {
if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil {
uid = uint32(v)
} else {
break
}
What this last function does is split the string up by the “;” character and then look for anything that starts with “uid=”. As it is iterating through all of the splits, a second occurrence of “uid=” would overwrite the first.
If only we could somehow inject arbitrary text into this function…
Going back to the delve debugger, we can take a look at this “remoteAddr” string and see what it contains during a “nc” connection that implements a proper HTTP GET request:
Request:
$ nc -U /run/snapd.socket
GET / HTTP/1.1
Host: 127.0.0.1
Debug output:
github.com/snapcore/snapd/daemon.ucrednetGet()
...
=> 41: for _, token := range strings.Split(remoteAddr, ";") {
...
(dlv) print remoteAddr
"pid=5127;uid=1000;socket=/run/snapd.socket;@"
Now, instead of an object containing individual properties for things like the uid and pid, we have a single string variable with everything concatenated together. This string contains four unique elements. The second element “uid=1000” is what is currently controlling permissions.
If we imagine the function splitting this string up by “;” and iterating through, we see that there are two sections that (if containing the string “uid=”) could potentially overwrite the first “uid=”, if only we could influence them.
The first (“socket=/run/snapd.socket”) is the local “network address” of the listening socket — the file path the service is defined to bind to. We do not have permissions to modify snapd to run on another socket name, so it seems unlikely that we can modify this.
But what is that “@” sign at the end of the string? Where did this come from? The variable name “remoteAddr” is a good hint. Spending a bit more time in the debugger, we can see that a golang standard library (net.go) is returning both a local network address AND a remote address. You can see these output in the debugging session below as “laddr” and “raddr”.
The remote address is set to that mysterious “@” sign. Further reading the
man unix
help pages provides information on what is called the “abstract namespace”. This is used to bind sockets which are independent of the filesystem. Sockets in the abstract namespace begin with a null-byte character, which is often displayed as “@” in terminal output.
Instead of relying on the abstract socket namespace leveraged by netcat, we can create our own socket bound to a file name that we control. This should allow us to affect the final portion of that string variable that we want to modify, which will land in the “raddr” variable shown above.
Using some simple python code, we can create a file name that has the string “;uid=0;” somewhere inside it, bind to that file as a socket, and use it to initiate a connection back to the snapd API.
Here is a snippet of the exploit POC:
## Setting a socket name with the payload included
sockfile = "/tmp/sock;uid=0;"
## Bind the socket
client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client_sock.bind(sockfile)
## Connect to the snap daemon
client_sock.connect('/run/snapd.socket')
Now watch what happens in the debugger when we look at the remoteAddr variable again:
> github.com/snapcore/snapd/daemon.ucrednetGet()
...
=> 41: for _, token := range strings.Split(remoteAddr, ";") {
...
(dlv) print remoteAddr
"pid=5275;uid=1000;socket=/run/snapd.socket;/tmp/sock;uid=0;"
There we go — we have injected a false uid of 0, the root user, which will be at the last iteration and overwrite the actual uid. This will give us access to the protected functions of the API.
We can verify this by continuing to the end of that function in the debugger, and see that uid is set to 0. This is shown in the delve output below:
dirty_sockv1 leverages the ‘POST /v2/create-user’ API function. To use this exploit, simply create an account on the Ubuntu SSO and upload an SSH public key to your profile. Then, run the exploit like this (using the email address you registered and the associated SSH private key):
$ dirty_sockv1.py -u you@email.com -k id_rsa
This is fairly reliable and seems safe to execute. You can probably stop reading here and go get root.
Still reading? Well, the requirement for an Internet connection and an SSH service bothered me, and I wanted to see if I could exploit in more restricted environments. This leads us to…
Version Two
dirty_sockv2 instead uses the ‘POST /v2/snaps’ API to sideload a snap containing a bash script that will add a local user. This works on systems that do not have the SSH service running. It also works on newer Ubuntu versions with no Internet connection at all. HOWEVER, sideloading does require some core snap pieces to be there. If they are not there, this exploit may trigger an update of the snapd service. My testing shows that this will still work, but it will only work ONCE in this scenario.
Snaps themselves run in sandboxes and require digital signatures matching public keys that machines already trust. However, it is possible to lower these restrictions by indicating that a snap is in development (called “devmode”). This will give the snap access to the host Operating System just as any other application would have.
Additionally, snaps have something called “hooks”. One such hook, the “install hook” is run at the time of snap installation and can be a simple shell script. If the snap is configured in “devmode”, then this hook will be run in the context of root.
I created a snap from scratch that is essentially empty and has no functionality. What it does have, however, is a bash script that is executed at install time. That bash script runs the following commands:
The commands below show the process of creating this snap in detail. This is all done from a development machine, not the target. One the snap is created, it is converted to base64 text to be included in the full python exploit.