SmarterStats - Yet Another RPC Framework
First of all, the SmarterTools team is pretty cool, a vendor I practice responsible disclosure with pleasure. I also needed some positive vendor vibes after my last experiences. I already worked with them successfully in the past. They provide a bunch of software products, one of them called SmarterStats: a web log analytics suite measuring the popularity of your websites given certain metrics. The installation of a trial version is straight forward and the code based on ASP .NET. The web interface can be reached at TCP port 9999
. According to Censys.io there exist a few thousand instances on the public internet with several hundreds also exposing TCP port 50003
(this will become relevant later). For our code audit we installed the latest version Build 8011 (Dec 7, 2021) available at that time. The patched version was published a few days ago (June 9th 2022) as Build 8195.
First, I usually check the IIS manager inetmgr
for the deployment structure, web root directories etc.
Second, the inetmgr
lists the HTTP handlers for which we’re especially interested in custom handlers.
Even more interesting are HTTP modules because according to the ASP .NET workflow, these are usually called before any authentication checks trigger. A nice example for this kind of entry-point leading to Pre-Auth Remote Code Execution (RCE) was shown by my colleague in this blog on Citrix ShareFile. Unfortunately, no custom HTTP modules can be identified for SmarterStats.
Next, we check for all .aspx
, .ascx
and .asmx
files in C:\Program Files (x86)\SmarterTools\SmarterStats\MRS
for potential vulnerabilities. As you might notice, most of these files contain references to C# code base such as
<%@ WebService Language="C#" CodeBehind="UserAdmin.asmx.cs" Class="SSWeb.Services.UserAdmin" %>
So, you search for the proper IIS worker process w3wp.exe
serving your SmarterStats application and load it into dnSpy. Then load all the modules related to the process and sort the .NET assemblies for a first overview. I like to sort them because it’s a quick visual check for custom vs. .NET framework assemblies by name.
Remember to browse the application before attaching to the IIS worker process. Otherwise, you might not see all the “cryptic” Asp_Web_junk
modules with their corresponding .NET code.
After digging through some code, I realize that our ultimate goal “Pre-Auth RCE” is at risk. But we only checked the web-interface at TCP port 9999
so far. Back in 2020 I reported another Pre-Auth RCE for SmarterStats affecting a service available at TCP port 50003
. This was fixed in Build 7422 (Apr 27, 2020). Back then, the RCE was based on the fact that they used .NET Remoting. So, let’s check if this port is still used by a SmarterStats process by default after a fresh installation. Sysinternals TCPView says “yes”.
Loading the process SSSvc.exe
into dnSpy again, let’s investigate how this works under the hood. It quickly becomes clear that .NET Remoting is not used anymore which is good, right?
The module name shown in TCPView was called SSCollect
so we search for this module in the code base. We find SStatSvc.SSCollect
extending System.ServiceProcess.ServiceBase
. The ServiceBase.OnStart(string[] args)
method is overridden by SSCollect
.
Let’s follow the ServiceWorker.StartService()
which calls ServiceWorker._serviceLifetimeThread = new Thread(new ThreadStart(ServiceWorker.ServiceLifetimeFunction))
ending in a ServiceWorker.Start()
.
Here, things are getting more specific and interesting. During this initialization procedure we find a call to GrpcManager.StartGrpc()
which nicely matches with a set of assemblies we spotted after loading all modules used by the SSSvc.exe
process: Grpc.Core.dll
and Grpc.Core.Api.dll
.
We have some classes with namespace prefix SmarterStats.Config.Protos
calling BindService
methods with distinct implementation classes. We also spot the TCP port 50003
and another interesting fact giving us confidence for another Pre-Auth chance: ServerCredentials.Insecure
.
I already heard about gRPC and protocol buffers before but honestly didn’t look into programmatic approaches too much. So what to do first? Reading documentation like a beginner. I start with this, telling me something about remote method invocation. From a security research perspective, this topic is often related to Java (RMI), .NET (Remoting) and more. gRPC uses so called protocol buffers by default to send serialized data over the wire. But this is not as dangerous as you might think. Data structures for automatically generated client stubs and server service skeletons are derived from .proto
files.
Unfortunately, we don’t have these files…so let’s look into the server code again. Starting with the first gRPC type SmarterStats.Config.Protos.Query
we see something interesting.
There is a class named SmarterStats.Config.Protos.QueryClient
. Hooray! We try to find the implementation now through all these virtual, abstract, override
function definitions. We start again in SmarterStats.Config.Protos.Query
where the method BindService(ServiceBinderBase serviceBinder, Query.QueryBase serviceImpl)
is implemented.
Choosing one random function of this specific service, virtual functions such as Task<GetAvailableQueriesWithInputsReply> GetAvailableQueriesWithInputs(GetAvailableQueriesWithInputsRequest request, ServerCallContext context)
bring us a step further to the real business code.
In SStatSvc.Communication.QueryServiceImplementation
this function is overridden
and we finally reach the business code
The request input class SmarterStats.Config.Protos.GetAvailableQueriesWithInputsRequest
seems to be pretty empty, i.e. no obvious user-controllable attributes exist. But this is somehow expected since the remote method name GetAvailableQueries
indicates that probably no further input is needed. Fine, now how do we get a working client running?
The documentation “A basic tutorial introduction to gRPC in C#” sounds like a good starting point. We clone the example project RoutingGuide and try to understand the project structure and code within. Also I’m really lazy which means… simply reuse the code to create a SmarterStats client.
We directly switch to the RouteGuideClient part of the Visual Studio solution. gRPC assemblies are already set up for us so only the SmarterStats types are needed. Checking with dnSpy again, we then add the reference C:\Program Files (x86)\SmarterTools\SmarterStats\Service\SmarterStats.Config.dll
to our solution.
Let’s write some really dumm code (I’m allowed to do this because I worked as a software developer years ago (⌐ ͡■ ͜ʖ ͡■)).
Running our “meaningless” RouteGuideClient.exe
works!
Now, let’s hunt for some Pre-Auth RCE bugs! We go through all binding service implementations step by step and stop at SStatSvc.Communication.ServiceOperationsServiceImplementation
because it contains a lot of interestingly sounding methods. The method GetExportedLogsForSite(GetExportedLogsForSiteRequest request, IServerStreamWriter<GetExportedLogsForSiteResponse> responseStream, ServerCallContext context)
e.g. has the word “Export” in it.
The call Path.Combine(Constants.ServiceTemporaryDirectory, this.request.FileToDownload)
already tells us everything we want to know: user-controlled file name + path traversal opportunity?! Let’s test this with our new gRPC knowledge.
And indeed, we can read the configuration file with credentials and a bunch of sensitive information.
What Path.Combine
can do for you (in different flavors), have a look at my previous blog on 3CX Pwnage. Pre-Auth Arbitrary File Read as NT AUTHORITY\SYSTEM
achieved.
So what would be a good method candidate for Remote Code Execution then? SaveFileTo(SaveFileToRequest request, ServerCallContext context)
sounds promising.
But the “Unauthorized to copy” could become a problem. Let’s see if it really is. The authorization check consists of taking a request parameter this.request.Auth
and puts it into a “crypto check” cryptographyHelper.DecodeFromBase64
method. The result has to match our user-controlled value this.request.Filename
. The DecodeFromBase64
method indeed decodes the value from a Base64 format and operates with some crypto on the result.
But did you also spot the cryptographyHelper.SetKey(creationTime.ToString("MMddyyyy") + " ksghsfkgjh", null)
? Yes, creationTime
comes from this.request.CreationDate.ToDateTime()
which we control as well. Does this mean we control the crypto key and initialization vector? Let’s write some more ugly code.
Execute the modified client
and we have written a file
which can be called
giving us Pre-Auth Remote Code Execution again. At the end of the day, we found a second Pre-Auth RCE for the service at TCP port 50003
(2020, 2022).
P.S.: I didn’t check all the other gRPC binding implementations but rather told the vendor to check the others for similar flaws themselves. Maybe you can find others! What I did see was a small opportunity for another Pre-Auth RCE in the patched version. Can you spot it as well?