My Expense Report resulted in a Server-Side Request Forgery (SSRF) on Lyft
Introduction
During a trip to a conference, I discovered that the Lyft app allowed users to create expense reports by exporting business ride history as a PDF or CSV file. Being an active Lyft user, this was excellent news to me since it made my life easier by simplifying the tedious process of work travel expenses. But it also begged the question: “Can I hack this thing?” Turned out, the answer is yes, thanks to my collaboration with Cody Brocious (@Daeken)
How Does It Work?
Once you complete a ride and rate or tip your driver, you are prompted with the following image that allows you to attach an expense code and note to it. Naturally, as I ended my ride at the airport, I placed an HTML tag for my expense info which allowed me access to an entirely new interface under the “Ride History” tab on the Lyft app. It showed me a section where it allowed me to select which rides I wanted to export into my expense repor.Once I selected my rides for my expenses, the Lyft application sent out an email where I received my expenses in two formats: CVS and PDF. And by opening the PDF I was able to confirm that the html tag (<h1>test) placed inside the “Expense Notes” was successfully rendered within the PDF:
This immediately caught my attention. I wanted to see if I would be able to exploit the PDF generator with SSRF being the possible outcome.
Exploring SSRF
Once we confirmed that we could insert HTML into the PDF generator, the next step was to see if we could actually get the app to fetch external resources to gather information (such as user-agent), which would help us understand the application better. Keep in mind that this also required us to take a Lyft each time we wanted to try our payloads. We dedicated a few rides to getting the user agent by forcing the PDF generator to fetch a remote file from a web server controlled by us, using tags like <iframe> and <img>. But, unfortunately, we weren’t able to get any of that information at the time.
A few weeks later, HackerOne was hosting a Live Hacking Event in New York, which allowed us to take a ton of rides using the Lyft app and it was a great opportunity to revisit this bug. Our focus this time was to understand why some tags like <h1> or <u>
were working in comparison to <img>
or <iframe>
. As mentioned earlier, the email also contains a CSV file exposing the exact string set as the expense code without rendering it. "Seeing this showed us our phone typo entering the payload, ‘left/right double quotation mark’ “
vs a regular quotation mark ".
Once we fixed this in our original payload, we took a ride, where we were able to get the PDF generator’s User-Agent, which shifted our focus from Lyft’s application to WeasyPrint instead.
WeasyPrint
WeasyPrint is a smart solution helping web developers create PDF documents. It turns simple HTML pages into gorgeous invoices, tickets, statistical reports… and it turns out it is also open sourced. Using WeasyPrint, you are able to create PDF files by feeding it an html template or URL. I have created a video on how to exploit WeasyPrint on YouTube, you should definitely check it out!
Here’s how WeasyPrint works, it takes an html template and creates a pdf from it. You can do this locally with the following command:
$> weasyprint input.html output.pdf
After installing an instance of it locally, we were able to understand how it all worked. This made our testing a lot easier since we no longer needed to take rides in order to test our payloads. After a few attempts without reviewing the source, we had an initial understanding of how WeasyPrint worked:
It allows a small number of HTML tags
No Javascript was allowed
No iframe or similar tags were allowed
After we reviewed a few files and discovered some interesting things in html.py, WeasyPrint had redefined a set of html tags including img, embed, object, and more. Based on our previous tests, we already knew that javascript was not an option in order to exploit this. At this point, our hopes were low and we started to think that the PDF generator was no longer exploitable until we discovered references to <link> inside of several files including pdf.py. This allowed us to attach the content of any webpage or local file to our PDF by using <link rel=attachment href="file:///root/secret.txt">
.
Using zlib and python, we created a script that helped us unpack the content of local files from our pdf.
import sys, zlib def main(fn): data = open(fn, 'rb').read() i = 0 first = True last = None while True: i = data.find(b'>>\nstream\n', i) if i == -1: break i += 10 try: last = cdata = zlib.decompress(data[i:]) if first: first = False else: pass#print cdata except: pass print(last.decode('utf-8')) if __name__=='__main__': main(*sys.argv[1:])
One Last Ride
This gave us a working POC on our localhost, so we took one last ride using the Lyft app to test our payload and we were able to confirm the existence of this bug.
Credits
A big thank you to the Lyft team, especially Vinay, for answering all of our questions and working closely with us to get this issue fixed.
Thank you to Daeken for his brilliant ideas and being a great resource throughout this process.
Thank you to @d0nutptr for helping validate this vulnerability by taking a few rides for me while I was on an airplane.
If you want to hear more about the fun we had while exploiting this issue, check out my video on YouTube: Exploiting a Server Side Request Forgery (SSRF) in WeasyPrint for Bug Bounty & HackerOne’s $50M CTF
Timeline
10.11.2018 - Initial finding by NahamSec
11.29.2018 - Working POC in NYC with @Daeken
11.29.2018 - Initial report to Lyft
11.29.2018 - Patch released by Lyft
11.30.2018 - Lyft notified us that the vulnerability was patched
12.05.2018 - Lyft awarded us with a max bounty on their program