Introducing lorito, an HTTP security suite
You’re participating in a bug bounty program and the target application accepts a user-controlled URL. How can you exploit that?
Over the years, the most common solution is a custom server code (usually PHP) doing the exploitation. And there’s a lot of work around that: managing a web server and certificates, editing remote files, adding more files, backing up working files, verifying that the request was received, debugging request headers, etc.
I did that in the past for specific cases. For instance, in Exploiting the telnet service in scrapy < 1.5.2, I had to set up a custom web server replying to an invalid HTTP method GET ='
.
But in more or less generic exploitation, we can do better. This post introduces lorito, a new open-source project to handle web exploitation and more!
Introducing lorito
lorito is a web application that enables you to exploit various classes of web vulnerabilities where the target application connects to your server. Please watch the introductory video to find out how it works!
Among the vulnerabilities you could exploit, here are a few examples:
- Server-Side Request Forgery (SSRF)
- Time of Check Time of Use (TOCTOU)
- Document-specific vulnerabilities exploiting parsers i.e. XML external entity (XXE) injection
- Blind Cross Site Scripting (XSS)
The rest of this post discusses a vulnerable web application and how lorito assists in exploiting the aforementioned vulnerabilities.
Server-Side Request Forgery (SSRF)
The initial version of the vulnerable web application can be found in the next snippet:
import requests
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI()
def is_safe_to_request(url: str):
"""
DNS resolution of `url` to avoid requests to the internal network.
Returns `False` if it's an internal network IP.
Otherwise `True`.
"""
...
@app.get("/admin")
def admin(request: Request):
if request.client.host == "127.0.0.1":
return PlainTextResponse(content="fl4g_l0r1t0_4dm1n")
@app.get("/fetch")
def fetch(url: str):
if is_safe_to_request(url):
res = requests.get(url)
return {"msg": res.text}
It’s listening on port 8000
and exposed as vulnerable.tld.
There are two endpoints:
/admin
: the admin section only reachable from localhost/fetch
: an endpoint accepting aurl
parameter
As you may guess, the goal is to get the flag from /admin
.
The only accessible endpoint is /fetch
, which includes a check that discards any url
pointing to an internal network IP. For this kind of cases, SSRF is still possible with the redirect trick.
Let’s use lorito to create a response with status code 307
and add the header Location: http://localhost:8000/admin
to bypass the SSRF protection.
Time of Check Time of Use (TOCTOU)
The developer found out about the redirect trick so he will validate that url
is not doing any redirect.
def is_safe_to_request(url: str):
"""
DNS resolution of `url` to avoid requests to the internal network.
Returns `False` if it's an internal network IP.
Otherwise `True`.
"""
...
res = requests.get(url, allow_redirects=False)
return not_internal_ip and res.status_code == 200
@app.get("/fetch")
def fetch(url: str):
if is_safe_to_request(url):
res = requests.get(url)
return {"msg": res.text}
In /fetch
endpoint we have a TOCTOU vulnerability, since the response to the requests sent by both is_safe_to_request
and fetch
might be different. The plan would be:
- First request to
url
: return a200
response - Second request to
url
: return the redirect trick response
Let’s introduce two powerful lorito capabilities:
- HTTP rebinding (same concept as DNS rebinding) to change responses for the same route manually.
- Latency to add a delay of
N
seconds before the response is returned. TOCTOU usually means short timeframes between the check and use, but latency gives us control over that.
In the video below, I first test that the redirect trick alone from the previous section doesn’t work against the new implementation. Then, I add a response for the same route, which creates a rebinding 🐙. This new response returns status code 200
, has a latency of 10
seconds and becomes the active response for that route.
Once /fetch
is called from Postman, lorito returns the active response and the delay takes place. In the meanwhile, I switch the active response in the rebinding to the response with status code 307
so the next request arriving 10 seconds later will receive the redirect trick response. As seen in Postman, the exploitation is successful.
Blind XML External Entity (XXE) injection
The developer has injected a middleware in requests
and it’s not vulnerable to SSRF anymore. The endpoint evolved and now it expects that the response from url
is a XML document.
from lxml import etree
@app.get("/fetch")
def fetch(url: str):
res = requests.get(url)
parser = etree.XMLParser(no_network=False)
etree.fromstring(res.text, parser=parser)
return {"result": "ok"}
That configuration for lxml
is vulnerable to XML External Entity (XXE) injection. However, the endpoint doesn’t return the output so it’s a blind injection. We’re going to follow the instructions from PortSwigger to exploit this and introduce two lorito features: URL references and response placeholders.
For the exploitation, we need 2 responses: payload.dtd
and file.xml
. Let’s start with payload.dtd
:
The first entity gets the flag from /admin
endpoint. The second exfiltrates the flag to our server. Response bodies in lorito support Liquid template language and workspace_url
is a variable making reference to the workspace URL.
I also use custom Liquid filter called http_protocol
to downgrade workspace_url
from https
to http
since there were a few issues between lxml
and https
.
Then, file.xml
looks like:
The 🍉 is a response placeholder. Instead of hardcoding workspace’s routes, please use placeholders. If you change the name of payload.dtd
to pl.dtd
, thanks to 🍉 file.xml
will point to the new route automatically. See it in action!
Blind Cross Site Scripting
The developer adds more logic to the admin panel and now it renders the user agent from the last visitor. This information is extracted from a new endpoint /index
.
@app.get("/admin")
def admin(request: Request, response_class=HTMLResponse):
if request.client.host == "127.0.0.1":
global last_user_agent
content = """
<html>
<body>
<h3>Last visit</h3>
<p>Last user agent: {}</p>
<h1>flag: fl4g_l0r1t0_4dm1n</h1>
</body>
</html>
""".format(last_user_agent)
return HTMLResponse(content=content, status_code=200)
@app.get("/index")
def index(
response_class=HTMLResponse,
user_agent: Annotated[str | None, Header()] = None
):
global last_user_agent
last_user_agent = user_agent
return "<html><head><title>Index</title></head></html>"
This modification introduced a Cross Site Scripting (XSS). To exploit it, we should inject our payload in the request to /index
and then wait for the developer to visit /admin
to execute our payload. A classic example of Blind XSS.
To exploit this, we’re going to modify XSSHunter’s probe:
- Replace
[HOST_URL]
with{{workspace_url}}
- Keep the screenshot as base64 so it will be rendered by lorito 📸
The result is here. Where do we place this file? We’re going to introduce other two lorito features: templates and copy payloads.
Templates are suitable when your exploitation logic is independent of the target application and you want to maximize their use. In lorito, you can create workspaces entirely based on a template, which essentially gives you a new URL.
As you can imagine, using a template for your blind XSS probe + lorito projects and workspaces allows to create fine-grained configurations.
The other feature is copy payloads. I let you watch the video to discover that 🔥
Elixir & Deployment
lorito is an Elixir application built on top of LiveView.
A few words about Elixir ❤: if you have a click watching The Soul of Erlang and Elixir • Sasa Juric and reading Programming Elixir 1.6, Elixir might be your next language of choice! I stopped writing CLIs and I go straight with a LiveView app.
For deployment, lorito requires a PostgreSQL database. Elixir projects can be deployed in several ways but my recommendation is to use Fly.io, it comes with custom domain support, SSL certificates and affordable costs.
Please check the guide to deploy lorito on Fly.io.
Licensing and Lorito Pro
lorito is an open-core project:
- Self-hosted open-source version is licensed under AGPL-3.0 license. Any security researcher can deploy an instance and use it.
- There will be a SaaS version called Lorito Pro, with a focus on enterprise needs (user management, RBAC, backups, etc). Join the waitlist to be notified when it’s alive!
Community
If you have any questions, feel free to start a discussion in Github. The same if you find a bug, please report it using Issues.
Happy hacking!