can you hack this screenshot service?? - CSCG 2021

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
“Screenshotter is a novel web app to  quickly take notes and screenshots.  To make screenshots, simply enter the URL of a  website and the app will store a screenshot of it.   The site is currently in beta, so  only cscg.de screenshots are allowed.” This is a web hacking CTF challenge that I  created for the Cyber Security Challenge Germany.   And for all those people who scoff at  CTF challenges for being unrealistic,   well this is a completely realistic challenge.  I found this vulnerability in a web app at work,   which felt like a CTF challenge, so I got inspired  to create a challenge with the same vulnerability.   Of course I adapted it for the CTF, but believe  me, the steps for discovery and exploitation are   very close to what I found in real life. So if you want to try it yourself,   you can find the files on GitHub. I think this  challenge can be a really good exercise for doing   real-world security audits of webapps. So  why not give it a try and practice with it?  When you look at the provided files, you can see  it’s based on docker-compose, so if you have that   installed, you can just type “docker-compose up”  and wait for the containers to build, and then it   should be available on localhost port 5000. And yes, all of these files are part of the   challenge, so we can read any code and really dig  deep into the app to find the vulnerabilities.   Of course the DO_NOT_ACCESS  folder contains the solution.   So that is not part of the challenge.. So now  you have a few more seconds to pause the video,   otherwise I will go ahead and tell  you how to approach and solve this. Like with every challenge, the first  step should be to get an overview of   the functionality. Apparently you can login  or register, and this is a demo version which   can only create screenshots of http://cscg.de. There is also this activity tab showing you what’s   going on on the app. And this is here to show you,  that there is a user flagger which added a note,   and requested a screenshot via a worker  service chrome, and we see the screenshot   also got successfully processed. This base64 value  is obviously interesting, and this can be decoded   to an IP address. Sometimes base64 values are  stripped of the paddings, and then it doesn’t   decode nicely, so adding some equal signs here at  the end makes the output correct. So no clue yet   what this is for, but we obviously remember it. Now let’s try to register and login with a user.   Here simply a test user. And now we can add  a note! But the special feature is obviously   screenshots, so let’s try to add one by using  the test URL. http://cscg.de. It’s processing now   and when we wait a moment, we should  get a screenshot of the website.   Awesome! If we now look at the  activity log, we can see that now we   also appear here. Test user added a note.  And test user requested a screenshot. Cool.  Pretty basic web app, not much functionality.  But where is the flag? We will see that soon. Let’s head into the source code of the app. The  docker-compose file is always great to start with.   Because it describes the high-level architecture  or setup of this web application. And here we   have three services. We have a chrome service,  a screenshotter service and an admin service.   Each of these services are built from dockerfiles  contained in the respective folders. Screenshotter   appears to be the main app, it’s the one exposed  to port 5000. The other services have no ports   exposed. Depends_on is a field that ensures  that docker-compose first setup chrome service,   THEN the web app, and at the end the admin.  So let’s look at the services in this order.  First the chrome service. The chrome service is  a simple docker file. It basically only installs   chrome browser stable version, and then starts  chrome in headless mode. So no actual browser   window is opened. Also you can see here that  it enables the remote debugging port of chrome   9222. But this port is not actually  exposed, so we cannot connect to it.  Ok so this simply started chrome in  headless mode with remote debugging enabled.  Now let’s checkout the main app. It’s written in  python and more specifically the flask framework.  Before running the flask  server it executes init.py.  And here it seems to just initialize the sqlite  database. It creates a database with commands   from schema.sql. Checking that we can see that we  have user table, a notes table and a logs table.  And after setting up the SQLite database  it will also create a random secret.   No clue yet what this is used for. Next we have the main app   and right at the top we see that it tries to  find the IP address of the “chrome” service.   If we cannot get the IP dynamically,  it will fallback to localhost.  This IP will then be used in the screenshot  function to connect to the chrome debug port 9222.   And in this other place, it is base64 encoded  and added to the public log, so the activity log,   with the text “username requested a screenshot  via worker chrome”, and the IP address. So cool!   This means with this output we can get the  IP address of the internal chrome service. Now just to give a bit of context, this  IP exposure was not part of the real app   this was inspired from. I know from  my experience with docker, that there   are default IP ranges for docker containers.  Usually starting with 172..., so I don’t need an   IP disclosure to know what a docker service  internal IP could be, but I thought I could   make the challenge a bit easier, by adding this  basic IP exposure to make it a bit less guessy.   Also because I was not sure how the  challenge was going to be hosted,   I wanted to make sure that there is an easy way  to find the internal IP of the chrome worker,   in case it would not be the default docker  IP range. And it turns out on my windows PC   here the IP is not the expected 172. IP.  So I guess it was good that I added this.   If this were not here, you would have to take  educated guesses or scan the IPs if possible. Anyway. Just after we get the Chrome IP, we also  get the secret! And this secret is used in an   hmac function to create a signature of some  data. And this signature function is called   in before request, which acts as a “session  middleware.”. It checks if we have a valid   session in the cookie. So if we are logged in or  not. Feel free to read this code more closely to   better understand it, but it should all be safe.  I just implemented some basic user authentication   that is good enough for this challenge. So  spoiler alert: this should not be important. But how do we approach this now. It’s not a  huge web app, but it’s also not really short.   Still quite a bit of code. Actually when hunting  for bugs in web applications, I like to go for   the unique interesting functionality. You know  every app has to implement user authentication,   and stuff like creating notes, that’s all boring.  But this app is interesting because it can create   screenshots. And I like to go after these kind  of weird features, because this is where often   the really fun vulnerabilities hide. So let’s  checkout the API call that creates a note.   We can easily find that by looking at  burp or the browser developer tools   when we submit the cscg.de URL to request  a screenshot. So we simply call add_note,   and the body contains the URL. Ok so here is  the route handler for add_note in python. here   we get the request body and right after you can  see a check if the requested URL is a cscg.de.   If not we just add a regular note  by inserting it into the database.  But if we have a cscg.de URL, we  are starting a new screenshot task.  This will be executed in parallel in a thread. So  let’s look at this thread. This screenshot_task   thread takes the new note_id. this was generated  at the start of the function, and is also used   to create this placeholder note, “processing  screenshot”. So we are just passing the note_id   as a reference to the placeholder note. And we  also pass in the body, so the URL that we are   requesting. Inside this thread we are now calling  the async function screenshot. And this function   now connects to the chrome service! It is using pyppeteer for that. “Unofficial Python port of puppeteer  [which is a] JavaScript (headless)   chrome browser automation library.”. So  this is just a python library to automate   and control chrome. And it controls  chrome via the chrome debug port 9222.   And I think this code is pretty self explanatory.  We first create a new incognito browser window.   Then we create a new page. We navigate to  that URL. We sleep for 10 seconds to wait   for the page to be fully loaded, we extract  the title, and we also create a screenshot!.   And then we close the browser window again. This image is then stored in the database,   by updating the note we created  before as a placeholder. So far it looks pretty solid, right? Where is  the vulnerability now? Maybe you already noticed   a first issue. But first let’s checkout  to the third service. The admin service.   And here we can also find the flag.txt  file. So the admin has the flag.  The dockerfile starts similarly to the  chrome service, it also installs chrome,   but then doesn’t start a headless  browser. It simply executes admin.py.  This script opens the flag and then wants  the IP of the main webapp and creates a   flagger password. A random value. The main code then starts in main.   Where we execute an endless loop. So here  we also use pypeteer, but this time we do   not connect to a remote chrome service, but just  access the locally installed chrome. We create a   new browser and are now simulating a user. So this admin service is just simulating   another user using the screenshotter app. A user  who has the flag. So what does this user do?  The user opens a new page and  navigates to the screenshotter site.   It then waits for 2 seconds and then enters  the login credentials of the flagger account,   using the random and secure flagger password.  So we probably cannot bruteforce that.  After the login it will then add the flag note!  This admin user creates a note with the flag!   And after that it also requests a screenshot  from cscg.de. So now we know the goal. We need   to get access to the notes of the flagger user! After waiting for a bit. Sleeping for 10 seconds   and reloading the page, it will eventually  lookup all notes and then delete the notes that   were created. After that the page is closed,  and the next user simulation loop starts.  cool! To summarize, this is a simulated user,  who owns the flag, which means we need to somehow   attack this user. And given that this user is  using an actual browser to perform actions is a   big hint that the attack has to be browser-client  based. So we need something like a XSS or CSRF. Btw, if you are now inspired by this challenge  to find the vulnerabilities yourself,   now is your last change to pause. Because next I  will tell you about all the puzzle pieces. 3.2.1. So we just learned from reading  the code what the target user does,   and that we need some kind of client-side  attack to leak the notes of flagger.   Those notes contain the flag. So how can  we do that? Well an XSS is probably the   most typical and most powerful way to leak  information. So let’s try to find a XSS.  Feel free to try to find it yourself,  because I will just show you where it is.  This web app is using flask templates. I think  it’s using jinja2 templates under the hood. And   generally they are secure. When you use  curly braces to output user controlled data,   it is properly HTML encoded and safe. But  there is one exception. And it is here.  When you have a note that is an image, so a  screenshot, then the alt text of the image is   set to the title. But it’s missing the quotes. And  template engines like this are not context aware.   The encoding doesn’t know if this is  just an HTML context or an attribute.   But the encoding is generally safe, just not in  this particular context. Because if an attribute   is missing quotes, there are no special characters  needed to get a XSS here. If the screenshot title   contains a space, we can break out of the alt  attribute. We can add additional attributes.   For example “onload”, which is executed  once the image is loaded. Boom XSS!  Okay, so this is a XSS you can easily  find by basic static code review,   if you have the experience about the typical  pitfalls of template engines like this.   If you didn’t know that, now you now. Okay the XSS is great, but once you think about   how it could be used, you realize it  seems useless. You need to be able to   control the title of a website requested  as screenshots. And the flagger user   is requesting cscg.de. So how would you influence  the title of that page. It seems impossible   to XSS the flagger user this way. But who knows. This is just one puzzle   piece that we can put into our pockets. And  maybe it can become useful later. Let’s see.  Now we cannot attack the flagger user, but could  we just as a proof of concept attack ourselves? A   self-xss? Well… if we could make screenshots of  arbitrary websites, then we could just create a   website with a malicious title. And here is  another puzzle piece you might have noticed   earlier. The check for the allowed URLs is  done with .startswith. This means we can   fully bypass the domain check. We could just use  our own domain and use it as a subdomain. So for   example http://cscg.de.liveoverflow.com. This URL  would be allowed! But there is one more trick,   actually you don’t need your own domain,  you can also use @. @ separates the URL   into a username part and the actual domain. So  http://cscg.de@liveoverflow.com would simply   point to liveoverflow.com, and cscg.de would be  a username that is just ignored. Let’s try it!  Submit the test URL, and requesting  screenshot - It seems to work! Let’s   give this a moment. And here we go! We  got a screenshot from liveoverflow.com.  Okay with this bypass we can now test the XSS. To do that I first create a test HTML page   with the XSS payload in the title, then I start a  local test server using python, which will listen   on port 8000. But locally accessible doesn’t help  us. The chrome service needs to reach it. So then   I’m using ngrok to get a public domain that is  tunneled to my local test server. So by requesting   cscg.de and the ngrok tunnel URL, I can make the  chrome service request my test index.html page.   And of course processing the screenshot takes  a moment, but BOOM! Here we go. We got a XSS.  But unfortunately we still don’t know how  we could XSS the flagger user with that,   because right now it’s just a self-xss. But let’s  see. More puzzle pieces. More is always better. So what else could we do with  the domain bypass. Any ideas?   Well… now we have a pretty much unrestricted  SSRF (server-side request forgery) to request   screenshots from arbitrary websites. Is  there any internal service we could try   to make screenshots of? Maybe the chrome  service? Let’s try it and see what happens.   We just have to take the exposed chrome IP  and the port 9222. Let’s submit and wait…   as always takes a moment. And here we go. We  got a screenshot. “Inspectable WebContents”! Mh…  So this is maybe where a bit of research has  to start. Probably many of you don’t know the   chrome devtools protocol but you can find a lot  of resources about it online. Or you can also play   with it yourself. To do that you could change the  docker-compose file to expose this port, and then   restart the whole service, now you have direct  test access to it in the browser for testing.   So here it is. Port 9222. Inspectable WEbContents.  And it takes a moment but then shows about:blank.   And in the devtools we can see that it  accesses an API endpoint json/list. Here it is.  Title about:blank. And a websocket debug URL  for it. As well as a devtools frontend url.  And when you refresh it a few times, for example  when you request a screenshot, or when flagger   requests a screenshot, another entry appears.  Here is suddenly a page requesting cscg.de!  So you can use the domain check bypass,  to request a screenshot of this json data,   and it will leak some information about what  pages the chrome service currently has open.  Let’s do another test. We can wait for flagger  to request a screenshot of cscg.de and once we   see the message in the activity log, we can  quickly submit our URL to the list of pages.   We wait for the screenshot, and here it  is. Here is the page where we requested   the list of pages and here we have  flagger requesting the cscg.de.   But what exactly could we do with the  webSocketDebuggerURL? This requires a bit of   research, but if you for example google that field  name, you might find references to puppeteer.   And we learned earlier that puppeteer is a  library to automate and control chrome. So   could we maybe use this debugger URL to control  chrome? Like we also did in python with pypeteer?  That is exactly what we can do. This protocol  is used by for example the chrome devtools,   so with this you can do EVERYTHING the  chrome devtools can do. Including for example   redirecting the page to a different site. A  site maybe with a XSS payload in the title. So here is a rough attack idea. We could request  a screenshot from the chrome debugger list,   to get the secret debug URL of the page flagger  is requesting. We can then probably use this   debug protocol, to redirect the page away from  cscg.de to a site with a XSS payload. When the   screenshot is done processing, the XSS payload  will be placed into the list of notes for flagger,   and when flagger then looks at the notes, it will  be executed. And this XSS payload then just has   to steal the private notes. This sounds like  a really good plan. Except there is one issue.  If flagger requests a screenshot, and then we  request a screenshot of the debug list, the chrome   service will load the list fine. But it will wait  10 seconds before actually taking the screenshot.  So after 10 seconds, the service takes a  screenshot of cscg.de, and then it will take a   screnshot of the page list. Now we have the secret  debug URL, BUT also the service is done processing   it and this page doesn’t exist anymore. So is  this useless? we cannot do anything with that?  Well, now you need a bit of creativity. Can you  think of a way to still make this attack work?   There is a small trick. And it actually took me  a bit to come up with this myself. So pause if   you want to think about this. Otherwise, I  will share how my exploit works. 3.2.1. Ok First we get the CHROME_IP from the activity log.  And then we wait until flagger deleted a note.   This is needed to get the right  timing. This message means,   soon flagger will request a screenshot.  And here is the trick. We are timing our   screenshot now to be started BEFORE flagger  requests cscg.de. But we are not requesting   the page list. We request this index.html  site. And this site simply has a timeout,   so it’s waiting for 7 seconds, and THEN redirects  to the endpoint leaking the debugger URLs.  So we request our screenshot, the index.html  is loaded. Flagger requests cscg.de.  After 7 seconds we redirect to the debugger port  list, and after 10 seconds the service takes a   screenshot of it. AND THIS WILL now contain  the flagger cscg.de debug URL. But thanks   to our precise timing, the chrome service still  hasn’t completed the screenshot for cscg.de yet.  Now we need to be fast! very quickly we need to  get the debug URL. And I’m using OCR for this,   specifically tessarect. This can extract text, or  the URL from the image. And once we got the secret   debug URL, we can now launch our actual attack.  We request another screenshot with our attack.html   page. And this now creates a new websocket  connection to the debug URL and executes a   Page.navigate. So we force the browser to navigate  to this XSS page. Now this page does not show   cscg.de anymore. Which means after the 10 seconds  are over for the flagger screenshot request, the   screenshot is created of the xss.html page and we  successfully got a XSS into the notes of flagger. This XSS payload is simple. It just takes the  whole document.body text, so the complete text   of the website, and sends it with a GET request  to a webserver of mine. Leaking the flag! That’s it. This was the challenge screenshotter. Let me know what you think. I really like  this challenge, because it is based on   an attack I found on a real web app. There is  no guessing, it’s not blackbox, you have the   complete code available, so I think it’s a great  challenge to practice doing security audits.   I hope you enjoyed this challenge, and  congratulations to everybody who solved it.
Info
Channel: LiveOverflow
Views: 149,377
Rating: undefined out of 5
Keywords: Live Overflow, liveoverflow, hacking tutorial, how to hack, exploit tutorial, ctf, cscg, capture the flag, cyber security challenge, germany, ecsc, european cyber, cybersecurity, web hacking, bug bounty, headless chrome, docker, container, internal service, ssrf, self-xss, flask, jinja2, template engine, chaining bugs, chain bugs, walkthrough, solution, hacking tutorial 2021, ethical hacking, bug bounty hunting, self-xss attack
Id: FCjMoPpOPYI
Channel Id: undefined
Length: 22min 48sec (1368 seconds)
Published: Thu Aug 19 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.