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.
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
.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
ServiceBase.OnStart(string args) method is overridden by
Let’s follow the
ServiceWorker.StartService() which calls
ServiceWorker._serviceLifetimeThread = new Thread(new ThreadStart(ServiceWorker.ServiceLifetimeFunction)) ending in a
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
We have some classes with namespace prefix
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:
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
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.
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”
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.
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.
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
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?